feat: CodeGraph layer, Headroom hygiene, and a one-PR plan-run loop - #15
Merged
Merged
Conversation
A third integration package beside rtk and headroom, on the same terms: it composes argument vectors, finds the binary, and installs it, and it reads nothing inside `.codegraph/` — the database is CodeGraph's schema on CodeGraph's schedule. `scc graph build | sync | status | query | explore` wraps the CLI with the two things scc already knows and CodeGraph does not: the workspace root, so `graph build` from `specs/` indexes the repository rather than a subtree, and whether the binary is there at all. `build` initializes once and refreshes after, with `--force` for the full re-index; `status --check` answers "does this checkout have a graph" with the findings code and runs no subprocess, so a CI runner without CodeGraph installed still has a correct answer. npm is the only installer scc will run. CodeGraph's headline install pipes a remote script into a shell, which is a fine thing for a person to type and not a thing scc executes on their behalf — no version pinned and nothing to inspect. InstallHint names it and leaves the decision where it belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b
…dex first
`headroom wrap` writes to disk, which this repo's own documentation denied. It
registers MCP servers into the agent's config, and those registrations outlive
the session that made them — which is why Headroom ships `unwrap` at all. So
`scc launch` now declines two of them by default:
--headroom-mcp retrieve keeps Headroom's own retrieve tool, which its proxy
needs to make compression markers actionable, and
drops the code-memory server; `all` and `none` are
the two ends.
--headroom-context-tool off, because Headroom's context-tool setup appends
RTK guidance to the same entry file `scc rtk`
splices, behind its own marker pair. Neither marker
is a substring of the other, so both tools'
idempotency checks pass and both append.
The opt-out flags are read off `headroom wrap <agent> --help` rather than
compiled in. Headroom has renamed this control once already (`--no-serena` →
`--code-memory none`) and the profiles disagree today: 0.33.0 wants
`--code-memory none` for claude and `--no-serena` for opencode. A flag name
hardcoded here turns that kind of release into a launch that dies on "no such
option", which is strictly worse than one unwanted MCP server. A build that
advertises no opt-out is reported, not overridden.
WrapArgs now takes scc's options and the pass-through separately, and puts
scc's first: `wrap` parses every flag it recognizes out of the tail, so a
colliding pass-through argument — `--verbose`, which both Claude Code and
`wrap` define — has to land last to win.
Launch also brings the symbol graph up to date before starting: `init` when
`.codegraph/` is absent, `sync` when it is there. It degrades the way the
Headroom path does, because a graph is an enhancement — a missing binary, a
declined install, or a failed index all still start the agent. `--no-graph`
opts out and a plan-only run reports without indexing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b
…g one Measured against rtk 0.42.4: `rtk init` writes 139 lines / 5140 bytes into the project entry file, scc ships 18 lines / ~900, both stamped v2, both giving the agent the same instruction. The entry file is preloaded into every request of the session, so that difference is paid continuously rather than once. Leaving the larger block in place because it got there first was not deference, it was a standing cost. So the default inverts: scc's block wins, and `--force` becomes `--keep` for anyone who curated their own. `Present` now means the block is already scc's byte for byte, which keeps re-running free. Where preferring scc's block does cost something is a block claiming a newer version — a real downgrade — so the run names the version it displaced and points at `--keep`, and the report carries `was`, `bytes`, and `was_bytes`. `scc rtk` also reports Headroom's block when it finds one. Headroom namespaces its markers (`headroom:rtk-instructions`), so scc cannot address that block and must not silently ignore it either: a file carrying both tells the agent the same thing twice. Detected, named, `headroom unwrap <agent>` offered as the fix, and left exactly where it is. The markers stay RTK's own rather than becoming `scc:rtk-instructions`, and there is now a test saying why: `rtk init` writes that exact pair, so sharing it is what makes the two tools converge on one copy. Namespacing would make each blind to the other and leave the file carrying both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b
…f delivered From a real run: the plan was implemented group by group, but the review subagents ran once at the end and one PR carried the whole thing. The skill only described the other shape — a PR per group — where the subagents run N times, CI settles N times, and there are N merges. So `pr: per-group | per-plan` becomes an answer the loop takes, validated like the rest. `per-plan` is cheaper and measurably faster; what it costs is granularity, and the skill says so: a large diff to review, a red pipeline that does not say which group broke it, and nothing on `main` if the run stops half way. The suite, the lint, and `scc validate` stay per group even there, because they are cheap and they are what makes a later failure attributable. Two more things that run exposed: The skill now takes whatever the invocation already decided and asks only for the rest. "Implement the whole plan, one PR at the end, delivered when CI passes" answers three of the four questions in one sentence, and re-asking what somebody just typed is the friction that stops people using the skill at all. And it states what delivered means, which it never did in one place: CI green on the pull request, not the agent's own assessment. An assessment is a claim; a green pipeline is a fact about the repository that a person can check without the agent. Resuming had to follow — under `per-plan` nothing reaches `main` until the end, so a session that read its position from `main` would conclude no group was done and be wrong. Template-set version bumped so `scc update` offers this to existing workspaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b
…eadroom Both claims were load-bearing and both were wrong: "Headroom wraps one process for one session and changes nothing on disk" was the stated reason Headroom is the default where RTK is opt-in. It does write to disk — MCP registrations in the agent's own config, which is why `unwrap` exists. The default survives on other grounds; the reasoning did not. "Everything after `--` is passed through untouched" is true of scc and not of the command line it produces: `headroom wrap` takes every flag it recognizes out of the tail before forwarding the rest, so a colliding argument is eaten there. The escape is a second terminator. Also documents `scc graph` and the launch-time index, the discovered-not- hardcoded opt-out flags, and why `internal/rtk` shares RTK's markers rather than namespacing its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b
The graph was being built on every `scc launch` and nothing instructed anyone to query it. A rule that produces an index no reader is pointed at is the whole investment wasted, silently. `code-search.md` routes the structural question — where does this live, what calls what, what breaks if I change it — to `codegraph_explore` or `scc graph explore`, and leaves reading for the question about this exact text. It names both surfaces because they answer identically and a subagent or a harness with no MCP still needs the graph. The load-bearing half is what it says the graph does *not* know: `docs/` is Markdown and no part of it is indexed — not the glossary, not the wiki, not an ADR, not a design.md. That matters more in this project than it would elsewhere, because the methodology deliberately keeps the *why* out of the code. "Where is this implemented" and "why is it like this" are different questions, and a rule that sent both to the graph would route the agent away from the knowledge base that exists to answer the second — which is the opposite of what scc is for. A missing or stale graph is explicitly never a reason to stop, matching how `scc launch` already degrades. TestEntryFileNamesEveryRule caught the orphan on the first run, which is what that test is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five related changes, one per commit. Everything here came out of one question — Headroom was registering MCP servers nobody asked for — and each answer exposed the next.
internal/codegraphandscc graphA third integration package beside
rtkandheadroom, on the same terms: composes argument vectors, finds the binary, installs it, and reads nothing inside.codegraph/.scc graph build | sync | status | query | exploreadds the two things scc knows and CodeGraph does not — the workspace root, sograph buildfromspecs/indexes the repo rather than a subtree, and whether the binary exists.status --checkanswers "does this checkout have a graph" with exit 2 and runs no subprocess, so a CI runner without CodeGraph installed still has a correct answer.npm is the only installer scc will run. CodeGraph's headline install pipes a remote script into a shell — fine for a person to type, not something scc executes on their behalf.
InstallHintnames it.scc launchdeclines whatwrapwould registerheadroom wrapwrites to disk, which this repo's docs denied. It registers MCP servers into the agent's config and they outlive the session — which is why Headroom shipsunwrap. Two defaults change:--headroom-mcp retrieve— keeps Headroom's retrieve tool (the proxy needs it to make compression markers actionable), drops the code-memory server.allandnoneare the ends.--headroom-context-tooloff — Headroom's context-tool setup appends RTK guidance to the same entry filescc rtksplices, behind its own markers. Neither marker is a substring of the other, so both tools' idempotency checks pass and both append.The opt-out flags are discovered from
wrap <agent> --help, not hardcoded. Headroom renamed this control once already, and the profiles disagree today — verified against 0.33.0:A hardcoded table would have got one right and broken the other. A build advertising no opt-out is reported, not overridden.
Launch also indexes before starting —
initwhen.codegraph/is absent,syncwhen present — degrading like the Headroom path, because a graph is an enhancement.--no-graphopts out; plan-only runs report without indexing.scc rtkprefers its own blockMeasured against rtk 0.42.4:
rtk initwrites 139 lines / 5140 bytes, scc ships 18 / ~900. Bothv2, both saying the same thing, and the entry file is preloaded into every request. End to end:--forcebecomes--keep. Where this costs something is a block claiming a newer version — a real downgrade — so the run names what it displaced and points at--keep.The markers stay RTK's own rather than
scc:rtk-instructions, with a test saying why:rtk initwrites that exact pair, so sharing it is what makes the two tools converge on one copy.plan-runlearns a second loop shapeFrom a real run: implemented group by group, but the review subagents ran once at the end and one PR carried the whole plan — about an hour end to end. The skill only described the other shape, where subagents run N times and CI settles N times.
pr: per-group | per-plan, validated like the rest of the loop answers. The trade-off is stated:per-planis cheaper and faster, and gives up granularity. Suite, lint, andscc validatestay per group even there — cheap, and what makes a later failure attributable.per-plannothing reachesmainuntil the end, so a session reading its position frommainwould conclude no group was done and be wrong.Template-set version bumped to 10 so
scc updateoffers this to existing workspaces.Verification
gofmt -l,go vet ./...,go test ./...green.-raceandgolangci-lintwere not run locally (no gcc, no linter on this machine) — left to CI, which is the gate this PR is asking to pass.A scaffolded workspace validates clean, including the edited skill against its own validator:
🤖 Generated with Claude Code
https://claude.ai/code/session_01J54qbk8RpC2tZH8LBz6T2b