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
15 changes: 13 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ These landed after phase 10, and all are documented in `design/orchestration.md`

**`@path` is the stale check** — the notes half of the codewiki citation rule, since a note about code that no longer exists is read as current, which is worse than the comment it replaced: that one at least died with the file. At *write* time it is a warning and never a block, because a note about a file this branch has not created yet is the note most worth having; `notes add` otherwise writes under the same verify-and-roll-back contract as `scc patch`. The ninth validator also reports the failure this file cannot tolerate quietly: a hand-written line that missed the grammar, which no query will ever return.

- **The spec records where it is being built: `scc spec track` and `scc spec sync`.** A branch was the one part of this methodology that left no trace in the artifacts — the spec said which boxes were ticked, git said a branch had been unmerged for three weeks, and nothing joined the two, so *which of these actually shipped* was answerable only by somebody holding both halves. Under `autonomy: auto` that is nobody. Three keys on `requirements.md` beside the kickoff answers — `branch:`, `pr:`, `delivery: in-progress|in-review|merged|abandoned` — with the vocabulary closed for the reason a task's flags are, and graded by the validator only when present, so every spec written before this keeps passing.

**`track` records what the caller knows; `sync` derives what git knows.** `--here` takes the branch from the checkout, `--pr <n>` the pull request, and `scc spec sync` walks every spec, asks git and (when installed) `gh`, and writes the answer back under the same verify-and-roll-back contract as `scc patch`. `scc spec list` shows the record beside the phases. **Neither guesses**: a deleted branch with no PR to ask about is reported undetermined and left alone, because merged and abandoned are indistinguishable once the ref is gone.

Two things in `sync` were wrong in the first cut and are worth keeping wrong-proof. **Merged is not "is an ancestor of the base"** — a branch created ten seconds ago satisfies that trivially, and the first run declared a spec delivered before a line of it existed; it is *ahead == 0 and behind > 0*, and the fast-forward case that no ref can resolve is called **not** merged, because this record exists to surface unfinished work. And **a settled record is not re-litigated**: a deleted branch on a spec already `merged` is what a merged branch looks like, and warning about it would put a line on every finished spec forever.

**Plans are deliberately out for now**, for a naming reason rather than a principle: `pr:` on a plan already means the delivery *shape* plan-run asks for at kickoff (`per-plan`/`per-group`), so one key would carry two meanings on one file. Plan tracking starts by renaming that answer.

