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
45 changes: 45 additions & 0 deletions internal/assets/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,51 @@ func TestScaffoldedEntryFileStaysShort(t *testing.T) {
// file addresses the set by pattern (`<rules dir>/<name>.md`) instead of listing
// nine links. What it still guarantees is the thing that actually breaks: a rule
// added to the template set without being announced fails here.
// Every rule is preloaded into every request of the session on a harness that reads
// `rules/` — so a line added here is not paid once, it is paid continuously, and the
// whole set has to stay something an agent can hold rather than skim.
//
// 55 lines is the budget for a new rule. Three predate it and are capped where they
// stand instead, because each is roughly half table and fenced example: cutting them
// to 55 would leave under ten lines of prose across six sections, which is enough to
// state a rule and not enough to say why — and a rule whose reason was deleted is one
// an agent quietly deviates from. They may shrink; they may not grow.
func TestRulesStayShortEnoughToBePreloaded(t *testing.T) {
const budget = 55
// Capped at what they measured when the budget landed. Lower a number here when a
// rewrite earns it; never raise one.
grandfathered := map[string]int{
"specs.md": 82,
"knowledge-base.md": 81,
"delivery.md": 62,
}

for _, f := range Workspace(paths.Claude) {
rules := paths.Claude.Dir + "/" + paths.Claude.RulesSeg
if !strings.HasPrefix(f.Rel, rules+"/") {
continue
}
raw, err := Render(paths.Claude, f)
if err != nil {
t.Fatalf("%s: %v", f.Name, err)
}
name := path.Base(f.Rel)
lines := strings.Count(strings.TrimRight(raw, "\n"), "\n") + 1

limit, capped := grandfathered[name]
if !capped {
limit = budget
}
if lines > limit {
t.Errorf("%s is %d lines, over its %d-line limit", name, lines, limit)
}
if capped && lines < limit {
t.Errorf("%s is down to %d lines — lower its cap from %d to lock the gain in",
name, lines, limit)
}
}
}

func TestEntryFileNamesEveryRule(t *testing.T) {
for _, h := range paths.Harnesses() {
raw, err := Render(h, entryFile(t, h))
Expand Down
100 changes: 37 additions & 63 deletions internal/assets/templates/rules/delivery.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,62 @@
# Delivery — branch, worktree, PR

Work does not happen on `main` and does not end with a green test run. It ends with
a pull request.

## One branch per unit of work, in its own worktree

Each unit of work — a spec, or a plan's leaf — gets its own branch, developed in its
own **git worktree**:
Work does not happen on `main` and does not end with a green test run. It ends with a
pull request. Each unit of work gets its own branch in its own worktree:

```
git worktree add ../<repo>-<slug> -b <type>/<slug>
```

The reason is that the user may run several agent sessions at once, one per
feature, and merge them into `main` as they land. A worktree is what makes that
possible: each session gets its own directory, none disturbs the others, and none
touches the checkout the user is sitting in. A shared tree with `git switch` cannot
do this — two sessions would fight over one working directory.
The user may run several sessions at once, one per feature, merging them as they land.
The worktree is what makes that possible: each session gets its own directory and none
touches the checkout the user is in. A shared tree with `git switch` cannot — two
sessions would fight over one working directory.

## Implementation is sequential — you write the code

**There is no implementation subagent and no parallel task dispatch.** This was
designed the other way first and rejected:
**There is no implementation subagent and no parallel task dispatch.** Designed the
other way first, and rejected:

- Delegating implementation puts the cheaper model on the hardest work while the
orchestrator keeps the part that needs the least capability. That is backwards.
- Every fresh agent re-pays for discovery. Within one spec your accumulated context
is the asset: you use the right parser in task 1.2 because you wrote task 1.1.
orchestrator keeps the part needing the least capability. That is backwards.
- Every fresh agent re-pays for discovery. Within one spec your accumulated context is
the asset: you use the right parser in 1.2 because you wrote 1.1.
- **File-disjointness is not independence, and a clean merge hides the difference.**
Two tasks touching no common file both need a `Money` type that does not exist
yet. Each creates its own, with different semantics. The merge is clean and
nothing signals that anything went wrong. Sequential execution cannot produce
this, because the later task sees the earlier task's code.

Feature-level parallelism does not have that problem and is supported: the *human*
picks the split, each session has a full context, and two features a person
deliberately separated are unlikely to collide.

## What running several sessions still costs
Two tasks touching no common file both need a `Money` type that does not exist yet.
Each invents its own, with different semantics, and the merge is clean. Sequential
execution cannot produce this: the later task sees the earlier task's code.

Worktrees isolate files, not the world outside them. Say these out loud rather than
discovering them:

- **Shared external resources.** Two suites running at once fight over a fixed port,
one test database, a shared temp path. Either the suite namespaces them per
worktree, or the runs are serialized.
- **Cross-feature breakage.** Two features green on their own branches can be broken
together. Only CI on `main` after the merge sees that.
Feature-level parallelism has none of that and is supported — a *human* picks the split
and each session has full context. Worktrees isolate files, not the world: suites
fighting over a fixed port or one test database must be namespaced or serialized, and
two features green separately can still break together, which only CI on `main` sees.

## The delivery sequence

Once the last task is done:

1. **Full suite + lint** on the integrated branch. The per-task scoped runs cannot
see breakage between tasks.
2. **`scc validate`** — the artifacts have to be in shape too, and exit `2` is not
done.
3. **`code-review` and `security-review`** subagents on the diff, dispatched
together. Each returns a verdict, a table of what it actually checked, and
findings by severity — you fix from that report, you do not re-review. `blocked`
or any `blocker`/`critical` finding means the PR does not open yet; `major`/`high`
is fixed before merge; `minor`/`low` is your call, and saying "not doing this, and
why" in the PR body is a legitimate answer. A gate reported `not-run` is not a
pass: run it yourself or say in the PR that it was not run.

The PR should arrive already reviewed: a PR is for the human, and spending their
attention on findings a subagent would have caught is pure waste. Fixing then
re-running the two agents is worth one round; a third round means the finding
needs a person, not another review.
1. **Full suite + lint** on the integrated branch. Per-task scoped runs cannot see
breakage between tasks.
2. **`scc validate`** — the artifacts have to be in shape too, and exit `2` is not done.
3. **`code-review` and `security-review`** subagents on the diff, dispatched together.
Each returns a verdict, what it checked, and findings by severity — you fix from
that report, you do not re-review. `blocked` or any `blocker`/`critical` means the PR
does not open yet; `major`/`high` is fixed before merge; `minor`/`low` is your call,
and "not doing this, and why" in the PR body is a legitimate answer. A gate reported
`not-run` is not a pass. The PR should arrive already reviewed — a human's attention
spent on what a subagent would have caught is waste. One round of fix-and-re-run is
worth it; a third means the finding needs a person.
4. **Commit and push.** Conventional Commits, written from the diff and the spec.
5. **Open the PR.** Body: what changed, which spec or plan, how it was verified.

## Then, CI — the answer you already have

Use the `ci:` answer recorded at kickoff ([autonomy.md](autonomy.md)); do not ask now.

- **`wait`** — watch the PR's checks until they settle. Red means fix, push, keep
watching. The work is not finished while CI is failing.
- **`no-wait`** — opening the PR is the finish line.
Then CI, using the `ci:` answer from kickoff ([autonomy.md](autonomy.md)) — do not ask
now. **`wait`** means watch the checks until they settle, fixing and pushing while they
are red: the work is not finished while CI is failing. **`no-wait`** means opening the
PR is the finish line.

## Degrading

- **No remote, or no `gh`** — commit on the branch and stop there, saying so. A
branch the user can push themselves is a real deliverable; silently skipping the
PR is not.
- **Worktrees accumulate.** Remove a worktree once its branch is merged; keep it if
it still holds unmerged work, and say which you did.
**No remote, or no `gh`** — commit on the branch and stop there, saying so. A branch
the user can push themselves is a real deliverable; silently skipping the PR is not.
**Worktrees accumulate** — remove one once its branch is merged, keep it if it holds
unmerged work, and say which you did.
79 changes: 28 additions & 51 deletions internal/assets/templates/rules/knowledge-base.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# The knowledge base — `docs/`

A spec answers *what this feature does now*. `docs/` answers *why* — the durable
reasoning, the decisions, the material read from outside. Neither replaces the other,
and keeping them separate is what lets a spec stay anchored to one feature.
reasoning, the decisions, the material read from outside. Keeping them separate is
what lets a spec stay anchored to one feature.

```
docs/
Expand All @@ -21,84 +21,61 @@ synonyms used where a canonical term belongs, and dependencies missing from

## wiki/

One page per concept, linked with `[[wikilinks]]`. Every page is reachable from
`index.md` — an unreachable page is an orphan, and an orphan is a page nobody will
ever find again. Record what changed in `changelog.md` when you change the wiki.
One page per concept, linked with `[[wikilinks]]`, every page reachable from
`index.md` — an orphan is a page nobody will ever find again. Record what changed in
`changelog.md`.

**`raw/` is a drop box, not storage.** Material collected from outside goes there to
be read, distilled into a wiki page, and then removed. A file still sitting in
`raw/` is a finding: it was collected and never processed.
**`raw/` is a drop box, not storage.** Outside material goes there to be read,
distilled into a wiki page, and removed. A file still sitting there is a finding: it
was collected and never processed.

## adr/

One record per decision that is **hard to reverse**. Numbered contiguously from
`0001`, one file per record, cited from anywhere as
`adr:0007-use-sqlite-for-the-cache`:
One record per decision that is **hard to reverse**, numbered contiguously from
`0001`, cited from anywhere as `adr:0007-use-sqlite-for-the-cache`. Frontmatter
`status:` is `proposed | accepted | rejected | superseded`; the body is `## Context`,
`## Decision`, `## Consequences`.

```markdown
---
status: accepted # proposed | accepted | rejected | superseded
---

# 0007 · Use SQLite for the cache

## Context
## Decision
## Consequences
```

**A superseded record is marked, never edited.** The point of an ADR is that it
records what was believed at the time; rewriting one destroys exactly the thing it
exists to preserve. Add the new record, and in the old one set:
**A superseded record is marked, never edited** — an ADR records what was believed at
the time, and rewriting one destroys the thing it exists to preserve. Add the new
record; in the old one set `status: superseded` and `superseded-by: 0012-…`.

```yaml
status: superseded
superseded-by: 0012-move-the-cache-to-redis
```

Not every design decision is an ADR. If it is cheap to change, `design.md` is where
it belongs.
Not every design decision is an ADR. If it is cheap to change, it belongs in
`design.md`.

## codewiki/

Prose that explains code, one page per area, with every section citing the exact
lines it is about:
Prose explaining code, one page per area, every section citing the exact lines it is
about:

```markdown
## How the dispatcher routes

[internal/cli/cli.go:48-64]()

One switch, no registration...
```

A citation that no longer resolves is a finding, so this is the one part of `docs/`
that goes stale loudly rather than quietly. **Every section cites something** — a
section that cites nothing is prose that has drifted free of the code it describes.

Write it for the parts where reading the code does not tell you why it is like that.
Do not narrate what a reader can see.
that goes stale loudly rather than quietly. **Every section cites something** — one
that cites nothing has drifted free of the code it describes. Write it where reading
the code does not tell you why it is like that; do not narrate what a reader can see.

## glossary.md

One canonical term per concept, and the synonyms to avoid. One entry per line:
One canonical term per concept and the synonyms to avoid, one entry per line:

```markdown
- **order total** — the amount charged, in minor units. Avoid: grand total, sum
- **workspace** — a directory holding {{.Manifest}}. Avoid: project root
```

Domain vocabulary drifts by default — three names for one thing appear within a week
of two people working in parallel. Pick one, list the others after `Avoid:`, and use
the canonical term everywhere: in code, in requirements, in the wiki. An avoided
synonym used as a whole word in `docs/` is a finding.
of two people working in parallel. Use the canonical term in code, in requirements,
in the wiki. An avoided synonym used as a whole word in `docs/` is a finding.

## stack.md

Every adopted technology, with one line on why. **Technology not listed here is an
open decision, never something adopted silently** — and because a project's
dependency file is structured data rather than source, this is checkable: a direct
dependency declared there and absent from `stack.md` is a finding.

So adding a dependency is a two-step act: add it, and say here why it earned its
open decision, never something adopted silently** — and because a dependency file is
structured data, this is checkable: a direct dependency declared there and absent
here is a finding. Adding a dependency is two acts: add it, and say why it earned its
place.
Loading
Loading