From a89a4d15ddc76832f23d1eaaebd5ad84fc25653a Mon Sep 17 00:00:00 2001 From: prode Date: Wed, 16 Sep 2026 13:41:55 -0300 Subject: [PATCH 1/3] fix(devcontainer): pin the image's tools and own the credential directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npm install -g ` with no version is a layer whose hash never changes, so Docker serves the first build's answer for as long as the cache survives while the registry moves on — and the rebuild that looks like it fixed that did not. Measured: an image rebuilt the same day carried scc v0.23.0 into a workspace whose settings.json registers the UserPromptSubmit hook v0.24.0 added, so every prompt of the session answered `unknown hook stage "user-prompt"`. The three tools are pinned to build ARGs now; bumping an ARG is what invalidates the layer. SCC_VERSION is the one with a contract to the workspace it runs in — the other two answer to nobody here. And a named volume mounted onto a path the image never created is created root-owned: Docker copies an image path's ownership into a fresh volume only when that path already exists. Without `/home/node/.claude` in the image, the first write of every session was `EACCES ... mkdir '/home/node/.claude/session-env'`, and the login it could not store read back as "not logged in". The image creates and chowns it. Verified: go build, golangci-lint run (0 issues), golangci-lint fmt --diff, the full suite, and `scc validate` (10 validators, 0 findings). Two new tests in internal/cli hold both halves against the seeded files rather than against the prose explaining them. --- .devcontainer/Dockerfile | 34 ++++++++++- internal/assets/assets.go | 15 ++++- internal/assets/assets_test.go | 2 +- .../assets/templates/devcontainer/Dockerfile | 34 ++++++++++- internal/cli/init_test.go | 57 +++++++++++++++++++ 5 files changed, 134 insertions(+), 8 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 16ae745..fdb6a9b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -22,13 +22,41 @@ FROM mcr.microsoft.com/devcontainers/javascript-node:22 # failed command at a time and falls back to reading whole files — the exact cost # this methodology exists to remove. # +# Pinned, and that is not tidiness. `npm install -g ` with no version is a +# layer whose hash never changes, so Docker serves the first build's answer for as +# long as the cache survives while the registry and this workspace both move on — +# and the rebuild that looks like it fixed that did not. Measured: an image rebuilt +# the same day carried scc v0.23.0 into a workspace whose settings.json registers +# the UserPromptSubmit hook v0.24.0 added, so every prompt of the session answered +# `unknown hook stage "user-prompt"`. Bumping an ARG is what invalidates the layer. +# +# SCC_VERSION is the one that has to be kept in step, because it is the only one +# of the three with a contract to this repository: scc scaffolds the harness hooks +# in the harness's own settings.json and this scc is what runs them, so a binary +# older than the scaffold is a vocabulary the container does not have. Keep it at +# or above the version that scaffolded the workspace. The other two answer to +# nobody here — Claude Code updates itself at runtime — and are pinned only so a +# rebuild is reproducible. +# # As `node` rather than as root, because this image puts that user's global prefix # (/usr/local/share/npm-global) on PATH and a root install would land somewhere # the agent does not look. +ARG SCC_VERSION=0.25.0 +ARG CLAUDE_CODE_VERSION=2.1.273 +ARG CODEGRAPH_VERSION=1.6.0 RUN su node -c "npm install -g \ - @anthropic-ai/claude-code \ - @protonspy/scc \ - @colbymchenry/codegraph" + @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \ + @protonspy/scc@${SCC_VERSION} \ + @colbymchenry/codegraph@${CODEGRAPH_VERSION}" + +# Where the credential volume lands, created here rather than left to the mount. +# Docker copies an image path's content *and its ownership* into a fresh named +# volume only when that path already exists; mount onto a path the image never +# created and Docker creates it root-owned, so the agent's first write under it +# fails — measured, as an EACCES on the per-session directory it makes at every +# session start, with the login it could not store then reported back as "not +# logged in". +RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude # `gh` — the delivery flow opens the pull request with it, and `scc validate --pr` # reads the PR body through it. diff --git a/internal/assets/assets.go b/internal/assets/assets.go index 462b146..ecf149f 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -270,7 +270,20 @@ import ( // and clear this artifact's findings". That moved into the skill bodies, where it is // paid by the run that invokes it instead of by every session; `argument-hint` moved // with it, since SKILL.md takes the same key. -const Version = "32" +// +// 33: the dev container stops handing the agent a stale binary and a home it cannot +// write. Two failures measured in one session, both of them Docker's defaults rather +// than anything scc decided. An unversioned `npm install -g` is a layer whose hash +// never changes, so an image rebuilt the same day still carried scc v0.23.0 while +// this workspace's settings.json registered the UserPromptSubmit hook v0.24.0 added +// — `unknown hook stage "user-prompt"`, on every prompt of the session, from a +// rebuild that looked like it had fixed it. The three tools are pinned to ARGs now, +// and SCC_VERSION is the one with a contract to the workspace it runs in. And a +// named volume mounted onto a path the image never created is created root-owned, so +// `/home/node/.claude` is created and chowned in the image: without it the first +// write of every session was `EACCES ... mkdir '/home/node/.claude/session-env'` and +// the login it could not store read back as "not logged in". +const Version = "33" // The embedded tree. "all:" so nothing is silently dropped for having a name the // default embed pattern skips. diff --git a/internal/assets/assets_test.go b/internal/assets/assets_test.go index 9ac2dbc..f5a016d 100644 --- a/internal/assets/assets_test.go +++ b/internal/assets/assets_test.go @@ -1075,7 +1075,7 @@ func (e errNoSuchRule) Error() string { // uses wherever a change is cheap to make and expensive to make by accident. func TestTheTemplateVersionMovesWithTheTemplates(t *testing.T) { // Bump Version, then replace this with the digest the failure prints. - const fingerprint = "220a7b9ef576f9f83fcfd3f7268952e34cbdc66542ee8be26e4cf755180ca373" + const fingerprint = "5f4a989994ef836fd039f1f188f1027ee4924164a0ae7a6ebd30ce345f6f6353" sum := sha256.New() // Version goes into the hash, and without it this test does not do the job its diff --git a/internal/assets/templates/devcontainer/Dockerfile b/internal/assets/templates/devcontainer/Dockerfile index 29a17e2..38a073d 100644 --- a/internal/assets/templates/devcontainer/Dockerfile +++ b/internal/assets/templates/devcontainer/Dockerfile @@ -22,13 +22,41 @@ FROM mcr.microsoft.com/devcontainers/javascript-node:22 # failed command at a time and falls back to reading whole files — the exact cost # this methodology exists to remove. # +# Pinned, and that is not tidiness. `npm install -g ` with no version is a +# layer whose hash never changes, so Docker serves the first build's answer for as +# long as the cache survives while the registry and this workspace both move on — +# and the rebuild that looks like it fixed that did not. Measured: an image rebuilt +# the same day carried scc v0.23.0 into a workspace whose settings.json registers +# the UserPromptSubmit hook v0.24.0 added, so every prompt of the session answered +# `unknown hook stage "user-prompt"`. Bumping an ARG is what invalidates the layer. +# +# SCC_VERSION is the one that has to be kept in step, because it is the only one +# of the three with a contract to this repository: scc scaffolds the harness hooks +# in the harness's own settings.json and this scc is what runs them, so a binary +# older than the scaffold is a vocabulary the container does not have. Keep it at +# or above the version that scaffolded the workspace. The other two answer to +# nobody here — Claude Code updates itself at runtime — and are pinned only so a +# rebuild is reproducible. +# # As `node` rather than as root, because this image puts that user's global prefix # (/usr/local/share/npm-global) on PATH and a root install would land somewhere # the agent does not look. +ARG SCC_VERSION=0.25.0 +ARG CLAUDE_CODE_VERSION=2.1.273 +ARG CODEGRAPH_VERSION=1.6.0 RUN su node -c "npm install -g \ - @anthropic-ai/claude-code \ - @protonspy/scc \ - @colbymchenry/codegraph" + @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \ + @protonspy/scc@${SCC_VERSION} \ + @colbymchenry/codegraph@${CODEGRAPH_VERSION}" + +# Where the credential volume lands, created here rather than left to the mount. +# Docker copies an image path's content *and its ownership* into a fresh named +# volume only when that path already exists; mount onto a path the image never +# created and Docker creates it root-owned, so the agent's first write under it +# fails — measured, as an EACCES on the per-session directory it makes at every +# session start, with the login it could not store then reported back as "not +# logged in". +RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude # `gh` — the delivery flow opens the pull request with it, and `scc validate --pr` # reads the PR body through it. diff --git a/internal/cli/init_test.go b/internal/cli/init_test.go index 1937538..a45fd4b 100644 --- a/internal/cli/init_test.go +++ b/internal/cli/init_test.go @@ -310,6 +310,63 @@ func TestInitSeedsTheDevContainer(t *testing.T) { } } +// The image pins the one tool whose vocabulary the workspace depends on. +// +// `npm install -g ` with no version is a layer whose hash never changes, so +// Docker keeps serving the first build's answer while the registry moves on — and +// the rebuild that looks like it fixed that did not. Measured: an image rebuilt the +// same day carried scc v0.23.0 into a workspace whose settings.json registered the +// UserPromptSubmit hook a later version added, and every prompt of the session +// answered `unknown hook stage "user-prompt"`. scc is the one of the three with a +// contract to the files around it, so it is the one this test holds; Claude Code +// updates itself at runtime and codegraph answers to nobody here. +func TestTheSeededImagePinsTheToolItMustMatch(t *testing.T) { + root := initWorkspace(t) + docker, err := os.ReadFile(filepath.Join(root, paths.DevcontainerSeg, "Dockerfile")) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + // The install has to name a version, and the version has to be a build arg + // rather than a literal in the RUN line: bumping the arg is what invalidates + // the layer, and a literal buried in a shell string is not a knob. + if !strings.Contains(string(docker), "ARG SCC_VERSION=") { + t.Errorf("the image does not pin scc to a build arg:\n%s", docker) + } + if !strings.Contains(string(docker), "@protonspy/scc@${SCC_VERSION}") { + t.Errorf("the image installs scc unversioned, so Docker's layer cache picks the version:\n%s", docker) + } +} + +// And it creates the directory the credential volume lands on. +// +// Docker copies an image path's content *and its ownership* into a fresh named +// volume only when that path already exists. Mount onto a path the image never +// created and Docker creates it root-owned, so the agent — which runs as node — +// cannot write its own state: measured as `EACCES: permission denied, mkdir +// '/home/node/.claude/session-env'` at every session start, with the login it could +// not store then reported back as "not logged in". +// +// Asserted across both files, because neither states it alone: the Dockerfile +// creates the path and devcontainer.json is what mounts a volume over it. +func TestTheSeededImageOwnsTheCredentialDirectory(t *testing.T) { + root := initWorkspace(t) + docker, err := os.ReadFile(filepath.Join(root, paths.DevcontainerSeg, "Dockerfile")) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + cfg, err := os.ReadFile(filepath.Join(root, paths.DevcontainerSeg, "devcontainer.json")) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + const home = "/home/node/.claude" + if !strings.Contains(string(cfg), "target="+home+",type=volume") { + t.Fatalf("the seeded config does not mount a volume at %s, so this test is checking the wrong path:\n%s", home, cfg) + } + if !strings.Contains(string(docker), "mkdir -p "+home) || !strings.Contains(string(docker), "chown node:node "+home) { + t.Errorf("the image does not create %s as node, so the volume lands root-owned:\n%s", home, docker) + } +} + // The two seeded files describe one container between them, and the one thing // neither can state alone is the user. devcontainer.json names it as remoteUser // and mounts the agent's credentials into that home; the Dockerfile drops to it From bd36aa2761cdeeb5af81d478a8825303f6451010 Mon Sep 17 00:00:00 2001 From: prode Date: Wed, 16 Sep 2026 13:42:00 -0300 Subject: [PATCH 2/3] chore(workspace): record the CodeGraph and RTK usage blocks in the entry file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scc launch` splices both into the entry file the agent is about to load — CodeGraph's under scc's own markers, RTK's under RTK's — and this checkout had been carrying them uncommitted. Committing them is what makes every session here start with the same guidance, rather than the first launch on a fresh clone writing them again. The bytes are the ones the binary writes; nothing in the prose above them changed. --- CLAUDE.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index bce5dad..b254831 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -370,3 +370,38 @@ Tests live beside the code and lean on a few package-local helpers rather than a Conventional Commits, scoped by package or surface, with a descriptive subject written as a claim about behavior — e.g. `feat(cli): return exit 2 when spec validation reports findings`. Changes land through PRs on `main`. **No attribution, in a commit message or a PR body.** No `Co-Authored-By` for an assistant, no session link, no generated-with footer, no naming of a model, vendor, or harness. This is the rule scc ships in `rules/delivery.md`, and it binds scc's own history first — a tool that told every workspace not to sign its work and then signed its own would be worth ignoring on both counts. Harness defaults append these unless told not to, so a session working here overrides that default rather than following it. + + +## CodeGraph +Ask the symbol graph before reading files. "Who calls this", "what breaks if I change it", +"where does this concept live" are one command here and a dozen reads otherwise. + +- `scc graph explore ""` — the relevant symbols' source plus the call paths between them. Start here. +- `scc graph query [--kind function|class] [--limit N]` — find a symbol by name. +- `scc graph status` — what the graph holds. `--check` exits 2 when there is none. +- `scc graph sync` — re-index after you have written code you then need to search. +- `scc graph build [--force]` — first index, or a full rebuild when the graph has gone wrong. + +`scc launch` indexes before the session starts, so the graph is current at turn one. +It goes stale as you edit: sync before searching for something you just wrote. +The graph is CodeGraph's — never edit `.codegraph/`, and never commit it. + + + +## RTK +Prefix EVERY command with `rtk`, including each link in a `&&` chain (`rtk git add . && rtk git commit -m "x"`). +No dedicated filter means it passes through unchanged — always safe. + +Covered: +- cargo build/check/clippy/test, go test, tsc, lint, prettier, next build +- jest, vitest, playwright, pytest, rspec, rake test, test `` +- git (all subcommands) +- gh pr view/checks, gh run list, gh issue list, gh api +- pnpm, npm run, npx, prisma, uv run +- ls, read, grep, find +- err, log, json, deps, env, summary, diff +- docker, kubectl, curl, wget + +Meta: `rtk gain [--history]`, `rtk discover`, `rtk proxy ` (no filtering), `rtk init [--global]` +Caveat: `rtk grep` with `-c -l -L -o -Z` runs raw. + From b347e6f08f560499e5b88e503f0401b0900387b1 Mon Sep 17 00:00:00 2001 From: prode Date: Wed, 16 Sep 2026 13:42:01 -0300 Subject: [PATCH 3/3] docs(skills): narrow gauntlet-loop to scc's own methodology and harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill wrote a gauntlet prompt for any goal, with a named artifact as the bar — screenshot Nike, read the published piece. scc has nothing to screenshot. What it has is the question design/ponytail-port.md closed without answering: nothing measures whether the rules earn the bytes they cost in every request of every session. So the bar becomes an arm: two harness configurations running the same ticket, with the critic blind to which is which, and the control checked for leaks — the operator's global config is where the last one hid. --- .claude/skills/gauntlet-loop/SKILL.md | 250 ++++++++++++++++++-------- 1 file changed, 180 insertions(+), 70 deletions(-) diff --git a/.claude/skills/gauntlet-loop/SKILL.md b/.claude/skills/gauntlet-loop/SKILL.md index 5c33caa..8fab86e 100644 --- a/.claude/skills/gauntlet-loop/SKILL.md +++ b/.claude/skills/gauntlet-loop/SKILL.md @@ -1,62 +1,126 @@ --- name: gauntlet-loop -description: Turns any goal into one short, paste-ready "gauntlet loop" prompt - a prompt that makes an agent set a concrete quality bar, split the work into small judgeable pieces, run a builder and a separate harsh critic on each, compare blind against the bar, and loop until it wins. Works for builds, writing, code, research, or design. Triggers on "/gauntlet-loop", "gauntlet loop", "gauntlet this", "make a gauntlet prompt", "loop until it beats X". +description: Turns a goal for scc's own methodology or harness into one short, paste-ready "gauntlet loop" prompt - a prompt that makes an agent set a control arm, split the work into judgeable pieces, run a builder and a separate harsh critic on each, compare blind on the same ticket, and loop until the change beats its own absence or comes out. Triggers on "/gauntlet-loop", "gauntlet loop", "gauntlet this", "loop until it beats X". --- -# Gauntlet Loop +# Gauntlet Loop — scc edition -The user gives a goal. You give back ONE short prompt they can paste into a fresh agent session. +The user gives a goal about scc: the methodology (the algorithm the agent runs) or the +harness that delivers it (rules, skills, CLI, hooks). You give back ONE short prompt they +can paste into a fresh agent session. -You are not doing the work. You are writing the prompt that makes another agent grind on the work until it beats a real reference. +You are not doing the work. You are writing the prompt that makes another agent grind on +it until the change can be told apart from its own absence. ## Flow -1. **Read the goal.** One line restatement in your head, not on screen. -2. **Set the bar.** If the user supplied a reference, use it. If not, offer **2 or 3 candidate bars**, one line each, and stop. Wait for their pick. Do not write the prompt yet. -3. **Write the prompt.** One block, paste-ready, no preamble, no headings inside it, no narration after it. -4. **Offer to run it.** One flat line under the prompt: "I can run this here." Not a question. +1. **Read the goal.** Name the surface it touches — algorithm, rules, skills, CLI, hooks. +2. **Set the arm.** If the user named a control, use it. If not, offer **2 or 3 candidate + arms**, one line each, with the number each would move, and stop. Wait for their pick. +3. **State the cost** in the same breath: how many agent sessions, roughly what that + spends. +4. **Write the prompt.** One block, paste-ready, no preamble, no headings inside it. +5. **Offer to run it.** One flat line under it: "I can run this here." Not a question. If they say run it, you become the lead agent and follow the prompt you just wrote. -## The bar is the whole trick - -Everything else in a gauntlet loop is scaffolding. The loop only produces quality if the thing it compares against is real. - -A bar has to pass three tests: - -- **Named.** A specific thing, not a category. "Stripe's pricing page" works. "Award-winning SaaS sites" does not. -- **Fetchable.** The critic can actually get it - screenshot the live page, read the published piece, run the binary, open the repo, watch the footage. If the agent cannot obtain it, it will hallucinate the comparison. -- **Comparable.** Both can sit side by side and a judge can pick one. If you cannot imagine the A/B, it is not a bar. - -Bars by goal type: - -| Goal | Bar that works | -|---|---| -| Website, app, UI | The live site of a specific best-in-class product, screenshotted at the same viewport | -| Game, 3D, visual | Real footage or screenshots from a named shipped title | -| Writing | A specific published piece by a named author or publication, same length and format | -| Code, tooling | A named repo's implementation, plus its benchmark or test suite as the measurable half | -| Research, analysis | A named analyst report or a paper's methods section, judged on rigour and coverage | -| Deck, doc, deliverable | A real artifact from a firm known for it, same page count | - -When you propose bars, prefer the hardest one the agent can genuinely reach. A bar that is too easy makes the loop exit on round one. - -If the goal has a measurable half (load time, token cost, benchmark score, word count, pass rate), name it alongside the reference. Taste plus a number beats taste alone. +## The bar here is an arm, not an artifact + +A landing-page gauntlet screenshots Nike. scc has nothing to screenshot. What it has is the +question `design/ponytail-port.md` closed without answering: **nothing measures whether the +rules earn the ~47KB they cost** (measured: `wc -c internal/assets/templates/rules/*.md +internal/assets/templates/entry.md` = 47695, and every unscoped byte of it is in every +request of every session). + +So the comparison is two harness configurations running **the same ticket**, and the critic +is blind to which is which. That is the whole port. A critic reading a rule and saying it +looks good is the vague-bar failure wearing a different hat. + +An arm has to pass three tests: + +- **Isolated.** The control must not secretly be running the thing. ponytail's own benchmark + was wrong for a whole draft because its plugin hook fired on the control arm. Here the leak + is the operator's global config — `~/.claude/rules/`, `~/.claude/settings.json`, a harness + directory already on the machine. Scaffold into a throwaway root and test the isolation + before trusting a number out of it (`design/ponytail-port.md` 4.2). +- **Ticketed, not prompted.** scc's claims are about what an agent leaves behind across a + session — a ticked box, a cited requirement, a clean `scc validate`. One completion cannot + exhibit those. Pin a target repo at a fixed commit and a ticket set. +- **Scored on what scc claims.** `scc validate` findings, the delivery gate's result, whether + boxes were ticked in the file and requirements cited — *plus* diff LOC, tokens, cost and + wall time. LOC alone measures ponytail's thesis, not this one (4.3). + +## The five surfaces + +| Surface | Control arm | The number already instrumented | The cheat to guard | +|---|---|---|---| +| **Algorithm** — routing, the ladder, spec→plan→task, `Ready`/`Next`/`Cycles` | the same ticket with that step of the methodology removed | findings caught, boxes ticked, requirements cited | a step that only ever fires where a validator already reports the failure | +| **Prompts** — `internal/assets/templates/rules/*.md`, `entry.md` | that rule absent from the scaffolded set | 55 lines/rule, 60 lines/entry (`TestRulesStayShortEnoughToBePreloaded`, `TestScaffoldedEntryFileStaysShort`); bytes preloaded | shortening the words while destroying the instruction | +| **Skills** — `assets.Skills()`, the eight `scc-*` | the skill not scaffolded | 340 chars/description, 2400 total (`TestSkillDescriptionsStayShortEnoughToPreload`) | moving cost from the body (paid once) into the description (paid always) | +| **CLI** — `internal/cli`, `map`/`patch`/`notes`/`check` | answering the same question with `Read` on the whole file | tokens per answer; `map tasks --json` is 1558 bytes against a 56KB plan reread | a flag that reports success on an edit it misfiled | +| **Hooks** — git stages, `SessionStart`/`Stop`/`UserPromptSubmit` | the stage registered but inert | silence on a clean branch, turns-to-resolution, loop safety | a Stop stage carrying something the next turn cannot resolve — that shipped once and cost nine turns | + +**Scoping is the other lever, and it is mechanical.** A rule may be scoped with a `paths:` +header only where a validator reports what it prevents (`TestOnlyRulesAValidatorBacksAreScoped`). +If the gauntlet's answer is "scope it", the critic checks that gate before accepting the +saving. + +## Two numbers, always + +Every scc gauntlet moves a **standing cost** and an **effect**, and it is only a win if it +moves the first down without moving the second. + +- Standing cost: bytes, lines, characters — already capped by tests, so a regression fails + `make check` rather than needing a judge. +- Effect: did the thing the surface exists to cause actually happen on the ticket. + +An optimizer told to cut tokens finds the first half in one round and the second never. Say +both in the prompt, or the loop exits on round one having deleted the methodology. + +## Invariants the builder may not trade away + +The critic checks these before it judges anything else. A change that wins by breaking one +has lost: + +- Exit codes are the contract: `0` ok, `1` usage/runtime, `2` findings. `--help` is `0`. +- `go.mod` stays stdlib-only. Six platforms, every dep a supply-chain surface. +- `TestFreshArtifactsPassTheirOwnValidators` — a validator firing on scc's own output is the + worst bug in the product. +- Never author what the user owns; edited files survive an update. +- LF everywhere, deterministic renders, Windows a first-class target. +- No hook stage may block or end a turn; `stop_hook_active` is honoured unconditionally. +- No attribution in a commit message or a PR body. +- `make check` green: `gofmt -l` + `go vet` + `go test -race`. ## Prompt template -Adapt the wording every time. Fill the brackets, keep it short, keep the last line. +Adapt the wording every time. Fill the brackets, keep it short, keep the last lines. ``` -Build [GOAL]. +[CHANGE] in scc, and find out whether it earns its place. + +The control arm is [ARM]. Scaffold both arms into throwaway roots from the same commit, +prove the control is not secretly running the thing — the operator's global config is the +leak — and run both over the same pinned ticket set. -The bar is [BAR]. Get the real thing first and compare against it directly, not against a description of it. +Break this into the smallest pieces that can be changed and judged on their own. For each +piece, fan out a builder and a separate critic with fresh context. The critic runs both +arms, reads the two transcripts and the two diffs blind with the arm labels stripped, says +which session produced better work, and names the single biggest remaining gap. Then it +goes back to the builder. -Break this into the smallest pieces that can be improved and judged on their own. For each piece, fan out a builder and a separate critic with fresh context. The critic inspects the actual output, puts it next to the bar blind with the labels stripped, says which one is better, and names the single biggest remaining gap. Then it goes back to the builder. +Score both halves: [STANDING COST] and whether the ticket ended with [EFFECT]. A change +that cuts the cost and loses the effect has failed. Check the invariants in CLAUDE.md +first — exit codes, stdlib-only, make check green, no validator firing on scc's own output +— a builder that wins by breaking one has lost. -The critic should be a harsh critic. Praise is not useful. If ours does not win, it keeps going. +The critic should be a harsh critic. Praise is not useful. If the arm carrying the change +does not win blind, it keeps going. -/loop on each piece until the critic picks ours blind. Do not stop before that. +/loop on each piece until the critic picks the change blind, or until it is clear the +change cannot be told apart from its absence — in which case say so and take it out. + +Budget: [N] sessions, stop at [CEILING]. Keep a live progress page updating as the work evolves so I can watch it. @@ -65,69 +129,115 @@ Fan out subagents and ultracode. Rules for what you fill in: -- Bake the bar in as a concrete, fetchable thing. URL, product name, repo, title. -- Add a budget or cost ceiling line **only if the user named one**. No default cap. -- Add tool names only if the goal needs them (image or video generation, a browser, a deploy target). -- Everything else stays out. No architecture, no file layout, no decomposition, no round count, no stack choice unless the user demanded it. The agent decides those, and it decides better than a spec written before the work started. +- The arm is a concrete configuration, reproducible from a command. Not "without the rules". +- **Always include the budget line.** This is the one place the generic gauntlet's "no + default cap" does not hold: the measurement half of `design/ponytail-port.md` was skipped + because 48 sessions cost an estimated $14–$120, and that is written into the repo. A + gauntlet that hides its price gets skipped for the same reason. +- "Or take it out" is not optional. The exit that was never reached (`ponytail-port.md` 5.3) + is the one where a rule that cannot be distinguished from its absence comes out. A loop + that can only conclude "keep it" is not a gauntlet. +- Everything else stays out. No file layout, no round count, no implementation. The agent + decides those better than a spec written before the work started. ## Length and voice -Short. Around 120 to 180 words. If the prompt needs a heading to stay readable, it is too long. +Short. 150 to 220 words — longer than a generic gauntlet, because the arm and the two +numbers have to be concrete. If it needs a heading to stay readable, it is too long. -Plain sentences. No bullet lists inside the prompt. It should read like someone telling an agent what perfect looks like and refusing to accept less. - -## Portability - -`/loop` and `ultracode` are Claude Code features. `/loop` reruns the prompt on an interval or lets the model pace itself. `ultracode` opts the turn into multi-agent orchestration. - -For any other agent, swap the last two lines for: "Keep looping until the critic picks ours. Run the builders and critics as parallel subagents." The structure carries over unchanged. +Plain sentences. No bullet lists inside the prompt. ## Two filled examples -**Visual goal.** User: "landing page for my running brand, athletic, green and dark, has to feel alive." +**A rule.** User: "ladder.md is 3KB in every request and I have never seen it change +anything." -Bars offered: A) Nike's current running campaign page B) On Running's homepage C) Gymshark's product landing page. User picks A. +Arms offered: A) the full set minus `ladder.md` B) `ladder.md` scoped behind a `paths:` +header C) `ladder.md` rewritten to 20 lines. User picks A. ``` -Build a landing page for a running brand. Athletic, peak performance, green and dark, energetic, aimed at a young healthy audience. It needs to be interactive and visually unmistakable. +Find out whether rules/ladder.md earns the 3014 bytes it costs in every request of every +session. + +The control arm is the full scaffolded rule set with ladder.md removed. Scaffold both arms +into throwaway roots from the same commit, prove the control is not secretly running it — +the operator's ~/.claude/rules is the leak — and run both over the same pinned ticket set +against a real repo at a fixed commit. + +Break this into the smallest pieces that can be judged on their own, one ticket class per +piece. For each, fan out a builder and a separate critic with fresh context. The critic +reads the two transcripts and the two diffs blind with the arm labels stripped, says which +session reached for the smaller thing that worked, and names the single biggest remaining +gap. -The bar is Nike's current running campaign page. Screenshot it at desktop and mobile and compare against those directly, not against a description of them. +Score both halves: bytes preloaded, and whether the ticket ended with scc validate clean, +boxes ticked and requirements cited. Check the invariants in CLAUDE.md first. -Break this into the smallest pieces that can be improved and judged on their own - hero, motion, type, colour, imagery, interaction, mobile. For each piece, fan out a builder and a separate critic with fresh context. The critic opens the real page in a browser, puts our screenshot next to Nike's blind with the labels stripped, says which is better, and names the single biggest remaining gap. Then it goes back to the builder. +The critic should be a harsh critic. Praise is not useful. -The critic should be a harsh critic. Praise is not useful. If ours does not win, it keeps going. +/loop until the critic picks the arm carrying ladder.md blind, or until it is clear it +cannot be told apart from its absence — in which case say so and take it out. -/loop on each piece until the critic picks ours blind. Do not stop before that. +Budget: 16 sessions, stop at $60. Keep a live progress page updating as the work evolves so I can watch it. Fan out subagents and ultracode. ``` -**Non-visual goal.** User: "a 2000-word explainer on vector databases for non-engineers." +**A CLI surface.** User: "make map --next cheap enough that nobody ever rereads the plan." -Bars offered: A) a specific Stripe engineering blog explainer B) a named Julia Evans post C) the Wikipedia article plus a comprehension test. User picks B. +Arms offered: A) the same questions answered with `Read` on the whole plan B) today's +`map brief` plus `--next` C) a single command returning both. User picks A. ``` -Write a 2000-word explainer on vector databases for readers who are smart but not engineers. +Cut what it costs an agent to work a plan through scc map, and prove the saving against +the thing it replaced. -The bar is Julia Evans' writing on hard technical topics. Pull three of her actual posts and compare against them directly, not against a description of her style. +The control arm is an agent answering the same questions by reading plans/*.md with Read. +Run both arms over the same pinned plan and the same question set — what is next, what is +blocked, what does task 1.7 depend on, what changed — in throwaway roots from the same +commit. -Break this into the smallest pieces that can be judged on their own - the opening, each explanation, the diagrams, the analogies, the ending. For each piece, fan out a writer and a separate critic with fresh context. The critic reads ours and hers blind with the bylines stripped, says which one a non-engineer would understand faster, and names the single biggest remaining gap. Then it goes back to the writer. +Break this into the smallest pieces that can be judged on their own, one subcommand per +piece. For each, fan out a builder and a separate critic with fresh context. The critic +runs both arms, compares the answers blind with the arm labels stripped, says which one a +session could act on without opening the file, and names the single biggest remaining gap. -The critic should be a harsh critic. Praise is not useful. If ours does not win, it keeps going. +Score both halves: tokens per answered question, and whether the answer was right — a +cheaper command that sends the agent back to the file has cost more, not less. Keep the +0/1/2 exit contract and make check green; --json stays compact and the human listing stays +readable. -/loop on each piece until the critic picks ours blind. Do not stop before that. +The critic should be a harsh critic. Praise is not useful. + +/loop on each subcommand until the critic picks ours blind. Do not stop before that. + +Budget: 12 sessions, stop at $40. Keep a live progress page updating as the work evolves so I can watch it. Fan out subagents and ultracode. ``` -## What breaks a gauntlet loop +## What breaks an scc gauntlet loop + +- **A leaking control.** The baseline secretly runs the thing and every arm ties. ponytail + shipped a whole draft this way. Test the isolation before trusting a number out of it. +- **Judging the rule instead of the run.** A critic that reads a rule and says it reads well + has measured prose, not behavior. It must read two transcripts. +- **One number.** Cost alone deletes the methodology; effect alone grows it without limit. +- **The builder judging its own work.** Separate agent, fresh context, arm labels stripped. +- **A soft critic.** Binary job: which session produced better work, A or B. Scores out of 10 + drift upward every round. +- **An exit that can only say "keep it."** If the loop cannot conclude "take it out", it is + an argument with extra steps — which is exactly the standing this port set out to stop + accepting. +- **A named exit after N rounds.** The exit is winning blind, being told apart from absence, + or the budget ceiling. Never a round count. + +## Portability -- **A vague bar.** The critic invents a comparison and approves everything. Most common failure by far. -- **The builder judging its own work.** The critic must be a separate agent with fresh context. It should not know how hard the builder tried. -- **A soft critic.** Say "harsh" in the prompt and give it a binary job: which one is better, A or B. Scores out of 10 drift upward every round. -- **Named exit after N rounds.** The exit is winning the comparison, or the user stopping the run. Never a round count. -- **Over-specifying.** Every extra instruction is one fewer decision the agent makes with its own judgment. Minimal wins. +`/loop` and `ultracode` are Claude Code features. For any other agent, swap the last two +lines for: "Keep looping until the critic picks the arm carrying the change. Run the +builders and critics as parallel subagents." The structure carries over unchanged.