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
27 changes: 24 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,25 @@ These landed after phase 10, and all are documented in `design/orchestration.md`

**`--` 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.
- **`scc graph`, the launch-time index, and its usage block.** 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. **`scc launch` also splices a CodeGraph usage block into the entry file**, the way it does RTK's, because an index nobody knows how to query is an index nobody queries.

Three things about that block, and each is the reason it is not written at `init` time instead. It goes in **only once the binary is present** — guidance naming a command the machine cannot run is worse than none, since an agent that tries `scc graph explore` and watches it fail discounts the whole file — and CodeGraph is usually not installed when `init` runs. It is **written before the index**, so a failed index still leaves the agent knowing the command that rebuilds one. And a **plan-only run writes nothing**: `--dry-run` that edited a file the user owns would be the one flag nobody expects to change anything doing exactly that.

**Its markers are scc's own** — `<!-- scc:codegraph-instructions v1 -->` — which is deliberately the opposite of the RTK decision above and for the opposite reason. Sharing RTK's markers is what makes `rtk init` and `scc rtk` converge on one copy of a block they *both* write. CodeGraph writes nothing into the entry file, and this block is scc's account of `scc graph` rather than CodeGraph's account of itself, so namespacing it leaves a future CodeGraph release free to add its own without either tool clobbering the other. The splice itself is `internal/mdblock`, shared by both: choosing the markers is the integration's decision, and everything after that choice — find, compare, replace, append, preserve the file's line endings — is one implementation rather than two sets of edge cases. 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.

**How to actually use it.** The graph answers relationship questions — who calls this, what breaks if I change it, where does this concept live — in one command where reading files costs a dozen:

```bash
scc graph explore "how does a plan get validated" # start here: relevant symbols' source plus the call paths between them
scc graph query renderTask --kind function # find one symbol by name (--limit N)
scc graph status # what the graph holds (--check exits 2 when there is none)
scc graph sync # re-index after writing code you then need to search
scc graph build --force # full rebuild, for a graph that has gone wrong rather than stale
```

Reach for `explore` before `Read` when the question is about relationships, and for `Read` when you already know the file. `explore` takes a sentence and needs no quoting discipline — the positionals are joined, so an unquoted question still arrives as the sentence it was typed as. It emits no `--json` and that is CodeGraph's design, not an omission: it is the CLI face of the `codegraph_explore` MCP tool and returns the same agent-shaped text.

The index goes stale as you edit. `scc launch` syncs before the session starts, so it is current at turn one; after that, `sync` before searching for something you just wrote.

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.

Expand All @@ -49,6 +67,8 @@ These landed after phase 10, and all are documented in `design/orchestration.md`
Three consequences had to land together or the result is worse than before: `Task.End` covers the flags (so `map show` returns them and `patch rm` removes them), `Detail` excludes them (so the searcher does not index `_Priority 2_` as prose), and **`renderTask` re-emits them plus the continuation** — without that, `patch task --method TDD` was a data-loss command that deleted a sixty-line description and every dependency the task declared.

**The reading surface is what gives "never read the plan" its authority.** `map brief` is the header, `map tasks` is the checklist, and no command returns both — so a session pays `brief` once and `--next` per task instead of ~14k tokens per reread. Forbidding the read without offering the equivalent query produces an agent that disobeys the rule, correctly — so the surface shipped in the phase before the rule did. `--next` is now determined (eligible → priority ascending, absent last → number compared *numerically*, which is also the fix for `1.10` sorting before `1.9`), and `--ready`/`--blocked`/`--deps` share that one implementation, because two notions of eligibility would be two answers to "what do I work on".

**`--next` prints the task whole; the listings clip.** Every task in a real plan runs past one line and the line below the checkbox is usually where the decision sits, so a `--next` that stopped at the line break sent the reader to the file — the exact cost this surface exists to remove. It therefore ignores `--width` and prints the continuation under the checkbox, as it sits in the file. The listings still clip to one line, because a list of sixty-one-line tasks is not a list. `Task.Continuation()` is the raw lines with the flags removed; `Task.Detail` is the same text collapsed to one line, which is right for a row in a table and wrong for anything meant to be read.
- **`scc plan approve|reseal|migrate`, and the seal.** `approve` validates, then writes `status: approved` and a `checksum:` over the file minus its own checksum line, LF-normalized. It is **tamper-evidence, not prevention** — `reseal --force` is one command away and sha256 is public — and it is recorded that way here so nobody builds a guarantee on it later. The check runs before an edit is applied, which is the whole value: a harness that edited by hand and then ran `patch check` would otherwise have its edit resealed by the command that should have reported it. A plan with no `status:` is never checked, which is what makes every pre-existing plan keep working.

