devthrottle
CLAUDE.md110 starsChanged 4 days agoMITView on GitHub
This is enterprise-level software requiring robust error handling, comprehensive logging, thorough testing, and responsive UI. Full coding standards: docs/CodingStyle.md UI style guide: docs/VisualStyle.md -- All UI changes must comply with this guide. Writing docs about a screen: docs/checking-docs-against-screen-dumps.md -- Check the sentence against the committed accessibility dump for that screen, not the XAML. The XAML says what a screen CAN show; the dump says what it DID show. The dumps are committed in devthrottle_internal at docs/qa/runs/<run>/report.html and anyone can read…
- Commits and pushes
1 other project uses a version of this.
Adopted by
Most recently taken up by
- thefrederiksen/cc-directorword for word, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 1
- thefrederiksen/cc-director★ 110100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository30 other files it gives its agents.
- agent-expertSkill
- cc-settings-apiSkill
- cencon-generateSkill
- code-maintenanceSkill
- commitSkill
- dead-code-finderSkill
- deploy-hosted-gatewaySkill
- developer-agentSkill
- dev-throttleSkill
- document-featuresSkill
- enter-issueSkill
- fleet-commsSkill
- implementation-loopSkill
- move-sessionSkill
- playwright-cliSkill
- product-agentSkill
- qa-agentSkill
- read-issuesSkill
- release-managerSkill
- review-codeSkill
- shipSkill
- skill-managerSkill
- support-agentSkill
- terminologySkill
- test-managerSkill
- ui-testSkill
- update-docsSkill
- voice-reviewSkill
- warning-fixerSkill
- wingman-brief-reviewerSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
18 rules. About 0 are common, 12 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈12
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 60
Ask the user if they want to close the application themselves
- Distinctive: line 61
NEVER automatically kill processes
- Distinctive: line 71
When cc-director.exe is launched from inside your claude.exe ConPty, the child claude.exe processes IT spawns inherit a nested …
- Very specific: line 118
When you must force-kill as a last resort: only kill a process whose path matches the slot YOU launched (e.g.
- Distinctive: line 136
How to apply: put both in the same command that starts the Gateway, so no run can forget them:
- Distinctive: line 152
Use INotifyPropertyChanged to update UI when data arrives
- Distinctive: line 153
NEVER block the UI thread with synchronous I/O
- Distinctive: line 219
Put try-catch ONLY in:
- Distinctive: line 224
Do NOT put try-catch in helper methods or service methods.
- Distinctive: line 228
All public methods need unit tests
- Distinctive: line 229
All bug fixes need regression tests
- Very specific: line 261
Run the local gate: Use the script, not a hand-rolled dotnet test - it is the ONE place the whole fleet gets faster when the suite …
- Distinctive: line 356
TEST RULE in the script's header has the whole rule.
- Very specific: line 429
When using any cc-* tool, check docs/cli-reference.md for exact flags before calling.
- Very specific: line 447
Never cut an id or a name short in list output.
- Very specific: line 465
All three ship in ONE container image - the Cockpit and the mobile app are built into the Gateway image (wwwroot/c and wwwroot/mobile, see …
- Distinctive: line 488
If the skill cannot do what is needed, that is a gap to FIX IN THE WORKFLOW.
- Very specific: line 552
How to apply: Write commits and PRs as Soren.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CC Director - Project Instructions
This is enterprise-level software requiring robust error handling, comprehensive logging, thorough testing, and responsive UI.
Full coding standards: docs/CodingStyle.md
UI style guide: docs/VisualStyle.md -- All UI changes must comply with this guide.
Writing docs about a screen: docs/checking-docs-against-screen-dumps.md -- Check the sentence against the committed accessibility dump for that screen, not the XAML. The XAML says what a screen CAN show; the dump says what it DID show. The dumps are committed in devthrottle_internal at docs/qa/runs/<run>/report.html and anyone can read them without a machine.
Critical Rules
00. NEVER READ OR WORK FROM A STALE CHECKOUT - THIS IS RULE ZERO
A checkout that is behind origin/main is a lie. Reading it and reporting what you find as fact is the single most expensive mistake we make. Verify freshness BEFORE you read a single file.
This has already cost real time, repeatedly, in one day:
- A session read
GatewayEndpoints.csin a tree 53 commits behind, concluded a shipped feature "was never merged or was reverted", and escalated. It had shipped days earlier. - Another session read
session_ops.pyin a tree 55 commits behind, concluded a fixed file was still broken, and wrote that into a GitHub issue comment as evidence. The fix was already on main.
Both sessions were confident. Both were wrong. Both were caught only because a third party double-checked. Do not rely on being double-checked.
The rule:
- Before reading code to answer a question, check the tree. The
SessionStarthook runsscripts/check-tree-freshness.ps1and fails loud when this tree is behind origin/main. If you see that banner, believe it. Run it yourself any time you are unsure:powershell -NoProfile -File scripts\check-tree-freshness.ps1 - To READ shipped code, read origin/main directly - never the working tree:
git fetch origin git show origin/main:path/to/file.cs git grep <pattern> origin/main - To WORK - edit, build, open a pull request - cut a worktree from origin/main. Never work in the shared checkout, and never
git checkout -bin it:git fetch origin git worktree add ../<repo>-<task> -b <branch-name> origin/main - Never
git pullthe shared checkout to "fix" staleness. Other sessions and agents are working in it; pulling underneath them breaks them. Cut a worktree instead. - A branch is stale the moment origin/main moves past it. Rebase onto origin/main or cut a fresh worktree. Do not build on a base you fetched hours ago.
When you state a fact about the code, it must come from origin/main or a worktree cut from it. If you cannot say which, you do not know the fact - go and check. Saying "the code says X" when X came from a stale tree is reporting fiction as evidence.
0a. WRITE IN PLAIN ENGLISH - NO ABBREVIATIONS
Never use abbreviations, acronyms, initialisms, or jargon. Write out the full words in plain English, everywhere.
This applies to chat replies, issue text, commit messages, code comments, reports, documentation - all output. Spell out what you mean. Say "pull request" not "PR", "the readiness checklist" not "DoR", and so on. If a short form exists, do not use it - use the ordinary words. Clarity over brevity, always. Do not be clever or terse at the cost of being understood.
0. NEVER KILL RUNNING PROCESSES WITHOUT PERMISSION
ABSOLUTELY NEVER use taskkill or any command to terminate cc-director.exe or any other running application without explicit user approval.
The user runs multiple instances of cc-director simultaneously. Killing processes to "fix" build errors is NOT acceptable. If a build fails due to locked files:
- Tell the user the build failed because files are locked
- Ask the user if they want to close the application themselves
- NEVER automatically kill processes
This rule has NO exceptions.
0b. LAUNCH cc-director.exe VIA WINDOWS TASK SCHEDULER, NEVER DIRECTLY
If you (the Claude agent) are running inside a Claude Code CLI session (you almost always are), DO NOT spawn cc-director.exe from your own process tree. Use the cc-director-launch Windows scheduled task instead.
Why
When cc-director.exe is launched from inside your claude.exe ConPty, the child claude.exe processes IT spawns inherit a nested pseudo-console. Grandchild claudes detect this as a non-TTY environment and exit within ~3 seconds with:
Error: Input must be provided either through stdin or as a prompt argument when using --print
This is claude.exe 2.1.143+ behavior on nested ConPty, not a CC Director bug.
The fix: Task Scheduler
Processes launched by Task Scheduler run under svchost.exe (the Schedule service), completely outside your ConPty. Grandchild claudes spawned by such a Director have clean stdio and survive.
One-time setup (idempotent, safe to re-run):
# Point the task at your current test build. The WorkingDirectory must be set, or
# Avalonia's first-time resource resolution may fail with exit -1.
$exe = "C:\repos\cc-director\scripts\local-build\cc-director5.exe"
$wd = "C:\repos\cc-director\scripts\local-build"
$action = New-ScheduledTaskAction -Execute $exe -WorkingDirectory $wd
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddYears(5) # far future, on-demand only
Register-ScheduledTask -TaskName "cc-director-launch" -Action $action -Trigger $trigger -ForceTo launch on demand:
Start-ScheduledTask -TaskName "cc-director-launch"The Director boots with parent = svchost.exe and binds NOTHING - the remove-the-network-port mission deleted its Control API, so there is no port to find and no REST to drive. Readiness is the instance registration the running process writes (<root>\instances\<slug>\config\director\instances\<directorId>.json, whose Pid is your process); its log is at %LOCALAPPDATA%\cc-director\logs\director\director-YYYY-MM-DD-<PID>.log. Drive a test Director through the Gateway it is connected to, and stop it with the named signal described below.
Slot convention to avoid colliding with the user's running Directors
The scripts\local-build directory holds development slots cc-director1.exe through cc-director4.exe (the main daily-driver cc-director.exe is no longer a local build - it is INSTALLED from a release into %LOCALAPPDATA%\cc-director\app and auto-updates in place). The user keeps long-lived Director processes running from the installed app and from slots 1-4, and you MUST NOT kill any of them. Reserve slot 5 or higher for your own test Directors. Build to that slot with scripts\local-build-avalonia.ps1 -Slot 5 -OutputDir "<repo>\scripts\local-build" (the default -OutputDir is the dead repo-root local_builds\ - always pass scripts\local-build) and point cc-director-launch at that path.
Cleaning up your own test Director
Shut a test Director down cleanly, do NOT force-kill it. Signal it — that makes the Director kill its own sessions and delete its crash journal, so it does not leave a phantom "interrupted" entry in the fleet (issue #960). A force-kill (Stop-Process -Force) gives the process no chance to clean up and DOES leave that phantom journal, so it is the last resort only — use it only if the signalled shutdown does not exit in time (the Director is genuinely stuck). The scripts\agent-session-isolation.ps1 teardown verb already does this.
There is no POST /shutdown any more, and there is no credential to resolve. Stopping a Director is a named signal, because it has to work when no socket is accepting connections. The signal is named for THAT Director's own identifier, which is the only string that names one process — this machine runs several Directors, so a request that could reach any of them would eventually reach the wrong one. Read the identifier out of the instance registration of the process you launched (<root>\instances\<slug>\config\director\instances\<directorId>.json, the file whose Pid is your Director), then:
$evt = [System.Threading.EventWaitHandle]::OpenExisting("Local\cc-director-shutdown-$directorId")
$evt.Set(); $evt.Dispose()OpenExisting throwing means nothing is listening — that Director cannot be asked to stop, and only then is a force-kill the answer. scripts\agent-session-isolation.ps1 (Get-DirectorIdForPid, Stop-DirectorCleanly) is the working reference.
When you must force-kill as a last resort: only kill a process whose path matches the slot YOU launched (e.g. cc-director5.exe). Confirm via Get-Process | Select-Object Id, ProcessName, Path first. Never use a blanket Stop-Process -Name cc-director* — that would kill the main build and the user's working sessions.
For non-session-creating tests (HTML rendering, REST endpoint smoke, build-only verification) launching from your context is still fine. Only session-creation tests need the Task Scheduler path.
0c. A TEST GATEWAY CHANGES NOTHING ON THE MACHINE: SET CC_GATEWAY_NO_TAILSCALE=1
Every Gateway you start yourself on this machine - dotnet run, dotnet CcDirector.Gateway.dll, a rig script, a
proof harness - is started with CC_GATEWAY_NO_TAILSCALE=1 and its own CC_DIRECTOR_ROOT (a scratch folder).
Without the first, a self-host Gateway's start-up REWRITES this machine's tailnet front door (tailscale serve
https 443) to point at the test port, and a timer re-asserts it every 60 seconds while it runs; the old value is
not logged, so it cannot be put back by reading anything. Without the second, it reads and writes the real
configuration under %LOCALAPPDATA%\cc-director.
Why: on 6 November 2026 a diagnosis rig booted a self-host Gateway on port 17881 with neither, and the machine's
front door was left proxying a dead port. The switch is read once, in TailscaleServeProvisioner's constructor
(src/CcDirector.Gateway/Tailscale/TailscaleServeProvisioner.cs): set, the provisioner maps nothing, starts no timer
and never reconciles. A hosted Gateway (CC_GATEWAY_HOSTED=1) never starts it at all.
How to apply: put both in the same command that starts the Gateway, so no run can forget them:
$env:CC_GATEWAY_NO_TAILSCALE = "1"; $env:CC_DIRECTOR_ROOT = "<scratch folder>"; dotnet run --project src\CcDirector.Gateway -- --port <free port>and check the Gateway's log says CC_GATEWAY_NO_TAILSCALE=1; Tailscale Serve auto-provisioning disabled before trusting
the run. Never run tailscale serve yourself to undo a mistake: say so, and the owner restores it.
1. Responsive UI - MANDATORY
Every user action MUST provide immediate visual feedback (<100ms).
- Show dialogs/panels immediately, even if empty
- Display "Loading..." text or spinner for any operation >200ms
- Load expensive data (file I/O, API calls) asynchronously in background
- Use INotifyPropertyChanged to update UI when data arrives
- NEVER block the UI thread with synchronous I/O
// BAD - Blocks UI
public MyDialog()
{
InitializeComponent();
var items = LoadFromDisk(); // FREEZES UI!
ListBox.ItemsSource = items;
}
// GOOD - Immediate response
public MyDialog()
{
InitializeComponent();
LoadingText.Text = "Loading...";
Loaded += async (_, _) =>
{
var items = await Task.Run(() => LoadFromDisk());
ListBox.ItemsSource = items;
LoadingText.Visibility = Visibility.Collapsed;
};
}2. Enterprise Logging - MANDATORY
Every public method must log entry, exit, and errors.
public Session CreateSession(string repoPath)
{
FileLog.Write($"[SessionManager] CreateSession: {repoPath}");
try
{
var session = CreateSessionInternal(repoPath);
FileLog.Write($"[SessionManager] Session created: {session.Id}");
return session;
}
catch (Exception ex)
{
FileLog.Write($"[SessionManager] CreateSession FAILED: {ex.Message}");
throw;
}
}3. No Fallback Programming
Fix root causes, don't add fallbacks that hide problems.
// BAD
try { return GetValue(); }
catch { return "Unknown"; } // Hides the real problem!
// GOOD
var value = GetValue();
if (value is null)
throw new InvalidOperationException("Value not available");
return value;4. Try-Catch at Entry Points Only
Put try-catch ONLY in:
- Event handlers (button clicks)
- Lifecycle methods (Loaded, Initialized)
- External event subscriptions
Do NOT put try-catch in helper methods or service methods.
5. Testing Required
- All public methods need unit tests
- All bug fixes need regression tests
- Use Arrange-Act-Assert pattern
- Name tests:
MethodName_Scenario_ExpectedResult
5a. LOCAL RUN, THEN A REVIEW, THEN MERGE - NEVER WAIT FOR CONTINUOUS INTEGRATION
The gate is two things, both of which happen here: .\scripts\test-local.ps1 goes green, and a
reviewer from a different agent family reads the change. Then merge. Nothing waits on GitHub.
THE TEST STANDARD (owner ruling, 11 October 2026). A test failure means a code defect - a sensor
that lights at random is worse than no sensor. So a test in a gate suite never lets Docker, a database
server, a real port, a real process, a sleep or the real clock decide whether it passes: it uses a fake
(an in-memory host or store, TestServer, an injected clock) or it is deleted. The whole release gate
finishes in 90 minutes or less. When a gate fails, re-run ONLY its failed tests (-RerunFailed, below),
never the whole gate.
A TEST WORKS OR IT IS REMOVED (owner ruling, 13 October 2026). A test that fails while the code is
correct is DELETED, the first time, with no repair attempt - pre-approved, no question to ask. The one
exception is a failure that shows a real product defect: then the product is fixed and the test stays
(issue #3965 is the worked example). In prose, do not call such a test "flaky"; say it failed on correct
code. (FLAKY stays as the NAME of the script's verdict, its record and its -ClearFlaky flag.) Cut by
test, not by file: clean tests in the same file stay. Only a real wait on the machine condemns a test,
never the mention of one: a test whose source merely names an address, reads Stopwatch.Frequency or
builds a connection string without opening it does not depend on the machine and stays. What a deleted test covered,
if it matters, is new work: a test that cannot depend on the machine.
There is no longer any case in which you hold a merge open waiting for a continuous integration result - not a release, not a change to the build or to continuous integration itself, not a cross-platform change. That exception list is gone deliberately. Waiting fifty minutes for an answer you already have locally was the single largest source of dead time in this repository (issue #1156), and every exception written into the rule was an invitation to pay it again.
Run the local gate. Use the script, not a hand-rolled
dotnet test- it is the ONE place the whole fleet gets faster when the suite improves, and a convention each caller re-implements cannot be improved centrally. A red local run is a red change; fix it before going further.Know exactly what the default run covers, because it is not everything:
- It runs every suite that fits the two-minute budget - 4,144 tests including the installer, as
measured on 2026-10-10 (the header of
scripts\test-local.ps1carries the current counts). - Three suites are PARKED and do NOT run by default -
Gateway.Tests(host-bound; about 10 minutes as four concurrent processes),Core.Tests(far outside the budget), andGateway.UnitTests(grew past the ceiling; issue #2824). Run them with-Parked. - The default run DOES run the parked tests your change can reach (since 17 October 2026), in
Gateway.UnitTestsandCore.Tests:scripts\select-parked-tests.ps1picks every test class whose file names a type declared in a changed file, and they run after the default suites, with no ceiling. It cannot see a test that reaches your code only through a shared helper; those are caught by the full run on GitHub every two hours, and a red there is fixed at once.Gateway.Testsis not selected - if your change reaches it, the COVERAGE GAP line says so - run-Gateway. -Fastis a no-op, retained only for callers that still pass it. The default is the fast run. Do not claim a change was gated by-Fast; it means nothing.- It runs no web tests and no Python tests at all.
So a regression inside a parked suite, the browser shells, or the Python toolbelt can reach main with a green local run. If your change touches any of those, run
-Parkedor the relevant suite yourself - do not let "the gate was green" stand in for coverage it never had.- It runs every suite that fits the two-minute budget - 4,144 tests including the installer, as
measured on 2026-10-10 (the header of
Get the change reviewed by a different agent family. The author is the last to see the defect, so the reviewer must not be the writer. The order is fixed (owner ruling, 28 September 2026): Codex first; if Codex cannot do it, Claude Code on Fable; if Fable cannot either, Claude Code on Opus. Never Pi, Grok or Gemini. Read the Codex seat's screen within a minute - a usage limit or an unsupported model looks like a waiting seat - and switch at once. The reviewer runs as a real tracked session, never backgrounded and never hidden:
cc-devthrottle session spawn <repo> --controlled-by self --agent Codex --prompt "<what to review>" --name "<Mission> - Reviewer - <what>" cc-devthrottle session spawn <repo> --controlled-by self --agent ClaudeCode --args "--model claude-fable-5-1 --dangerously-skip-permissions" --prompt "<what to review>" --name "<Mission> - Reviewer - <what>"- Merge.
gh pr merge <number> --squash --delete-branch, then park the checkout back on main.
When continuous integration goes red afterwards, fix it forward immediately. That is the whole trade, and it only works if the red is actually chased: a red that is left standing turns the backstop into noise, and then nobody looks at it at all. Chase it the moment you see it.
Chasing a red is not the same as waiting for a green. Nothing is ever held open pending a
check. But the web and Python jobs are the ONLY place those tests run at all, and since 30
September 2026 CI no longer starts on its own - it runs every two hours when main has moved, and
when asked. So if you touched
the browser shells or the Python toolbelt, start it yourself right after merging
(gh workflow run CI --ref main). Merge without waiting; come back for the answer.
A release is a tag on the LAST GOOD COMMIT, and it runs no tests (the owner, 17 October 2026).
The full suite - Release build, every suite including the parked ones - runs on main by itself in
.github/workflows/ci.yml, every two hours when main has moved. Every commit it passes is a last good
commit. scripts\new-release.ps1 -Release adds ONE commit on top of the newest of them, holding only the
version in Directory.Build.props and the notes file, tags it, and merges it back to main with a merge
commit. The release workflow's first job refuses a tag on anything else. Nothing is tested at release
time, so nothing at release time can go red. Why: v2.23.0 took fifteen hours under the old flow,
which made a new release commit, tested it from scratch, and cut a new candidate on every red - while
GitHub's full run had already passed the code. The release-manager skill holds the steps.
So a red full run on main is an emergency. It opens an issue; fix or revert the change at once, before other work. A main that stays red has no new last good commit, so nothing new can be released.
The local -Parked run is still the way to run the three parked suites on your own machine before you
merge a change that touches them:
.\scripts\test-local.ps1 -Parked -Configuration Release-Parked adds the three skipped suites. -Configuration Release matches what users download,
because the script defaults to Debug while the continuous integration job it replaced ran Release.
Every test process writes its temporary files inside the run folder, and they are deleted when the
run ends. Test runs had left tens of thousands of folders in the user's TEMP and filled the disk under
the v2.18.0 gate, where every failure in two suites was "not enough space on the disk". A -Parked run
refuses to start (exit 7) with too little free space.
The gate starts no database, and needs no Docker - not by default, not with -Parked. Every test
that needs a real PostgreSQL lives in its own project, src\CcDirector.Gateway.DatabaseTests, and
.\scripts\test-database.ps1 is the only thing that runs it: it builds a throwaway PostgreSQL in Docker
for its own run, runs only that project, and destroys the database after. It stops if Docker is not
running, and it fails a run in which any test skipped, because under it every test has its database and a
skip reads exactly like a pass in every report we produce. A plain dotnet test of the solution reports
those tests SKIPPED with a reason naming the script. Whether a release also runs that script is the
release process's decision, not this gate's.
The installer IS covered. This section once said the gate was THREE commands, with the installer
suites run by hand. The script names all three installer suites in its own project list -
tools\cc-director-setup.Tests, tools\cc-director-setup-engine.Tests and
tools\cc-director-setup-cli.Tests - so extra dotnet test commands only re-run suites the gate
already ran. Read the project list in scripts\test-local.ps1 rather than a count written here.
If you are about to release, run the one command.
The frozen candidate is retired (17 October 2026). Releases no longer cut a candidate or run a
release gate; see "A release is a tag on the LAST GOOD COMMIT" above. A local -Parked run that is red
is still handled the same way: re-run only its failed tests, once (.\scripts\test-local.ps1
-RerunFailed <run folder>). A test that fails again is a code defect: fix it on main. A test that passes
alone failed on correct code: delete it on main (the rule above). Tests tagged
[Trait("Gate", "Quarantine")] never run in a gate; -Quarantine runs them without a verdict. THE FLAKY
TEST RULE in the script's header has the whole rule.
One release seat at a time, enforced. scripts\new-release.ps1 -Claim -Version <v> creates the
branch release-claim only if it does not exist, naming the version and the holder; a second session
is refused and told who holds it, and only the holder can release - one release at a time, of any
version. On 17 October two sessions fixed the same failure in parallel
because nothing stopped a second release seat.
Gate runs in separate worktrees may run at the same time, limited only by the machine's memory
and disk. There is no gate lock: every test process keeps its files under its own temporary root
and every host binds a port the operating system assigns, so two runs in two worktrees share nothing.
The release-gate lock that once refused a second -Parked or -Gateway run with exit 6 was removed
on 17 October 2026 by the owner's decision, after it starved independent gates for over an hour; the
Gateway.Tests suite lock it was added to work around had already gone in October 2026 (#3888).
A one-off workflow is disabled by the pull request that finishes it. A workflow added on a branch
to rehearse or reproduce something stays registered and dispatchable after its file and branch are
gone. Run gh workflow disable <id> as part of finishing that work. On 10 October 2026 eleven such
workflows were still active, cluttering the Actions page where the release run is watched.
6. UI Thread Safety
// ALWAYS dispatch to UI thread for ObservableCollection changes
Dispatcher.BeginInvoke(() =>
{
_sessions.Add(newSession);
});7. THE CLIENT IS DUMB - THE GATEWAY OWNS ALL RULING
Every display verdict is computed on the Gateway and pushed; clients only render it, verbatim. Colors, labels, triage buckets, and the voice-mode display state (badge, message, and which actions are offered) are all FOLDED once on the Gateway and stamped onto the session the client reads. A client never re-derives a verdict, never guesses, never branches to decide what a state "means".
Why: a client that rules for itself will, the moment the Gateway hands it something it did not expect, render something plausible instead of something true. That is exactly how the Voice screen came to show a red "Voice unavailable" badge next to a "Generate narration now" button that could never work: the Gateway sent no reason, so the phone GUESSED "offer a button". A dumb client cannot guess. See docs/new_architecture/sessions.html.
How to apply: compute the verdict in one Gateway place (e.g. SessionOrdering for color/label/triage, VoiceDisplayFold for the voice screen), put the finished strings and booleans on the DTO, and have the client read them. If you find yourself writing a conditional in a .tsx/.xaml view that decides what a state means (as opposed to how to lay out what the Gateway already decided), move it to the Gateway. Adding a new state is one edit in the fold, never a new branch in every client.
8. SETTINGS IS ONE PAGE ON TWO SURFACES - IT MUST STAY IN SYNC
The Cockpit and the mobile app show the same Settings: the same tabs, in the same order, with the same names, and cards that look the same. The desktop has more room and may show MORE DETAIL within a tab; the phone may show a reduced version of the same card. Neither may have a tab, a setting, or a name the other does not.
Why: they were allowed to drift and became two different products for one account. The Cockpit had Notifications / AI / Car Mode with the dictation checks on a separate page entirely; the phone had a single untabbed "AI settings" scroll with no notification settings, no Car Mode end phrase, and the two checks on standalone screens. Found on the phone, it read as broken. Two lists in two files drift apart by default; one list in one file cannot.
How to apply: the tab set and every card live ONCE, in packages/client-core/src/settings/ (tabs.ts, SettingsTabs.tsx, and the four tab components), and both shells mount them. Each shell supplies only its own frame - page heading and left rail on the desktop, back link and app bar on the phone - and its own layout tuning: the phone re-tunes the shared settings-* CSS for touch under a .screen ancestor, so LAYOUT differs and CONTENT cannot. A route that exists on one surface only (the account page, the Transcription Health report) is passed in as an optional href, never hard-coded, so no surface renders a dead link. Adding a setting means adding it to the shared component - if you find yourself editing a settings card inside apps/cockpit or apps/mobile, stop: it belongs in client-core.
Naming Conventions
| Type | Convention | Example |
| Classes | PascalCase + suffix | SessionManager, ConPtyBackend |
| Methods | Verb + Noun | CreateSession(), SendTextAsync() |
| Private fields | _camelCase | _sessionManager, _sessions |
| Async methods | Suffix Async | KillSessionAsync() |
| Tests | Method_Scenario_Result | CreateSession_InvalidPath_Throws |
Logging Format
FileLog.Write($"[ClassName] MethodName: context={value}, result={result}");
FileLog.Write($"[ClassName] MethodName FAILED: {ex.Message}");CC Director CLI Tools
Reference: docs/cli-reference.md
When using any cc-* tool, check docs/cli-reference.md for exact flags before calling. Key gotcha: use --count / -n for result limits, NOT --limit.
COMMAND-LINE TOOLS FOLLOW THE AXI STANDARD
cc-devthrottle follows AXI (Agent eXperience Interface): https://axi.md. AXI was created by
Kun Chen (https://github.com/kunchenguid/axi). Every change to a cc-devthrottle command must meet
it, and new command-line tools should be built to it from the start. Our checklist, the rules that
do not bend, and what "done" means are in docs/axi-standard.md. Tracking
issue: #2922.
Why: agents run these tools far more than people do. Measured on session list (#2920, 90 agent
runs): 54% fewer tokens, 60% fewer commands, 63% less time, and no loss of accuracy.
The rules most often broken, so you recognise them:
--jsonkeeps its shape - other code parses it - and every filter applies to--jsontoo. A filter that is silently ignored is a defect, and so is an unknown flag that does not fail.- Never cut an id or a name short in list output. An agent cannot act on
devthr.... - An empty result says so (
count: 0), never blank output. - It must work on Windows, macOS and Linux; the
cc-devthrottletests run on all three.
When in Doubt
- Log more, not less
- Fail explicitly, not silently
- Show UI feedback immediately
- Write a test
- Read docs/CodingStyle.md
THERE IS EXACTLY ONE WAY TO DEPLOY THE GATEWAY, THE COCKPIT AND THE MOBILE APP
Use the deploy-hosted-gateway skill. Never any other way.
All three ship in ONE container image - the Cockpit and the mobile app are built into the Gateway image
(wwwroot/c and wwwroot/mobile, see the repo-root Dockerfile). "Deploy the Cockpit" and "deploy
mobile" mean this same one path. There is no separate Cockpit deploy and no separate mobile deploy.
FORBIDDEN, for every agent:
az webapp config container set,az webapp restart,az webapp deployment slot swap, or any other hand-rolledazcommand againstdevthrottle-gw- deploying from the Azure Portal
- building an image and pinning it by hand
Why this is a rule and not a preference. It refuses any ref that is not main. It refuses a commit whose checks have FAILED. It measures the real external outage at ~0.1s resolution and fails the run if it exceeds the budget. And it serialises against the rollback and the other workflows that restart the site, because two containers on one shared file share corrupted a database on 2026-07-30 and took the service down for 32 minutes. A hand-rolled deploy has none of that.
A deploy costs about a minute of outage, on purpose (2026-09-20). The plan is Basic B2 - $25 a month, 2 cores, 3.5 GB - chosen by the owner over the old S1, whose single 1.75 GB worker was so short of memory that deploys were taking 87 to 360 seconds. Basic has no deployment slots, so the staging slot is gone and a deploy is an in-place restart. The budget is 120 seconds, and the owner's ruling is that a KNOWN two-minute outage is acceptable while there are no users. Zero-outage deploys need a slot-capable plan (Premium v3 P0v3, 4 GB, ~$57) plus the warmed-swap workflows from git history before that date.
If the skill cannot do what is needed, that is a gap to FIX IN THE WORKFLOW. Say so and stop.
Rolling back is also a workflow (rollback-hosted-gateway.yml): it puts a previous commit's image
back. Never pin an image by hand.
A BUILT-IN SKILL IS CHANGED IN ONE PLACE, AND IT REACHES PEOPLE BY DEPLOYING THE GATEWAY
The four skills DevThrottle ships - dev-throttle, fleet-comms, move-session, terminology -
have one source: src/CcDirector.Gateway/Skills/Content/<id>.skill.md. That file is compiled
into the Gateway binary, seeded into the skill store at startup, and pulled down by every Director
onto disk where agents read it.
.claude/skills/<id>/SKILL.md is NOT a second copy to maintain. It exists only so Claude Code
finds the skill while working inside this repository, and it is the shipped body plus the YAML
frontmatter Claude Code requires. It is REGENERATED from the shipped file. Do not hand-edit it.
You cannot change a built-in on the Gateway instead. SkillStore holds built-ins READ-ONLY,
refuses a tenant skill created under a built-in id, and the seeder is the only writer of their
content - so skill push or an edit in the store is either refused or erased on the next Gateway
start. The Gateway is where the skill is SERVED from, not where it is authored.
The loop, and there is no shorter one:
- Edit
src/CcDirector.Gateway/Skills/Content/<id>.skill.md. - Regenerate the
.claude/skills/<id>/SKILL.mdcopy from it (frontmatter + body, nothing else). - Merge.
- Deploy the Gateway - the
deploy-hosted-gatewayskill. The seeder republishes the body as a new version, in both directions, so a rollback republishes the older one and the version rows are an honest record of what the fleet was served. - It comes back down to
~/.agents/skillson every machine, and you can read it current.
Why this is a rule and not tidiness. On 2026-09-14 the two copies had drifted apart in BOTH
directions. The shipped fleet-comms showed five spawn examples and never mentioned
--controlled-by or --standalone, while an undeclared spawn had been REFUSED since 2026-09-13 -
so for a day every agent outside this repository was handed examples the product rejects, while the
copy a developer reads was correct. Meanwhile the repository move-session carried a paragraph no
user ever saw, and the shipped dev-throttle carried a browsers section the repository copy had
lost. Each had content the other lacked.
And the shadowing makes it invisible exactly where it would be caught: Claude Code reads a project's
own .claude/skills ahead of the ~/.claude/skills links a Director installs, so inside the
repository where DevThrottle is built, the hand-edited copy wins over the shipped one.
BuiltInSkillsHaveOneSourceTests fails when the two bodies differ.
ShippedSkillsTeachOwnershipTests fails when a shipped skill shows a spawn the product would refuse.
Deleting a .claude/skills copy entirely is allowed and is the cleanest end state - the guard never
demands one exist, only that one which exists agrees.
NEVER MENTION CLAUDE ANYWHERE IN GITHUB - ABSOLUTE
NO Claude / Claude Code / Anthropic / AI attribution EVER appears in anything that touches GitHub, or anywhere else.
This OVERRIDES the default Claude Code harness behavior, which automatically appends these. Ignore that default. It is unsolicited advertising in Soren's repos and it is not acceptable.
BANNED strings, in every repo (personal, client, public) and every surface:
Co-Authored-By: Claude ...(commit message trailers)Generated with [Claude Code](https://claude.com/claude-code)(PR/issue bodies)- The robot-emoji "Generated with" footer, anywhere
- Any mention of Claude, Claude Code, or Anthropic in commit messages, PR titles or bodies, issue text, review comments, code comments, changelogs, or docs
How to apply: Write commits and PRs as Soren. No trailer. No footer. Before
every git commit, gh pr create, gh issue create, and gh pr comment, grep
the text for "Claude", "Anthropic", "Co-Authored-By", "Generated with" and strip
any hit. If attribution reaches a commit that is not yet pushed, amend it before
it goes anywhere near GitHub.
THE SPOKEN WORDS REACH THE AGENT VERBATIM - HARD RULE FOR EVERY TRANSCRIPTION PATH
What the user said is what the agent gets. Only TWO changes are allowed between the speech model's output and the agent's prompt: a deterministic, in-process, dictionary find/replace of individual terms the user listed by hand, and the removal of a sentence the audio does not support.
The removal, in full (the owner's amendment, 2026-09-10): a sentence may be
removed only when the audio in its own time span contains no speech-level sound;
nothing may ever be added or reworded. It exists because whisper-large-v3 must
caption every part of a clip and cannot answer "nothing was said here", so on a silent
stretch it writes the likeliest caption from its training data - "Thank you.", "you",
"Bye." - and about one dictation in eight carried a word the speaker never said. A word
the model invents is an instruction nobody gave, so removing it SERVES this rule rather
than bending it. What may never happen is a removal the audio does not justify: see
devthrottle_internal/docs/architecture/transcription-pipeline-versions.md for the rule
and its measured score, and the research behind it in
devthrottle_internal/docs/research/transcription/2026-09-09-unspoken-words.md. Both live in the
INTERNAL repository, not this one.
This binds the Gateway (CcDirector.Gateway/Transcription, CcDirector.Core/Dictation),
the Director's local dictation, the phone app, Notes, Wingman, and the proxy in
devthrottle_internal/website/api/v1/audio.js - every path that turns audio into text.
FORBIDDEN, in any of those paths:
- Any generative model that can reword, translate, summarise, answer, or "clean up"
the transcript. A model that WRITES text is not a transcriber. This includes
post-processing passes (the o4-mini cleanup removed in July 2026) AND generative
speech-to-text models used as the transcriber itself: on 2026-08-29 the primary
provider was
gpt-4o-transcribe, and it emitted invented German ("Koennt ihr dem Aufgabschornel noch geholfen?") and rewrote whole sentences ("Why do you use it that strong now to send email?") for an English speaker. The Gateway's stored RawText matched what hit the screen and CleanupApplied was false - the damage was done INSIDE the speech model, where no downstream rule can catch it. The primary transcriber must be a non-generative acoustic model (whisper-large-v3). - Replacing a word the user did NOT list. The fuzzy matcher did this from 2026-08-08 to 2026-08-17 ("sure" -> "Soren" 261 times, "many" -> "Banya" 138, "code" -> "Codex", "single" -> "SignalR", "focus" -> "Opus") before it was made opt-in and off (#2597). It stays off. A judge model may only ever answer with candidate ids - it never supplies text - and it does not make an unlisted swap allowed; the user's own list does.
- Sending the model a vocabulary
prompt, a language override, or any bias the user did not configure. A hint changes what the model hears.
REQUIRED:
dictation_transcripts.RawTextis the speech model's FULL output byte-for-byte, including any sentence the evidence gate removed.CleanedTextdiffers from it only by acceptedChangedWords, each a hand-listed correction, and by whole sentences the gate dropped - never by a reword and never by an addition. A test must proveRaw + ChangedWords - DroppedSentences == Cleaned.- A removal is only permitted when it is RECORDED. The dropped sentences travel with the result, so a removal is answerable by query exactly as a dictionary edit is. A gate that quietly shrinks a transcript, leaving the evidence only in a log file, is the defect fixed in devthrottle#2805 - not the design.
- The proof of compliance is the stored rows, not the code comments: diff
RawText vs CleanedText over the last 30 days
(see the 2026-08-29 query in devthrottle_internal
docs/incidents/). Any edit whose Find is not in the tenant's Corrections list is a defect, not a feature. - Changing the transcription provider or model is a product decision made by the owner in writing. Never switch to a generative model for "reliability".
Why: the agent acts on the words. A wrong word from the speaker is the speaker's problem; a wrong word from us is an instruction the user never gave.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

