agentleFS
thefrederiksenthefrederiksen/devthrottle/CLAUDE.md

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

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
  1. Distinctive:

    Ask the user if they want to close the application themselves

    line 60
  2. Distinctive:

    NEVER automatically kill processes

    line 61
  3. Distinctive:

    When cc-director.exe is launched from inside your claude.exe ConPty, the child claude.exe processes IT spawns inherit a nested …

    line 71
  4. Very specific:

    When you must force-kill as a last resort: only kill a process whose path matches the slot YOU launched (e.g.

    line 118
  5. Distinctive:

    How to apply: put both in the same command that starts the Gateway, so no run can forget them:

    line 136
  6. Distinctive:

    Use INotifyPropertyChanged to update UI when data arrives

    line 152
  7. Distinctive:

    NEVER block the UI thread with synchronous I/O

    line 153
  8. Distinctive:

    Put try-catch ONLY in:

    line 219
  9. Distinctive:

    Do NOT put try-catch in helper methods or service methods.

    line 224
  10. Distinctive:

    All public methods need unit tests

    line 228
  11. Distinctive:

    All bug fixes need regression tests

    line 229
  12. Very specific:

    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 …

    line 261
  13. Distinctive:

    TEST RULE in the script's header has the whole rule.

    line 356
  14. Very specific:

    When using any cc-* tool, check docs/cli-reference.md for exact flags before calling.

    line 429
  15. Very specific:

    Never cut an id or a name short in list output.

    line 447
  16. Very specific:

    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 …

    line 465
  17. Distinctive:

    If the skill cannot do what is needed, that is a gap to FIX IN THE WORKFLOW.

    line 488
  18. Very specific:

    How to apply: Write commits and PRs as Soren.

    line 552

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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.cs in 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.py in 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:

  1. Before reading code to answer a question, check the tree. The SessionStart hook runs scripts/check-tree-freshness.ps1 and 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
  2. 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
  3. To WORK - edit, build, open a pull request - cut a worktree from origin/main. Never work in the shared checkout, and never git checkout -b in it:
    git fetch origin
    git worktree add ../<repo>-<task> -b <branch-name> origin/main
  4. Never git pull the shared checkout to "fix" staleness. Other sessions and agents are working in it; pulling underneath them breaks them. Cut a worktree instead.
  5. 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 -Force

To 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.

  1. 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.ps1 carries 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), and Gateway.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.UnitTests and Core.Tests: scripts\select-parked-tests.ps1 picks 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.Tests is not selected - if your change reaches it, the COVERAGE GAP line says so - run -Gateway.
    • -Fast is 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 -Parked or the relevant suite yourself - do not let "the gate was green" stand in for coverage it never had.

  2. 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>"
  3. 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

TypeConventionExample
ClassesPascalCase + suffixSessionManager, ConPtyBackend
MethodsVerb + NounCreateSession(), SendTextAsync()
Private fields_camelCase_sessionManager, _sessions
Async methodsSuffix AsyncKillSessionAsync()
TestsMethod_Scenario_ResultCreateSession_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:

  • --json keeps its shape - other code parses it - and every filter applies to --json too. 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-devthrottle tests run on all three.

When in Doubt

  1. Log more, not less
  2. Fail explicitly, not silently
  3. Show UI feedback immediately
  4. Write a test
  5. 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-rolled az command against devthrottle-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:

  1. Edit src/CcDirector.Gateway/Skills/Content/<id>.skill.md.
  2. Regenerate the .claude/skills/<id>/SKILL.md copy from it (frontmatter + body, nothing else).
  3. Merge.
  4. Deploy the Gateway - the deploy-hosted-gateway skill. 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.
  5. It comes back down to ~/.agents/skills on 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.RawText is the speech model's FULL output byte-for-byte, including any sentence the evidence gate removed. CleanedText differs from it only by accepted ChangedWords, each a hand-listed correction, and by whole sentences the gate dropped - never by a reword and never by an addition. A test must prove Raw + 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.