After approval the work is fixed and only discovery moves: `add` allocates the number (high-water mark including removed tasks, so nothing is stored anywhere) and demands `--reason`; `rm` strikes the task out where it stands rather than deleting it; rewriting a task or the prose is refused. What discovery can never touch is guaranteed structurally rather than by instruction — `Why`, `Out of scope`, `Done when` and the title are reachable only through `append`/`prepend`/`replace`, and those are exactly the three refused.
Expand Down Expand Up @@ -126,6 +146,7 @@ Three packages sit off to the side of that tree — `rtk`, `headroom`, `codegrap
| `internal/workspace` | Resolves the root by walking up for *any* harness's `scc-manifest.json` marker; `Harnesses(root)` says which trees exist. Owns `KebabCheck`, `SafeName`, `AtomicWrite`. Knows nothing about specs or wikis. |
| `internal/render` | CLI terminal output (`✓ ✗ ! •`, `NO_COLOR`/TTY aware), split across stdout/stderr. |
| `internal/textutil` | Line-ending and BOM normalization, in exactly one place. |
| `internal/mdblock` | The marker-delimited splice: keeping one generated block current inside a Markdown file somebody else owns. `Markers.Splice/Block/Version`, idempotent, CRLF-preserving, and everything outside the markers untouched. Which markers is the integration's decision — `rtk.Markers` are RTK's own, `codegraph.Markers` are namespaced as scc's; what happens after that choice lives here once. |
| `internal/finding` | One finding type and one frozen JSON shape (`{findings, count}`) for every validator, plus the grouped human report. |
| `internal/manifest` | `<harness>/scc-manifest.json`: `{path, hash, version}` per managed file plus the harness, deterministic serialization, `Status → pristine\|edited\|missing`. Unknown fields are preserved. Every call takes the `paths.Harness` whose manifest it means. |
| `internal/assets` | The embedded template set — rules, review agents, skills, slash commands, artifact templates. **Workspace templates are data-free except for the harness profile** (a `(version, harness)` pair still renders byte-identically everywhere, and the manifest records both, so the future three-way merge can still reconstruct the old side); **artifact templates take data** (`spec new` renders them and the user owns the result); **seeds are the `docs/` anchors** — data-free like a workspace file, untracked like an artifact. `Render(h, file)` is the only way to get a workspace file's bytes: it expands paths and synthesizes the per-harness header for agents and commands. `Version` is the template-set version and must be bumped whenever a workspace template changes. |
Expand All @@ -134,9 +155,9 @@ Three packages sit off to the side of that tree — `rtk`, `headroom`, `codegrap
| `internal/artifact` | The navigable model of one artifact, layered on `mdscan`: sections (two ends — the subtree, and the body before the first child), tasks with their continuation *and their flags*, requirements, spec-reference leaves, paragraph blocks. Owns **every grammar** (task, requirement, spec reference, flag), `Find` for address resolution, `Editor` for line splices resolved against the original and applied bottom-up, `Search`, the schedule (`Ready`/`BlockedTasks`/`Next`/`Cycles`, one implementation shared by `--next`, `--ready` and `--blocked`), and the seal. Knows nothing about findings or exit codes. |
| `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/rtk` | RTK's marker pair (`rtk.Markers`, spliced by `internal/mdblock`), the foreign-block detection that names Headroom's copy, and finding or `cargo install`ing the binary. |
| `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/codegraph` | CodeGraph's argument vectors (`init`/`sync`/`index`/`status`/`query`/`explore`), the `.codegraph/` presence test, `codegraph.Markers` for the usage block `scc launch` splices, 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`, `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.
Expand Down
19 changes: 18 additions & 1 deletion internal/artifact/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,24 @@ func (t Task) Group() string {
// Summary is the task's description clipped to n runes, for a listing where one
// task is one line. It is the first line only: a 61-line task exists, and printing
// it in a list would defeat the point of the list.
func (t Task) Summary(n int) string { return clip(t.Text, n) }
//
// A non-positive n is the whole line, for the caller that is printing one task
// rather than a list of them.
func (t Task) Summary(n int) string {
if n <= 0 {
return strings.TrimSpace(t.Text)
}
return clip(t.Text, n)
}

// Continuation is the rest of the description as it sits in the file, flag lines
// excluded — the lines a listing drops and a single-task answer must not.
//
// It is the raw lines rather than Detail because a task's continuation carries the
// decision, and often carries it as an indented list or a fenced example; Detail is
// the same text collapsed to one line, which is right for a row in a table and wrong
// for anything meant to be read.
func (t Task) Continuation() []string { return t.detail }

// Requirement is one numbered EARS requirement.
type Requirement struct {
Expand Down
15 changes: 15 additions & 0 deletions internal/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,21 @@ const RTKTemplate = "rtk.md"
// the same block has to be recognizable to a tool that is not scc.
func RTKBlock() (string, error) { return Content(RTKTemplate) }

// CodeGraphTemplate is the embedded name of the CodeGraph usage block.
const CodeGraphTemplate = "codegraph.md"

// CodeGraphBlock returns the marker-delimited CodeGraph instructions `scc launch`
// splices into the entry file.
//
// A fragment like the RTK one, and delimited by markers of scc's own — which is the
// opposite of that decision, for the opposite reason. `rtk init` writes an RTK block
// into this same file, so sharing its markers is what makes the two tools converge
// on one copy. CodeGraph writes nothing into the entry file at all: this block is
// scc's account of `scc graph`, not CodeGraph's account of itself, so namespacing it
// leaves a future CodeGraph release free to add its own without either clobbering
// the other.
func CodeGraphBlock() (string, error) { return Content(CodeGraphTemplate) }

// ReviewAgents names the two subagents scc ships. Both read and neither writes:
// review is where a cold context is worth paying for, and authorship is not.
var ReviewAgents = []string{"code-review", "security-review"}
Expand Down
4 changes: 4 additions & 0 deletions internal/assets/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ func TestWorkspaceSetAndTreeAgree(t *testing.T) {
t.Errorf("RTKBlock(): %v", err)
}
referenced[RTKTemplate] = true
if _, err := CodeGraphBlock(); err != nil {
t.Errorf("CodeGraphBlock(): %v", err)
}
referenced[CodeGraphTemplate] = true
for name := range inTree {
if !referenced[name] {
t.Errorf("embedded template %q is in no harness's Workspace(), Seeds(), and is neither an artifact template nor a fragment", name)
Expand Down
15 changes: 15 additions & 0 deletions internal/assets/templates/codegraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- scc:codegraph-instructions v1 -->
## 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 "<question>"` — the relevant symbols' source plus the call paths between them. Start here.
- `scc graph query <name> [--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.
<!-- /scc:codegraph-instructions -->
Loading
Loading