Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,26 @@ These landed after phase 10, and all are documented in `design/orchestration.md`

The profile also carries `PreloadsRules`, because where the rules go is scc's choice but what the harness then does with them is not. Claude Code loads `.claude/rules/*.md` at launch with the same priority as `CLAUDE.md`; Codex and opencode load nothing from `rules/`, which is scc's own directory there. The entry file branches on it: told to "read the rule when the concern is live", an agent that already has all nine in context re-reads them, putting the same ~26KB in twice — and learns that the one document it is meant to trust is wrong about its own environment. Confirm per harness with `/context`.
- **`scc update` (phase 11), as replace-or-keep rather than the planned three-way merge.** It hashes every managed file against this build and against the manifest, prints the plan grouped by outcome, asks, and then replaces what is safe to replace. An edited file is kept and named; `--force` is the separate decision. `internal/merge` is still unbuilt.
- **`scc rtk`, and `scc init --rtk`.** Wires in [RTK](https://github.com/rtk-ai/rtk), the CLI proxy that filters command output: `cargo install` when the binary is missing, plus a splice of RTK's marker-delimited usage block into the entry file. Insert-only — a block already between the markers is left alone whatever version it claims, because RTK owns that text and `rtk init` refreshes it; `--force` is the separate decision. Opt-in in both places, since the block tells the agent to prefix every command with a binary the machine may not have. `--check` reports without writing and exits 2 when the block is missing.
- **`scc launch <harness>`.** Starts the harness this workspace was scaffolded for, from the workspace root, behind [Headroom](https://github.com/headroomlabs-ai/headroom)'s compression proxy (`headroom wrap <slug>`). Headroom is the *default* here, which is the deliberate opposite of how RTK is wired: RTK edits a file the user owns and changes how every later command is typed, while Headroom wraps one process for one session and changes nothing on disk. So it degrades instead of failing — missing binary, declined install, unattended run, or a harness Headroom does not wrap all end in the agent starting bare with a warning saying why. `--no-headroom` forces that path, and a missing binary prompts for `uv tool install` (`--yes`/`--no-install` are the unattended answers). This is the one command that does not obey the 0/1/2 exit-code contract; see the convention below.
- **`scc rtk`, and `scc init --rtk`.** Wires in [RTK](https://github.com/rtk-ai/rtk), the CLI proxy that filters command output: `cargo install` when the binary is missing, plus a splice of RTK's marker-delimited usage block into the entry file. **scc's block wins by default**, replacing whatever sits between the markers; `--keep` is the separate decision. The reason is size, not authorship: `rtk init` (measured on 0.42.4) writes 139 lines / 5140 bytes, scc ships 18 lines / ~900, both stamped `v2`, both saying the same thing — and the entry file is preloaded into every request of the session, so the difference is paid continuously rather than once. Between two blocks of the same version the condensed one is simply better, and leaving the larger one because it got there first is a standing cost. Where that costs something is a block claiming a *newer* version, which is a real downgrade: the run names the version it displaced and points at `--keep`. Opt-in in both places, since the block tells the agent to prefix every command with a binary the machine may not have. `--check` reports without writing and exits 2 when the block is missing.

**scc uses RTK's own markers rather than namespacing its own**, and that is load-bearing: `rtk init` writes `<!-- rtk-instructions v2 -->` … `<!-- /rtk-instructions -->` into the project entry file (verified against rtk 0.42.4), so addressing the block by that pair is what makes `rtk init` and `scc rtk` converge on one copy. A namespaced `scc:rtk-instructions` would make each tool blind to the other's block and leave the file carrying both — which is the bug, not the fix. Headroom is the counter-example: it *does* namespace (`headroom:rtk-instructions`), which is why a workspace wired by both can end up with two blocks. scc detects that one and names it in the report (`rtkFile.Foreign`), with `headroom unwrap <agent>` as the fix — reported, never touched, because that block belongs to Headroom.
- **`scc launch <harness>`.** Starts the harness this workspace was scaffolded for, from the workspace root, behind [Headroom](https://github.com/headroomlabs-ai/headroom)'s compression proxy (`headroom wrap <slug>`), with the workspace's symbol graph brought up to date first. Headroom is the *default* here, which is the deliberate opposite of how RTK is wired: RTK edits a file the user owns and changes how every later command is typed, while Headroom wraps one process for the length of one session. So it degrades instead of failing — missing binary, declined install, unattended run, or a harness Headroom does not wrap all end in the agent starting bare with a warning saying why. `--no-headroom` forces that path, and a missing binary prompts for `uv tool install` (`--yes`/`--no-install` are the unattended answers). This is the one command that does not obey the 0/1/2 exit-code contract; see the convention below.

Two things about `wrap` are load-bearing and were originally documented wrong here:

**It does write to disk.** `headroom wrap` registers MCP servers into the agent's own config (`~/.claude.json` and the Codex/opencode equivalents), and those registrations outlive the session that made them — which is why Headroom ships `unwrap` at all. So `scc launch` defaults to **`--headroom-mcp retrieve`**: Headroom's own retrieve tool stays, because the proxy's compression markers are unactionable without it, and the code-memory server it would otherwise install does not, because code intelligence in an scc workspace is CodeGraph's job. `all` keeps Headroom's defaults; `none` drops the retrieve tool too.

**It also wants the entry file, and scc says no.** `headroom wrap`'s context-tool setup appends RTK guidance to `$PWD/CLAUDE.md` or `$PWD/AGENTS.md` — the same file `scc rtk` splices — behind its own marker pair, `<!-- headroom:rtk-instructions -->`. Neither marker is a substring of the other, so both tools' idempotency checks pass and both append: an entry file carrying the same RTK instructions twice, in every request of the session. So `scc launch` passes `--no-context-tool` by default. Headroom already gates that injection behind `HEADROOM_RTK`, which makes this belt-and-braces — but only until an environment exports that variable for its own reasons, and `wrap claude` resolves `setup_context_tool = (context_tool or _rtk_opt_in()) and not no_rtk`, so the flag wins over both the env var and `--context-tool`. `--headroom-context-tool` hands it back.

**The opt-out flags are discovered, not hardcoded.** `internal/headroom` reads `headroom wrap <agent> --help` and picks the spelling that build advertises. This is not defensiveness for its own sake: Headroom renamed this exact control once already (`--no-serena` → `--code-memory none`, and `--no-tokensave` in between), and the harness profiles disagree today — `wrap opencode` still takes `--no-serena` while `wrap claude` and `wrap codex` take `--code-memory`. A flag name compiled into scc 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 advertising no opt-out is reported, not overridden.

**`--` reaches `wrap`, not only the agent.** `headroom wrap` parses every flag it recognizes out of the tail and forwards only the rest, so a pass-through argument that collides with one of Headroom's — `--verbose`, which both Claude Code and `wrap` define — is silently eaten. `WrapArgs` therefore takes scc's options and the pass-through as separate parameters and puts scc's first, so a colliding argument the user typed lands last and wins. To force something past `wrap` to the agent, use a second terminator: `scc launch claude -- -- -p`.

- **`scc graph`, and the launch-time index.** A wrap over [CodeGraph](https://github.com/colbymchenry/codegraph) — `build | sync | status | query | explore` — plus the same index run automatically by `scc launch`: `codegraph init` when `.codegraph/` is absent, `sync` when it is there. Launch is the one moment where indexing is free, and it degrades exactly the way Headroom does, for the same reason: a graph is an enhancement, so a missing binary or a failed index still starts the agent. `--no-graph` opts out and a plan-only run (`--json`/`--dry-run`) reports without indexing.

What scc adds over typing `codegraph` directly is the two things it already knows: the workspace root, so `scc graph build` from `specs/` indexes the repo rather than a subtree, and whether the binary is there at all. The graph itself is *not* an scc artifact — not in the manifest, never touched by `scc update`, and `.codegraph/` stays CodeGraph's directory on CodeGraph's schedule. Unlike the launch path, a missing binary in `scc graph` is a hard error: the whole command is the binary.

npm is the only installer scc will run. CodeGraph's headline install pipes a remote script into a shell (`curl … | sh`, `irm … | iex`), which is a fine thing for a person to type and not a thing scc executes on their behalf — `InstallHint` names it and leaves the decision where it belongs.
- **The four seeded `docs/` anchors** (`assets.Seeds`). `init` writes `glossary.md`, `stack.md`, `wiki/index.md`, and `wiki/changelog.md` — the knowledge base's only fixed-name documents, each holding the format its validator checks. A seed is written once and tracked nowhere: not in the manifest, not by `scc update`.

`scc` is a redesign of `csdd` (`github.com/protonspy/csdd`), narrowed to spec-driven development and deliberately leaner. When reaching for something from there, port the *decision*, not the file. Already decided against: a TUI, an embedded web dashboard, an MCP server, a devcontainer.
Expand Down Expand Up @@ -68,6 +86,8 @@ cmd/scc/main.go os.Exit(cli.Run(os.Args[1:]))
plain files on disk: <harness>/ · specs/ · plans/ · docs/ · CLAUDE.md|AGENTS.md
```

Three packages sit off to the side of that tree — `rtk`, `headroom`, `codegraph` — reached only from `internal/cli`. They are the third-party integrations, and they are the only code that starts another process.

`internal/cli/cli.go` is the whole dispatcher: `Run(args)` switches on `args[0]` and hands off to `run<Resource>` in a file named for that resource. Each handler owns its own `flag.FlagSet`. Adding a subcommand means adding a case there plus one file — nothing is registered dynamically, so the command set is readable in one place.

| Package | Role |
Expand All @@ -84,10 +104,11 @@ cmd/scc/main.go os.Exit(cli.Run(os.Args[1:]))
| `internal/ears` | EARS requirement parsing, all five patterns plus complex. |
| `internal/validate` | The eight validators, one file each, sharing `mdscan` and `finding`. The exception is `stack_manifests.go`: the seven dependency-file readers age on their own schedule, so they sit beside the rule rather than inside it. |
| `internal/rtk` | RTK's marker pair and the idempotent splice of its block into the entry file, plus finding or `cargo install`ing the binary. |
| `internal/headroom` | Headroom's agent-slug table, the `wrap` argument vector, and finding or installing the binary (uv, then pip — never npm, which ships the SDK and no CLI). The slugs live here rather than on `paths.Harness` because they are Headroom's vocabulary, not scc's layout. |
| `internal/headroom` | Headroom's agent-slug table, the `wrap` argument vector, the MCP opt-out discovered from `wrap <agent> --help`, and finding or installing the binary (uv, then pip — never npm, which ships the SDK and no CLI). The slugs live here rather than on `paths.Harness` because they are Headroom's vocabulary, not scc's layout. |
| `internal/codegraph` | CodeGraph's argument vectors (`init`/`sync`/`index`/`status`/`query`/`explore`), the `.codegraph/` presence test, and finding or `npm install -g`ing the binary. Composes command lines and reads nothing inside the graph — the database is CodeGraph's schema on CodeGraph's schedule. |
| `internal/cli` | The dispatcher and every command handler. |

`internal/rtk` and `internal/headroom` are the only packages that shell out to another program. Keep that boundary there rather than in a command handler: a third party's binary name, install command, and argument vocabulary all age on that third party's schedule, and one package per integration is what keeps a version bump from touching the dispatcher.
`internal/rtk`, `internal/headroom`, and `internal/codegraph` are the only packages that shell out to another program. Keep that boundary there rather than in a command handler: a third party's binary name, install command, and argument vocabulary all age on that third party's schedule, and one package per integration is what keeps a version bump from touching the dispatcher. Headroom's renamed MCP flag is the worked example — the fix stayed inside `internal/headroom`, and nothing else in the tree knows the flag exists.

`go.mod` is stdlib-only. Keep it that way unless a dependency earns its place — the binary is distributed to six platforms and every dep is a supply-chain surface.

Expand Down
3 changes: 2 additions & 1 deletion internal/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import (
// each rule its own trigger line instead of running four of them together in a
// sentence — project.md above all, since a build command nobody read is guessed —
// and it stops telling a harness that preloads the rules to go and read them.
const Version = "9"
const Version = "10"

// The embedded tree. "all:" so nothing is silently dropped for having a name the
// default embed pattern skips.
Expand Down Expand Up @@ -143,6 +143,7 @@ func Workspace(h paths.Harness) []File {
"delivery.md",
"specs.md",
"knowledge-base.md",
"code-search.md",
} {
set = append(set, File{
Name: "rules/" + rule,
Expand Down
20 changes: 13 additions & 7 deletions internal/assets/templates/commands/scc-plan-run.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Run a plan under plans/ to completion group by group — worktree, implement, review, PR, CI, merge, then the next group
argument-hint: [the plan, plus any standing instruction for every group]
description: Run a plan under plans/ to completion — implement group by group, then deliver as one PR per group or one at the end, and settle CI before calling it delivered
argument-hint: [the plan, plus how to run it and any standing instruction]
---

Use the `plan-run` skill.
Expand All @@ -11,11 +11,17 @@ Read the plan and name the groups back, numbered and in order, before writing an
code. The order is the one thing the user can correct cheaply now and expensively
after three merges.

Then ask how this loop should run — automatic or gated, a worktree per group or the
current checkout, and what happens once each PR is open. Three questions, one
exchange, after they can see the groups they are agreeing to. **These are the
developer's calls.** Anything the plan's frontmatter already records is a proposed
answer to confirm, not a decision already made.
Then take every answer the line above already gave and ask only for what is left.
"Implement the whole plan, one PR at the end, delivered when CI is green" has settled
most of it; re-asking what someone just typed is the friction that stops people using
this at all. Restate what you took so a wrong reading is cheap to correct, then put
the remaining questions in one exchange — automatic or gated, one PR at the end or one
per group, a worktree per group or the current checkout, and what happens once a PR is
open. **These are the developer's calls.** Anything the plan's frontmatter already
records is a proposed answer to confirm, not a decision already made.

The plan is delivered when CI is green on its pull request — never on the strength of
a passing local suite.

Anything said above about *how* to implement is a standing instruction: it applies
to every group, and you carry it into each one explicitly rather than trusting it to
Expand Down
3 changes: 3 additions & 0 deletions internal/assets/templates/entry.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Triggered by what you are about to touch:
project's commands exist nowhere else: `scc` ships the file as a stub for the team
to fill in, and runs none of them itself. A command that did not come from there is
a guess, and a guessed test command that exits 0 looks exactly like a passing suite.
- `code-search.md` — **before you go looking for code you have not read yet.** This
workspace keeps a symbol graph, and a structural question answered from it costs one
call instead of a grep and six reads.
- `specs.md` — writing requirements, design, or tasks for a spec
- `tasks.md` — working through a spec's task list
- `knowledge-base.md` — something was learned, or a decision was made
Expand Down
44 changes: 44 additions & 0 deletions internal/assets/templates/rules/code-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Code search — ask the graph before you read the files

This workspace keeps a symbol graph of its own code, in `.codegraph/`, rebuilt
whenever `scc launch` starts an agent. It exists so a structural question costs one
call instead of a grep and six reads.

Reach for it **first**, when the question is about structure:

| The question | Ask |
|---|---|
| Where does this behavior live, and what calls what? | `codegraph_explore`, or `scc graph explore "<question>"` |
| What breaks if I change this symbol? | `scc graph impact <symbol>` |
| Who calls this / what does it call? | `scc graph query <name>`, `callers`, `callees` |

Read files directly when the question is about *this exact text* — a line you are
editing, a diff you are reviewing, a file you have just written. The graph is a map;
it is not the territory, and it does not replace reading the code you are about to
change.

Two ways in, and they answer identically. Use the `codegraph_explore` tool where it
is registered. Use `scc graph explore` in a shell when it is not — from a subagent,
or from a harness with no MCP surface.

## What the graph does not know

**It indexes code, not this repository's knowledge.** `docs/` is Markdown and no part
of it is in the graph: not the glossary, not the wiki, not an ADR, not a `design.md`.

That matters more here than it would elsewhere, because this project deliberately
keeps the *why* out of the code. A question the graph answers well — "where is this
implemented" — is a different question from the one the knowledge base answers —
"why is it like this, and what was ruled out". Asking the graph the second kind gets
you a confident answer about the wrong thing. See [knowledge-base.md](knowledge-base.md)
for where that half lives.

## When it is not there

A missing or stale graph is never a reason to stop. `scc launch` builds it on a best
effort and starts the agent either way, so a session may legitimately have none —
CodeGraph is not installed, the index failed, or someone passed `--no-graph`.

Fall back to ordinary reading and say nothing about it. If a graph query returns
something that contradicts the file in front of you, the file wins and the index is
stale: `scc graph sync`.
Loading
Loading