- **The five seeded `docs/` anchors** (`assets.Seeds`). `init` writes `glossary.md`, `stack.md`, `notes.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 @@ -156,7 +164,7 @@ 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.
Four packages sit off to the side of that tree — `rtk`, `headroom`, `codegraph`, `git` — reached only from `internal/cli`. They are the third-party integrations, and they are the only code that starts another process. `git` is the one scc never installs and never writes with: every call in it is a query.

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

Expand All @@ -178,10 +186,11 @@ Three packages sit off to the side of that tree — `rtk`, `headroom`, `codegrap
| `internal/validate` | The nine 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 (`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/git` | `git` and `gh`, read-only: does this branch still exist, has it landed, and what did the forge do with the pull request. Two binaries in one package because the question has two halves and no caller should have to route between them — and nothing here installs or writes, so running it over every spec in a workspace is safe by construction. |
| `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.
`internal/rtk`, `internal/headroom`, `internal/codegraph`, and `internal/git` 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 Expand Up @@ -239,3 +248,5 @@ Tests live beside the code and lean on a few package-local helpers rather than a
## Commits

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.
82 changes: 80 additions & 2 deletions design/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -881,8 +881,86 @@ Once the last task is done:
spending their attention on findings a subagent would have caught is the waste
this ordering avoids. One fix-and-re-review round; a second means the finding
wants a person.
3. **Commit and push** — Conventional Commits, generated from the diff and the spec.
4. **Open the PR.**
3. **Commit and push** — Conventional Commits, generated from the diff and the spec,
and carrying no attribution (below).
4. **Open the PR**, and record it on the spec: `scc spec track <feature> --pr <n>`.

### The record carries no attribution

A commit message and a PR body say what changed and why. They do not say what typed
it: no `Co-Authored-By` for an assistant, no session link, no generated-with footer,
no naming of a model, a vendor, or a harness — not in the message, not in the PR title
or body, not in a branch name.

This is not modesty and it is not about hiding the tool. **The work is the user's.** A
trailer is a claim on authorship, and a tool that signs what it did for somebody is
taking a share of something it was hired to produce; the honest signature is the diff,
which is already in the record and already attributable by whoever cares to look. The
practical half matters too: these lines outlive the session that wrote them, they end
up in release notes, `git shortlog`, and every downstream fork, and a repository whose
history names a product from 2026 has dated itself for nothing.

Harness defaults push the other way — several of them append a footer unless told not
to. So this is stated in the rule that owns delivery rather than left to the tool's
own configuration, and it is why `TestNoTemplateShipsAnAttributionFooter` exists: a
template that demonstrated a footer would teach the example, and the example is what
gets copied.

### The spec records where it is being built

A branch is the one part of this methodology that left no trace in the artifacts. The
spec said what the feature does and which boxes were ticked; git said a branch called
`feat/user-auth` had been sitting unmerged for three weeks. Nothing joined the two, so
*which of these actually shipped* was answerable only by somebody holding both halves —
and under `autonomy: auto` there is nobody holding either.

Three keys on `requirements.md`, beside the kickoff answers, because that file is
already the spec's header:

```yaml
branch: feat/user-auth
pr: 28
delivery: in-progress | in-review | merged | abandoned
```

`scc spec track <feature> --here` writes the branch you are on, `--pr <n>` the pull
request; `scc spec sync` reads git and the forge back into every spec, and `scc spec
list` shows the result beside the phases. The vocabulary is closed for the same reason
a task's flags are: an open one produces three spellings of *done* inside a month, and
"what is still unfinished" is exactly the question a synonym destroys. Absent is the
fifth state and needs no name — it is what every spec written before this looks like,
and it is why the validator grades these only when they are present.

**The split between the two commands is the one scc makes everywhere.** `track` records
what the caller knows and cannot get wrong. `sync` derives what git knows and can. And
neither one guesses: a branch that has been deleted, with no pull request to ask about,
is reported undetermined and left exactly as it was, because a merged branch and an
abandoned one are indistinguishable once the ref is gone.

Two decisions inside `sync` are worth naming, because both were wrong in the first
version and the failures were instructive:

- **Merged is not "is an ancestor of the base".** A branch created ten seconds ago and
never committed to satisfies that trivially, so the first run declared a spec
delivered before a line of it existed. Merged is *ahead == 0 and behind > 0*:
everything the branch had is on the base, and the base has moved on since. The one
case left ambiguous is a fast-forward merge nothing has advanced past, where no ref
can tell "just branched" from "just landed" — resolved as **not** merged, because
this record exists to surface unfinished work and the error that leaves a loose end
visible is the one to make.
- **A settled record stops being re-litigated.** A deleted branch on a spec already
marked `merged` is not an ambiguity worth reporting: it is what a merged branch is
supposed to look like, and warning about it would put a line on every finished spec
forever, which is how a report stops being read.

`gh` is the only thing that can tell merged from abandoned once the branch is gone, and
it is optional like every other binary scc talks to: without it those rows say so, once
per run rather than once per spec.

**Plans are deliberately not tracked this way yet**, and the reason is a name rather
than a principle: `pr:` on a plan already means the delivery *shape* (`per-plan` /
`per-group`) that plan-run asks for at kickoff, so one key would carry two meanings on
one file. The day plan tracking is wanted, that answer gets renamed first.

### Waiting for CI is a question, not a policy

Expand Down
107 changes: 107 additions & 0 deletions internal/artifact/delivery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package artifact

import "strconv"

// The delivery record: three frontmatter keys that say where a spec is being built
// and how far that has got.
//
// It exists because a branch is the one part of this methodology that leaves no trace
// in the artifacts. A spec says what the feature does and which tasks are ticked; git
// says a branch called `feat/user-auth` has been sitting unmerged for three weeks.
// Nothing joined the two, so "which specs are actually finished" was a question only a
// person holding both halves in their head could answer — and under `autonomy: auto`
// there is no such person. These keys are the join, and `scc spec sync` is what keeps
// them true without anybody remembering to.
//
// They live on `requirements.md`, with the kickoff answers, because that is already
// the spec's header: one file to read for everything about the spec that is not a
// requirement, a design, or a task.
const (
// KeyBranch is the branch the work is on, as git spells it.
KeyBranch = "branch"
// KeyPR is the pull request's number, digits only. A number rather than a URL
// because it is what `gh pr view` takes and what a person says out loud; the URL
// is reconstructible and the number is not.
KeyPR = "pr"
// KeyDelivery is how far the work has got, from DeliveryStates.
//
// Deliberately not `status:`, which already means the approval seal on a plan
// (see KeyStatus). One key with two meanings across two artifacts is the defect
// this vocabulary exists to avoid, and `delivery:` names its own concern —
// the rule that owns it is delivery.md.
KeyDelivery = "delivery"
)

// The delivery states, and the whole set of them.
//
// Four, and the set is closed for the same reason a task's flags are: an open
// vocabulary here would produce three spellings of "done" inside a month, and the
// question this record answers — what is still unfinished — is exactly the one a
// synonym destroys.
//
// Absent is the fifth state and needs no name: a spec nobody has started carries no
// branch, no PR, and no delivery line, which is also what every spec written before
// this shipped looks like.
const (
// DeliveryInProgress — a branch exists and has not landed.
DeliveryInProgress = "in-progress"
// DeliveryInReview — a pull request is open.
DeliveryInReview = "in-review"
// DeliveryMerged — the work is on the base branch. Terminal, and the reason the
// branch and PR are kept rather than cleared: the spec then permanently records
// what delivered it.
DeliveryMerged = "merged"
// DeliveryAbandoned — the pull request was closed unmerged, or the branch was
// dropped. Terminal, and it has to be sayable: a spec that was tried and dropped
// is not the same as one nobody started, and only one of the two is a loose end.
DeliveryAbandoned = "abandoned"
)

// DeliveryStates returns the vocabulary, in the order work moves through it.
func DeliveryStates() []string {
return []string{DeliveryInProgress, DeliveryInReview, DeliveryMerged, DeliveryAbandoned}
}

// ValidDelivery reports whether s is one of them.
func ValidDelivery(s string) bool {
for _, v := range DeliveryStates() {
if s == v {
return true
}
}
return false
}

// Settled reports whether a state is terminal — the work is not coming back. It is
// what separates "still open" from "done with", which is the only distinction a
// reader scanning for loose ends actually makes.
func Settled(state string) bool {
return state == DeliveryMerged || state == DeliveryAbandoned
}

// Delivery is the record as one value.
type Delivery struct {
Branch string `json:"branch,omitempty"`
// PR is 0 when there is none. A pointer would distinguish "absent" from "zero",
// and there is no pull request number zero, so it would only buy a nil check at
// every use.
PR int `json:"pr,omitempty"`
State string `json:"delivery,omitempty"`
}

// Tracked reports whether anything has been recorded at all. An untracked spec is not
// a defect — it is a spec nobody has started, or one that predates the record.
func (d Delivery) Tracked() bool { return d.Branch != "" || d.PR != 0 || d.State != "" }

// ReadDelivery pulls the record out of a parsed frontmatter map.
//
// A `pr:` that is not a number comes back as 0 rather than as an error: reading is
// not where a malformed value gets reported, the validator is, and a reader that
// failed here would take out `scc spec list` for the whole workspace over one typo.
func ReadDelivery(fm map[string]string) Delivery {
d := Delivery{Branch: fm[KeyBranch], State: fm[KeyDelivery]}
if n, err := strconv.Atoi(fm[KeyPR]); err == nil && n > 0 {
d.PR = n
}
return d
}
17 changes: 16 additions & 1 deletion internal/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,22 @@ import (
// rule of its own because its trigger is a keystroke — you are about to type a
// comment that is not a docstring — and prior-art.md gains the read side, since this
// is now the one corpus under docs/ that does have an index.
const Version = "19"
// 20: delivery.md — the commit and the PR carry no attribution. No `Co-Authored-By`
// for an assistant, no session link, no generated-with footer, no naming of a model,
// vendor or harness, in a message or a PR body or a branch name. The work is the
// user's: a tool that signs what it did for somebody is claiming a share of it, and
// the honest signature is the diff. It lands in delivery.md at no cost to the budget —
// the opening paragraph's fenced one-liner goes inline, which pays for the paragraph
// exactly.
// 21: delivery.md — the spec records where it is being built. A branch was the one
// part of this methodology that left no trace in the artifacts: the spec said which
// boxes were ticked and git said a branch had been unmerged for three weeks, and
// nothing joined the two, so "which of these actually shipped" was answerable only by
// somebody holding both halves — which under `autonomy: auto` is nobody. `scc spec
// track --here` records the branch and `--pr` the pull request; `scc spec sync` reads
// git and the forge back into every spec. The rule pays for the paragraph by
// tightening §8's argument, which the design doc holds in full anyway.
const Version = "21"

// The embedded tree. "all:" so nothing is silently dropped for having a name the
// default embed pattern skips.
Expand Down
Loading
Loading