@warren-dean/code-kit 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "name": "code-kit",
10
10
  "source": "./",
11
11
  "description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
12
- "version": "0.18.0",
12
+ "version": "0.19.0",
13
13
  "category": "development",
14
14
  "tags": ["guardrails", "architecture", "lanes", "layers", "spec-driven", "ci", "multi-agent"],
15
15
  "license": "MIT"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-kit",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
5
5
  "author": {
6
6
  "name": "Warren Langeveldt"
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  What changed in each release of code-kit, newest first. Versions follow the plugin manifest and the npm package `@warren-dean/code-kit`. Each release is also on [GitHub Releases](https://github.com/warrendeanlangeveldt/code-kit/releases).
4
4
 
5
+ ## 0.19.0 (2026-10-19)
6
+
7
+ A build uses far fewer tokens. In two days of two builds, output was under 1% of the tokens: nearly all of it was agents rereading large contexts on every request.
8
+
9
+ - Each lane agent builds one story. A send-back, or a story carried on, goes to a fresh agent, never to the one that built it. Lane agents had reached 900k to 970k tokens of context over hundreds of requests.
10
+ - A lane agent past `agents.lanes.contextLimit` (250,000 tokens by default) is told to finish its step, commit, write its report and hand back. The loop then has the lead carry the story on with a fresh agent that reads that report first.
11
+ - `code-kit worktrees` lists the project's worktrees. `worktrees release <branch>` frees one for a fresh agent, keeping the branch's commits and moving its report to the main checkout, and `worktrees prune` removes every one that's merged and clean. One project had 11 such leftovers, about 4.5 GB; `code-kit next` now points them out.
12
+ - The lead's context is compacted between turns once it's past `compact.at` (40% of its window by default), without asking. Waiting for a milestone wasn't enough: one lead reached 968k tokens first.
13
+ - The loop reads the lead's replies with Sonnet, from the reply itself. Before, it forked the lead's whole conversation on the lead's own model at the end of every turn.
14
+ - Lane agents read every file they change in full, but of its imports and callers only what the change needs; a fresh Context Graph card counts as reading a file. They run the narrowest tests with failures only in the output, and the full suite once before committing. The lead's review starts from the lane's report, the reviewer's findings and the diff's summary.
15
+ - `agents.reviewer.model` now defaults to Sonnet and covers the security reviewer too, unless its agent file names a model.
16
+ - The lane rules changed, so `code-kit next` asks the lead to bring code-kit's instructions up to date.
17
+
5
18
  ## 0.18.0 (2026-10-17)
6
19
 
7
20
  code-kit's instructions to your agents now keep up with code-kit, it tells you when something needs you, and the fixes found by checking two live builds.
package/README.md CHANGED
@@ -337,6 +337,7 @@ The loop keeps the build moving between your prompts, under `harness.autonomy`.
337
337
  - **Stories waiting on other work.** When the lead says a story isn't due yet because it waits on work still going (a paired story a lane is building, a rebuild, a check running), the loop parks that story and leaves it out of every step, so a step naming it with others prompts only the others. It wakes when its own branch or state moves, or when a story it waits on (one the lead named, or an unfinished one building on it) changes state; that story's commits along the way don't wake it, nor do other lanes'. A story the lead's turn left exactly where it stood is parked too, whatever the reply said. After 30 minutes with nothing moving, the lead is asked once more.
338
338
  - **A step that doesn't move.** One the lead was prompted with twice in a row that is still next goes to you instead: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Ask again**.
339
339
  - **A lean lead.** A lane writes its full report, with its commands and their output, to `.claude/state/reports/<branch>.md`, and hands the lead a few lines naming it; the stop hook won't let a lane on a story branch finish without it, or with a hand-back of more than ten lines, and the lead reads the file when it reviews. Lanes don't message the lead while they work. When a milestone finishes, the lead's context is compacted before the next one (`harness.compact.milestone`: you're asked by default, or it's done at once): the summary keeps what's in flight and what waits on you, and drops the finished stories' detail.
340
+ - **Token use.** A long-lived agent rereads its whole context on every request, which is most of what a build costs. So each lane agent builds one story; one past `agents.lanes.contextLimit` is told to wrap up, and the lead carries its story on with a fresh agent that reads its report (`code-kit worktrees release` frees the branch). The lead's context is compacted between turns once it's past `compact.at`. The loop reads the lead's replies with Sonnet, and the reviewers run on Sonnet by default.
340
341
  - **Designs.** A design story is handed to the lead once a session; while you and the lead design it, the loop leaves it to you.
341
342
  - **One session runs it.** The first session whose loop acts runs it for the project. Another session you open there, for any reason, never prompts its lead: its band reads "the loop runs in another session", with **Lead from here** to move the loop to it.
342
343
  - **Modes.** **Pause** and **Resume** stop and start it. Under `propose`, each step waits for **Go**; under `off`, the loop does nothing. When every story is done, the band says so and the loop stops.
@@ -436,6 +437,7 @@ code-kit verify [--base ref] [--branch name] [--no-checks] [--json] # a branch'
436
437
  code-kit security [--base ref] [--branch name] [--json] # whether a branch needs the security review, and why: sign-in, permissions, outside input, personal data, payments, secrets, dependencies
437
438
  code-kit status [--base ref] [--json] # stories and requirements against the spec
438
439
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
440
+ code-kit worktrees [--json] | worktrees release <branch> | worktrees prune # the project's worktrees; free one for a fresh agent on its story, or remove every merged and clean one, keeping their reports
439
441
  code-kit instructions [--write] [--json] | instructions show lead|lane # code-kit's part of CLAUDE.md and the lane agents: where each stands, brought up to date, or printed
440
442
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
441
443
  code-kit merge <branch> --delegated|--person [--into branch] # merge a branch that passes verify: the lead where approvals.delegate.merge is on, or the person
@@ -482,33 +484,33 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated [--story ST
482
484
 
483
485
  `.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
484
486
 
485
- | Field | Required | What it is |
486
- | ------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
487
- | `version` | yes | `1` |
488
- | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
489
- | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
490
- | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
491
- | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
492
- | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
493
- | `anyActor` | no | Paths every writer may change (e.g. the lockfile), read-only agents included. `.claude/state/**` is always included. An entry may be `{ "glob", "reviewer" }` to name the lane (or `"lead"`) that reviews changes to it; otherwise the lead reviews them, and `who` and `trace` say so. |
494
- | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude?, owners? }`: the agent name maps a subagent to its lane; `owners` (GitHub handles) are the people whose sessions work it, in a team (needs `team`). A worktree with a `.lane` file holding the lane name makes the main session that lane. |
495
- | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
496
- | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them. Init sets it for every lane that owns screens; `/code-kit:design` adds approved designs |
497
- | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
498
- | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
499
- | `shell.block[]` | no | `{ pattern, why, person?, command? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on. `command: true` matches the pattern from the start of each command the line runs, so a heredoc or quoted text that mentions it isn't refused |
500
- | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
501
- | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
502
- | `tests[]` | no | Globs naming the test files, for the weakened-test and fix rules. Default: the usual conventions (`*.test.*`, `*.spec.*`, `__tests__/`, `test/`, `tests/`, `e2e/`, `*_test.go`, `test_*.py`). |
503
- | `regression.prepare` | no | A command run in a fresh checkout before a fix's tests run there without the fix, such as `npm ci`. Without it, the checkout's `node_modules` and virtualenvs are linked in. |
504
- | `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
505
- | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected; `true` says the project wants that tool, so until it's set up here `next` gives its setup before any code. See [Adapters](#adapters). |
506
- | `approvals.lead` | no | `true` lets the lead act on what the person says in chat, for when `!` isn't available (on a phone, say): it records their approvals with `code-kit approve`, and runs `shell.block` commands marked `person`. Default `false`. See [Approvals](#approvals). |
507
- | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
508
- | `setup` | no | The version of init's setup the project has (3 now). Without it, or lower, `next` names what the project is missing (the design gate, design stories before screens, a QA lane, a security reviewer) and starts no new story until re-running init adds it |
509
- | `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider? }`. `techLead` is the GitHub handle of the person who owns the plan, specs and contracts; `staleDays` is how many working days without a push make a claim stale (default 3); `intents` is the discussion category intents go in (default `Intents`; create it once in the repository's Settings › Discussions); `board` is `{ owner, number }`, a GitHub Project the plan is mirrored onto; `provider` is `"github"` (default) or `{ command, fallback? }` for another team provider, such as the code-kit teams hub; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached. See docs/specs/16-teams.md. |
510
- | `branches.protected` | no | Default `["main", "master"]` |
511
- | `harness` | no | What the mod's harness does in the session: `autonomy` (`autonomous`, `propose` or `off`; default `autonomous`), `hold.minutes` (how long a call waiting on your approval is held, 0 to 10; default 2), `stall.nudgeMinutes` and `stall.restartMinutes` (default 5 and 10), `stall.maxRestarts` (default 2), `agents.reviewer.on` (default `false`) and `agents.reviewer.model` (default the session's), `agents.lanes.model` (the model each lane agent starts on, unless the lead's call or the lane's agent file names one; default the session's), `background.pauseAtPercent` (default 80), `compact.milestone` (when a milestone finishes, compact the lead's context: `ask`, `auto` or `off`; default `ask`), `notify.on` (a system notification when something new needs you; default `true`) and `report.on` (agents draft bug reports for code-kit, which you send or discard; default `false`). Change them with `/harness` or `code-kit settings set`. |
487
+ | Field | Required | What it is |
488
+ | ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
489
+ | `version` | yes | `1` |
490
+ | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
491
+ | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
492
+ | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
493
+ | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
494
+ | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
495
+ | `anyActor` | no | Paths every writer may change (e.g. the lockfile), read-only agents included. `.claude/state/**` is always included. An entry may be `{ "glob", "reviewer" }` to name the lane (or `"lead"`) that reviews changes to it; otherwise the lead reviews them, and `who` and `trace` say so. |
496
+ | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude?, owners? }`: the agent name maps a subagent to its lane; `owners` (GitHub handles) are the people whose sessions work it, in a team (needs `team`). A worktree with a `.lane` file holding the lane name makes the main session that lane. |
497
+ | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
498
+ | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them. Init sets it for every lane that owns screens; `/code-kit:design` adds approved designs |
499
+ | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
500
+ | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
501
+ | `shell.block[]` | no | `{ pattern, why, person?, command? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on. `command: true` matches the pattern from the start of each command the line runs, so a heredoc or quoted text that mentions it isn't refused |
502
+ | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
503
+ | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
504
+ | `tests[]` | no | Globs naming the test files, for the weakened-test and fix rules. Default: the usual conventions (`*.test.*`, `*.spec.*`, `__tests__/`, `test/`, `tests/`, `e2e/`, `*_test.go`, `test_*.py`). |
505
+ | `regression.prepare` | no | A command run in a fresh checkout before a fix's tests run there without the fix, such as `npm ci`. Without it, the checkout's `node_modules` and virtualenvs are linked in. |
506
+ | `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
507
+ | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected; `true` says the project wants that tool, so until it's set up here `next` gives its setup before any code. See [Adapters](#adapters). |
508
+ | `approvals.lead` | no | `true` lets the lead act on what the person says in chat, for when `!` isn't available (on a phone, say): it records their approvals with `code-kit approve`, and runs `shell.block` commands marked `person`. Default `false`. See [Approvals](#approvals). |
509
+ | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
510
+ | `setup` | no | The version of init's setup the project has (3 now). Without it, or lower, `next` names what the project is missing (the design gate, design stories before screens, a QA lane, a security reviewer) and starts no new story until re-running init adds it |
511
+ | `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider? }`. `techLead` is the GitHub handle of the person who owns the plan, specs and contracts; `staleDays` is how many working days without a push make a claim stale (default 3); `intents` is the discussion category intents go in (default `Intents`; create it once in the repository's Settings › Discussions); `board` is `{ owner, number }`, a GitHub Project the plan is mirrored onto; `provider` is `"github"` (default) or `{ command, fallback? }` for another team provider, such as the code-kit teams hub; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached. See docs/specs/16-teams.md. |
512
+ | `branches.protected` | no | Default `["main", "master"]` |
513
+ | `harness` | no | What the mod's harness does in the session: `autonomy` (`autonomous`, `propose` or `off`; default `autonomous`), `hold.minutes` (how long a call waiting on your approval is held, 0 to 10; default 2), `stall.nudgeMinutes` and `stall.restartMinutes` (default 5 and 10), `stall.maxRestarts` (default 2), `agents.reviewer.on` (default `false`) and `agents.reviewer.model` (code-kit's reviewer and the security reviewer; default `sonnet`), `agents.lanes.model` (the model each lane agent starts on, unless the lead's call or the lane's agent file names one; default the session's), `background.pauseAtPercent` (default 80), `compact.at` (compact the lead's context between turns once it's past this share of its window; default 40, 0 for never), `compact.milestone` (when a milestone finishes, compact the lead's context: `ask`, `auto` or `off`; default `ask`), `agents.lanes.contextLimit` (the context a lane agent may reach before it hands its story to a fresh agent; default 250,000 tokens), `notify.on` (a system notification when something new needs you; default `true`) and `report.on` (agents draft bug reports for code-kit, which you send or discard; default `false`). Change them with `/harness` or `code-kit settings set`. |
512
514
 
513
515
  Add `.claude/approvals/`, `.claude/state/` and `.claude/code-kit.draft.json` to `.gitignore`, and `.claude/approval-log.jsonl merge=union` to `.gitattributes`, so merging branches that both logged changes keeps every entry. Commit `.claude/code-kit.baseline.json` if the project has one.
514
516
 
package/bin/code-kit.mjs CHANGED
@@ -33,8 +33,9 @@
33
33
  // the harness settings; set is the person's own change
34
34
  // record approvals a person gave in chat (with approvals.lead on, the lead runs it)
35
35
  // --config <file> reads a draft (.claude/code-kit.draft.json) instead of .claude/code-kit.json.
36
- import { execFileSync } from 'node:child_process';
36
+ import { execFileSync, spawnSync } from 'node:child_process';
37
37
  import {
38
+ copyFileSync,
38
39
  existsSync,
39
40
  mkdirSync,
40
41
  mkdtempSync,
@@ -1930,8 +1931,84 @@ function instructionsCommand() {
1930
1931
  );
1931
1932
  }
1932
1933
 
1934
+ /** LEAN-6: a worktree of the project, with its branch, story, report and whether it's merged and clean. */
1935
+ function worktreeList(config) {
1936
+ const root = resolve('.');
1937
+ const run = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8' }).stdout?.trim() ?? '';
1938
+ const from = findBase(config, false);
1939
+ const blocks = run('worktree', 'list', '--porcelain').split('\n\n').filter(Boolean);
1940
+ return blocks.slice(1).map((b) => {
1941
+ const path = /^worktree (.+)$/m.exec(b)?.[1];
1942
+ const branch = /^branch refs\/heads\/(.+)$/m.exec(b)?.[1] ?? null;
1943
+ const report = branch ? `.claude/state/reports/${branch.replace(/[^\w.-]+/g, '_')}.md` : null;
1944
+ const status = spawnSync('git', ['-C', path, 'status', '--porcelain'], { encoding: 'utf8' });
1945
+ return {
1946
+ path,
1947
+ branch,
1948
+ story: branch ? storyOfBranch(branch) : null,
1949
+ merged: Boolean(
1950
+ branch &&
1951
+ from &&
1952
+ spawnSync('git', ['merge-base', '--is-ancestor', branch, from], { cwd: root }).status === 0,
1953
+ ),
1954
+ clean: status.status === 0 && !status.stdout.trim(),
1955
+ report: report && existsSync(join(path, report)) ? report : null,
1956
+ };
1957
+ });
1958
+ }
1959
+
1960
+ /** LEAN-6: releases a worktree: its report kept in the main checkout, the worktree removed, its branch kept. */
1961
+ function releaseWorktree(w) {
1962
+ if (!w.clean)
1963
+ die(
1964
+ `${w.branch ?? w.path} has uncommitted changes; nothing was removed. Commit them on the branch, or have its agent do so, then release it.`,
1965
+ );
1966
+ if (w.report) {
1967
+ mkdirSync(join(resolve('.'), '.claude/state/reports'), { recursive: true });
1968
+ copyFileSync(join(w.path, w.report), join(resolve('.'), w.report));
1969
+ }
1970
+ const removed = spawnSync('git', ['worktree', 'remove', w.path], {
1971
+ cwd: resolve('.'),
1972
+ encoding: 'utf8',
1973
+ });
1974
+ if (removed.status !== 0) die(`git worktree remove failed: ${removed.stderr.trim()}`);
1975
+ return `${w.branch ?? w.path}${w.report ? ` (its report kept at ${w.report})` : ''}`;
1976
+ }
1977
+
1978
+ /**
1979
+ * LEAN-6: `worktrees` lists the project's worktrees; `release <branch>` frees one for a fresh agent on the
1980
+ * same story; `prune` releases every one whose branch is merged and that holds nothing uncommitted.
1981
+ */
1982
+ function worktreesCommand() {
1983
+ const { config } = load();
1984
+ const list = worktreeList(config);
1985
+ if (rest[0] === 'release') {
1986
+ const w = list.find((x) => x.branch === rest[1] || x.path === rest[1]);
1987
+ if (!w) die(`No worktree has ${rest[1] ?? '<branch>'} checked out.`);
1988
+ return out(`Released ${releaseWorktree(w)}. The branch and its commits stay.`);
1989
+ }
1990
+ if (rest[0] === 'prune') {
1991
+ const done = list.filter((w) => w.merged && w.clean);
1992
+ if (!done.length) return out('No worktree is both merged and clean; nothing was removed.');
1993
+ for (const w of done) out(`Released ${releaseWorktree(w)}.`);
1994
+ return;
1995
+ }
1996
+ if (flag('--json')) return out(JSON.stringify(list, null, 2));
1997
+ if (!list.length) return out('No worktrees besides the main checkout.');
1998
+ for (const w of list)
1999
+ out(
2000
+ ` ${w.branch ?? '(detached)'}${w.story ? ` · ${w.story}` : ''}: ${w.merged ? 'merged' : 'not merged'}, ${w.clean ? 'clean' : 'uncommitted changes'} ${w.path}`,
2001
+ );
2002
+ const prunable = list.filter((w) => w.merged && w.clean).length;
2003
+ if (prunable)
2004
+ out(
2005
+ `\n${prunable} merged and clean: \`code-kit worktrees prune\` removes them, keeping their reports.`,
2006
+ );
2007
+ }
2008
+
1933
2009
  const commands = {
1934
2010
  check,
2011
+ worktrees: worktreesCommand,
1935
2012
  instructions: instructionsCommand,
1936
2013
  loop: loopCommand,
1937
2014
  approve: () => (rest.length ? approve(rest) : usage()),
@@ -1970,6 +2047,7 @@ function usage() {
1970
2047
  ' | security [--base ref] [--branch name] [--json]\n' +
1971
2048
  ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
1972
2049
  ' | instructions [--write] [--json] | instructions show lead|lane\n' +
2050
+ ' | worktrees [--json] | worktrees release <branch> | worktrees prune\n' +
1973
2051
  ' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
1974
2052
  ' | merge <branch> --delegated|--person [--into branch] | requests [--json] | requests drop <n>|all\n' +
1975
2053
  ' | ask "<question>" [--story ST-n] [--blocks] [--why "…"] [--option "Label: effect"]... | ask [--json]\n' +
@@ -236,6 +236,14 @@ if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+settings\s+set\b`)))
236
236
  block(
237
237
  `Blocked: changing the harness settings is the person's own act, from the code-kit pane (/harness) or their terminal. Tell them what you'd change and why, and stop.\nCommand: ${cmd}`,
238
238
  );
239
+ // LEAN-6: releasing or pruning worktrees is the lead's, which knows which stories are still at work.
240
+ if (
241
+ runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+worktrees\s+(?:release|prune)\b`)) &&
242
+ actor.kind !== 'lead'
243
+ )
244
+ block(
245
+ `Blocked: releasing worktrees is the lead's, which knows which stories are still at work. Finish your story and hand back.\nCommand: ${cmd}`,
246
+ );
239
247
  // ONB-8: code-kit's instructions are the lead's to bring up to date, like the rest of the kit.
240
248
  if (
241
249
  runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+instructions\b.*\s--write\b`)) &&
@@ -47,12 +47,13 @@ export const SETTINGS = {
47
47
  about: 'A background reviewer for each finished branch',
48
48
  },
49
49
  'agents.reviewer.model': {
50
- default: null,
50
+ default: 'sonnet',
51
51
  problem: (v) =>
52
52
  v === null || (typeof v === 'string' && v.length > 0)
53
53
  ? null
54
54
  : `must be a model (${MODELS.join(', ')}, or an id), or null for the session's`,
55
- about: "The reviewer's model; none means the session's",
55
+ about:
56
+ "The reviewers' model (code-kit's reviewer and the security reviewer); none means the session's",
56
57
  },
57
58
  'agents.lanes.model': {
58
59
  default: null,
@@ -63,12 +64,26 @@ export const SETTINGS = {
63
64
  about:
64
65
  "The lane agents' model, set as each is dispatched; none means the session's. A lane's agent file with its own model keeps it",
65
66
  },
67
+ 'agents.lanes.contextLimit': {
68
+ default: 250000,
69
+ problem: (v) =>
70
+ Number.isInteger(v) && v >= 0 ? null : 'must be a whole number of tokens, 0 for no limit',
71
+ about:
72
+ "The context a lane agent may reach before it's asked to wrap up and hand its story to a fresh agent; 0 for no limit",
73
+ },
66
74
  'background.pauseAtPercent': {
67
75
  default: 80,
68
76
  problem: (v) =>
69
77
  Number.isFinite(v) && v > 0 && v <= 100 ? null : 'must be a percentage from 1 to 100',
70
78
  about: "The plan's 5-hour use at which background agents pause",
71
79
  },
80
+ 'compact.at': {
81
+ default: 40,
82
+ problem: (v) =>
83
+ Number.isFinite(v) && v >= 0 && v <= 100 ? null : 'must be a percentage from 0 (off) to 100',
84
+ about:
85
+ "How full the lead's context may get, as a share of its window, before it's compacted between turns; 0 for never",
86
+ },
72
87
  'compact.milestone': {
73
88
  default: 'ask',
74
89
  problem: (v) => (COMPACT.includes(v) ? null : `must be one of ${COMPACT.join(', ')}`),
@@ -7,7 +7,7 @@ import { dirname, join } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  /** The version of code-kit's instructions: raised whenever the templates' rules change. */
10
- export const INSTRUCTIONS_VERSION = 4;
10
+ export const INSTRUCTIONS_VERSION = 5;
11
11
 
12
12
  const TEMPLATES = fileURLToPath(new URL('../../templates/', import.meta.url));
13
13
  const BEGIN = (v) => `<!-- code-kit:begin v${v} -->`;
@@ -1,7 +1,7 @@
1
1
  // What to do next, from the project's state alone: no docs → spec-design; a spec still being shaped →
2
2
  // spec-design; a ready spec or existing code without the kit → init; then, from the plan and git:
3
3
  // review finished stories, dispatch ready ones, wait on running ones, or plan the next milestone.
4
- import { execFileSync } from 'node:child_process';
4
+ import { execFileSync, spawnSync } from 'node:child_process';
5
5
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
6
  import { join, posix } from 'node:path';
7
7
  import { loadConfig, mergeDelegation } from './config.mjs';
@@ -254,6 +254,12 @@ export function nextStep(root, base) {
254
254
  attention.push(
255
255
  `${unbuildable.join(', ')} can't be dispatched until the plan's gaps are fixed.`,
256
256
  );
257
+ // LEAN-6: worktrees whose story is merged and which hold nothing uncommitted are only disk space.
258
+ const leftovers = mergedWorktrees(root, config);
259
+ if (leftovers)
260
+ attention.push(
261
+ `${leftovers} worktree${leftovers === 1 ? ' is' : 's are'} merged and clean; \`code-kit worktrees prune\` removes them, keeping their reports.`,
262
+ );
257
263
  // ONB-8: code-kit's own instructions to the lead and the lanes, brought up to date before the next step:
258
264
  // a newer code-kit's rules reach the agents only through them.
259
265
  const instructions = instructionsState(root, config);
@@ -485,3 +491,23 @@ export function missingSkills(root) {
485
491
  }
486
492
  return out;
487
493
  }
494
+
495
+ /** LEAN-6: how many of the project's worktrees have a merged branch and nothing uncommitted. */
496
+ function mergedWorktrees(root, config) {
497
+ const list = git(root, 'worktree', 'list', '--porcelain');
498
+ if (!list) return 0;
499
+ const from = config.branches?.protected?.[0];
500
+ if (!from || !git(root, 'rev-parse', '--verify', '-q', from)) return 0;
501
+ let n = 0;
502
+ for (const block of list.split('\n\n').slice(1)) {
503
+ const path = /^worktree (.+)$/m.exec(block)?.[1];
504
+ const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
505
+ if (!path || !branch) continue;
506
+ const merged =
507
+ spawnSync('git', ['merge-base', '--is-ancestor', branch, from], { cwd: root }).status === 0;
508
+ if (!merged) continue;
509
+ const status = spawnSync('git', ['-C', path, 'status', '--porcelain'], { encoding: 'utf8' });
510
+ if (status.status === 0 && !status.stdout.trim()) n++;
511
+ }
512
+ return n;
513
+ }
@@ -340,10 +340,27 @@ export function milestonesDone(stories = []) {
340
340
  return [...by].filter(([, list]) => list.every((s) => s.state === 'done')).map(([m]) => m);
341
341
  }
342
342
 
343
- /** LEAN-3: what the lead's summary keeps when its context is compacted after a milestone. */
344
- export const compactInstructions = (milestone) =>
343
+ /** LEAN-3, LEAN-4: what the lead's summary keeps when its context is compacted, after a milestone or not. */
344
+ export const compactInstructions = (milestone = null) =>
345
345
  [
346
- `${milestone} is done; the build goes on with the next milestone.`,
346
+ milestone
347
+ ? `${milestone} is done; the build goes on with the next milestone.`
348
+ : "The lead's context is compacted between turns; the build goes on.",
347
349
  'Keep: the milestone just finished, in a line; every story in flight now, with its lane, its branch and where it stands (building, in review, in the merge queue, waiting and on what); the decisions and questions waiting on the person; what the person decided or asked for in this session that still applies; and the rules and preferences they set.',
348
350
  "Drop: the finished stories' details, lane reports and hand-backs, command output, diffs and file contents. Each is in git, the plan or the lanes' report files, to read again when it's needed.",
349
351
  ].join('\n');
352
+
353
+ /** LEAN-5: the context a model request carried: what it sent, read from the cache, and wrote to it. */
354
+ export const requestContext = (usage) =>
355
+ (usage?.input_tokens ?? 0) +
356
+ (usage?.cache_read_input_tokens ?? 0) +
357
+ (usage?.cache_creation_input_tokens ?? 0);
358
+
359
+ /** LEAN-5: what a lane agent past its context limit is told. */
360
+ export const wrapUpText = (limit) =>
361
+ `Your context is past ${Math.round(limit / 1000)}k tokens, and every step from here rereads all of it. Finish the step you're on, commit your work, write your report, and hand back now. The lead carries the story on with a fresh agent that reads your report.`;
362
+
363
+ /** LEAN-5, LEAN-6: the lead's prompt to carry a story on with a fresh agent once its agent handed back at the limit. */
364
+ export function continuePrompt({ type, story, branch, cli = 'code-kit' }) {
365
+ return `${type} handed ${story} back at its context limit, with its work committed${branch ? ` on ${branch}` : ''}. Carry the story on with a fresh agent: run node "${cli}" worktrees release ${branch ?? '<branch>'} (it keeps the commits and moves the report to the main checkout), then dispatch ${story} again with the same brief, adding "Read your earlier report first: .claude/state/reports/${(branch ?? '<branch>').replace(/[^\w.-]+/g, '_')}.md in the main checkout": /code-kit:dispatch ${story}`;
366
+ }
@@ -61,6 +61,9 @@ import {
61
61
  COMPACT_FLOOR,
62
62
  PARK_MS,
63
63
  compactInstructions,
64
+ continuePrompt,
65
+ requestContext,
66
+ wrapUpText,
64
67
  milestonesDone,
65
68
  parkWatch,
66
69
  readOutcome,
@@ -1049,6 +1052,23 @@ export function register(on) {
1049
1052
  const who = attribution(agentId, agentsSeen.get(agentId) ?? null, model.check, model.state);
1050
1053
  ledger = withUsage(ledger, who, usage);
1051
1054
  $.ui.invalidate('ui.render');
1055
+ // LEAN-5: a lane agent past its context limit is told to wrap up; its story goes on fresh.
1056
+ const limit = harness().agents.lanes.contextLimit;
1057
+ const agent = agentId ? agentsSeen.get(agentId) : null;
1058
+ const lane = Object.entries(model.check?.lanes ?? {}).find(
1059
+ ([, l]) => l.agent === agent?.type,
1060
+ );
1061
+ if (!limit || !lane || requestContext(usage) < limit) return;
1062
+ const a = activityOf(agentId, await $.clock.now());
1063
+ if (a.overflow) return;
1064
+ a.overflow = true;
1065
+ a.lane = lane[0];
1066
+ a.story = who.story ?? a.story ?? lane[0];
1067
+ await act.record(
1068
+ 'wrap up',
1069
+ `${lane[0]} · ${a.story} at ${Math.round(requestContext(usage) / 1000)}k`,
1070
+ );
1071
+ await $.session.send({ to: { agentId }, text: wrapUpText(limit) }).catch(() => {});
1052
1072
  },
1053
1073
  // --- the lead loop (spec 05) ---------------------------------------------------------------
1054
1074
  record: async (kind, target, prompt) => {
@@ -1177,14 +1197,15 @@ export function register(on) {
1177
1197
  const done = milestonesDone(model.state?.stories ?? []);
1178
1198
  if (loop.milestones === null) {
1179
1199
  loop.milestones = new Set(done);
1180
- return false;
1200
+ return act.compactAtThreshold();
1181
1201
  }
1182
1202
  const fresh = done.filter((m) => !loop.milestones.has(m));
1183
- if (!fresh.length) return false;
1203
+ // LEAN-4: past compact.at, the lead's context is compacted between turns, milestone or not.
1204
+ if (!fresh.length) return act.compactAtThreshold();
1184
1205
  const mode = harness().compact?.milestone ?? 'ask';
1185
1206
  if (mode === 'off' || offering || !where.interactive || (lease.other && !lease.mine)) {
1186
1207
  for (const m of fresh) loop.milestones.add(m);
1187
- return false;
1208
+ return act.compactAtThreshold();
1188
1209
  }
1189
1210
  const box = await $.prompt.read().catch(() => null);
1190
1211
  if (box?.text?.trim()) return false;
@@ -1219,6 +1240,25 @@ export function register(on) {
1219
1240
  });
1220
1241
  return true;
1221
1242
  },
1243
+ // LEAN-4: the lead's context compacted at once when it's past compact.at; true when it took over.
1244
+ compactAtThreshold: async () => {
1245
+ const at = harness().compact?.at ?? 40;
1246
+ if (!at || offering || !where.interactive || (lease.other && !lease.mine)) return false;
1247
+ const percent = (await $.session.usage().catch(() => null))?.context?.percent ?? 0;
1248
+ if (percent < Math.max(at, COMPACT_FLOOR)) return false;
1249
+ const box = await $.prompt.read().catch(() => null);
1250
+ if (box?.text?.trim()) return false;
1251
+ Promise.resolve()
1252
+ .then(() => $.session.compact({ instructions: compactInstructions() }))
1253
+ .catch((err) => ({ skip: String(err?.message ?? err) }))
1254
+ .then(async (r) => {
1255
+ if (r?.skip) notice = `The lead's context wasn't compacted: ${r.skip}`;
1256
+ await act.record('compact', `at ${Math.round(percent)}%`);
1257
+ $.ui.invalidate('ui.render');
1258
+ await act.step('resume').catch(() => {});
1259
+ });
1260
+ return true;
1261
+ },
1222
1262
  // LOOP-11: the lead's last reply, read once before the loop prompts it again. A reply that waits on
1223
1263
  // the person, or a turn they stopped, holds the loop until they speak; a step the lead didn't take
1224
1264
  // holds that step while it's still next.
@@ -1258,9 +1298,15 @@ export function register(on) {
1258
1298
  return;
1259
1299
  }
1260
1300
  if (!reply.text.trim()) return act.parkUnmoved(sent, null);
1261
- const said = await act.sideCall(
1262
- outcomePrompt({ prompt: sent?.prompt ?? null, answer: reply.text }),
1263
- );
1301
+ // LEAN-6: the reply and the prompt it answered are enough to read it, so a small model reads
1302
+ // them, instead of a fork over the lead's whole conversation on its own model at every turn's end.
1303
+ const said = await $.model
1304
+ .complete({
1305
+ model: 'sonnet',
1306
+ prompt: outcomePrompt({ prompt: sent?.prompt ?? null, answer: reply.text }),
1307
+ maxTokens: 600,
1308
+ })
1309
+ .catch(() => ({ isAnswered: false }));
1264
1310
  const heard = said.isAnswered ? readOutcome(said.text) : null;
1265
1311
  // Spec 05's data: what came of the step, once known.
1266
1312
  const record = sent ? loop.steps.findLast((x) => x.prompt === sent.prompt) : null;
@@ -2021,6 +2067,16 @@ export function register(on) {
2021
2067
  // SET-4: a lane agent starts on the lane model the person set, unless the lead's call names one or
2022
2068
  // the lane's agent file sets its own.
2023
2069
  on('agent.spawn', async ($, e, next) => {
2070
+ // LEAN-7: the security reviewer runs on the reviewers' model, unless the call or its file names one.
2071
+ const reviewerModel = harness().agents.reviewer.model;
2072
+ if (e.subagentType === 'security-reviewer' && reviewerModel && !e.model) {
2073
+ const file = where
2074
+ ? await $.fs.read(`${where.cwd}/.claude/agents/security-reviewer.md`).catch(() => null)
2075
+ : null;
2076
+ const own = typeof file === 'string' ? agentFileModel(file) : null;
2077
+ if (!own || own === 'inherit') return next({ ...e, model: reviewerModel });
2078
+ return next(e);
2079
+ }
2024
2080
  const setting = harness().agents.lanes.model;
2025
2081
  const lanes = model.check?.lanes ?? {};
2026
2082
  if (!setting || e.model || !Object.values(lanes).some((l) => l.agent === e.subagentType))
@@ -2078,7 +2134,25 @@ export function register(on) {
2078
2134
  const res = await next(e);
2079
2135
  // LOOP-3: a lane agent that has handed back its work isn't stalled while it waits; a message that
2080
2136
  // starts it again shows as its next tool call.
2081
- if (e.agentId) activityOf(e.agentId, await $.clock.now()).handedBack = true;
2137
+ if (e.agentId) {
2138
+ const a = activityOf(e.agentId, await $.clock.now());
2139
+ a.handedBack = true;
2140
+ // LEAN-5: an agent that handed back at its limit: the lead carries its story on with a fresh one.
2141
+ if (a.overflow && !a.continued && act) {
2142
+ a.continued = true;
2143
+ const agent = agentsSeen.get(e.agentId);
2144
+ const story = (model.state?.stories ?? []).find((s) => s.id === a.story);
2145
+ loop.pending.push({
2146
+ story: a.story,
2147
+ prompt: continuePrompt({
2148
+ type: agent?.type ?? 'The lane agent',
2149
+ story: a.story,
2150
+ branch: story?.branch ?? null,
2151
+ cli: where?.cli,
2152
+ }),
2153
+ });
2154
+ }
2155
+ }
2082
2156
  // REVW-3: a subagent's last turn may be the reviewer's report.
2083
2157
  if (e.agentId && act) await act.reviewed(e.agentId, e.answer).catch(() => {});
2084
2158
  // LOOP-1: the lead is idle; the loop's next step, if one is due (submitted from turn.complete,
@@ -382,6 +382,8 @@ function stub(on: any, w: World) {
382
382
  });
383
383
  on('model.complete', ($: any, e: any) => {
384
384
  w.sideAsks.push({ model: e.model, prompt: e.prompt });
385
+ // LEAN-6: the loop reads the lead's reply with a small model; the test's reading answers it.
386
+ if (/Say where that reply left things/.test(e.prompt)) return { value: w.sideReply };
385
387
  return { value: { isAnswered: true, text: 'From the facts alone.', usage: null } };
386
388
  });
387
389
  // Claude Code's own settings hooks beneath: none answer a permission denial.
@@ -2312,6 +2314,60 @@ test(
2312
2314
  },
2313
2315
  );
2314
2316
 
2317
+ test('LEAN-5 a lane agent past its context limit is told to wrap up, and its story goes on with a fresh agent', async ($, on) => {
2318
+ const w = project();
2319
+ w.next = { step: 'wait', args: '', then: [] };
2320
+ w.agents = [
2321
+ { id: 'a1', type: 'web-engineer', description: 'ST-4 booking form', status: 'running' },
2322
+ ];
2323
+ const clock = await start($, on, w);
2324
+ await clock.advance(2000);
2325
+ await request($, 'a1', 100);
2326
+ expect(w.sent).toEqual([]);
2327
+ // 270k of context, past the 250k limit: told once, however many requests follow.
2328
+ await request($, 'a1', 300);
2329
+ await request($, 'a1', 320);
2330
+ expect(w.sent).toHaveLength(1);
2331
+ expect(w.sent[0].text).toMatch(/^Your context is past 250k tokens/);
2332
+ await $.turn.complete({
2333
+ turnId: 't-a1',
2334
+ agentId: 'a1',
2335
+ answer: 'ST-4: committed, report written.',
2336
+ durationMs: 1,
2337
+ isAborted: false,
2338
+ } as any);
2339
+ await leadTurn($, 't-2');
2340
+ await settle(clock);
2341
+ expect(w.prompts.at(-1)).toMatch(
2342
+ /^web-engineer handed ST-4 back at its context limit, with its work committed on web\/st-4\. Carry the story on with a fresh agent: run node ".*" worktrees release web\/st-4/,
2343
+ );
2344
+ expect(w.prompts.at(-1)).toMatch(
2345
+ /Read your earlier report first: \.claude\/state\/reports\/web_st-4\.md/,
2346
+ );
2347
+ });
2348
+
2349
+ test("LEAN-4 past compact.at the lead's context is compacted between turns, without asking", async ($, on) => {
2350
+ const w = project();
2351
+ const compacted: string[] = [];
2352
+ on('session.compact', ($: any, e: any) => {
2353
+ compacted.push(e.instructions);
2354
+ return {
2355
+ messages: [{ role: 'user', text: 'Summary: ST-4 in progress on web/st-4.', toolUses: [] }],
2356
+ };
2357
+ });
2358
+ w.contextPercent = 35;
2359
+ const clock = await start($, on, w);
2360
+ await leadTurn($);
2361
+ await settle(clock);
2362
+ expect(compacted).toEqual([]);
2363
+ w.contextPercent = 46;
2364
+ await leadTurn($, 't-2');
2365
+ await settle(clock);
2366
+ expect(compacted).toHaveLength(1);
2367
+ expect(compacted[0]).toMatch(/^The lead's context is compacted between turns/);
2368
+ expect(w.asked.filter((a) => /Compact/.test(a.question))).toEqual([]);
2369
+ });
2370
+
2315
2371
  test('LOOP-3 an agent whose work is done is never nudged or restarted: it handed back, or its branch waits in the merge queue', async ($, on) => {
2316
2372
  const w = project();
2317
2373
  w.next = { step: 'wait', args: '', then: [] };
@@ -2477,7 +2533,9 @@ test("LOOP-11 a reply that waits on the person holds the loop; the dialog asks t
2477
2533
  await leadSays($, 't-2', 'Before I dispatch ST-9: should its prices include GST?');
2478
2534
  await settle(clock);
2479
2535
  // The side call read the reply over the lead's own conversation, with the step it answered.
2480
- expect(w.sideAsks.at(-1).fork).toBe(true);
2536
+ // LEAN-6: read by a small model from the reply itself, not a fork over the lead's conversation.
2537
+ expect(w.sideAsks.at(-1).fork).toBeUndefined();
2538
+ expect(w.sideAsks.at(-1).model).toBe('sonnet');
2481
2539
  expect(w.sideAsks.at(-1).prompt).toMatch(/prompted you: "Dispatch ST-9/);
2482
2540
  expect(w.sideAsks.at(-1).prompt).toMatch(/should its prices include GST\?/);
2483
2541
  expect(w.asked.at(-1)).toEqual({
@@ -2541,7 +2599,8 @@ test("LEAN-3 when a milestone finishes, the person is asked to compact the lead'
2541
2599
  ...w.stories.map((s) => ({ ...s, milestone: 'M1' })),
2542
2600
  ];
2543
2601
  const compacted: string[] = [];
2544
- w.contextPercent = 62;
2602
+ // Under compact.at (40%), so only the milestone brings the question.
2603
+ w.contextPercent = 30;
2545
2604
  on('session.compact', ($: any, e: any) => {
2546
2605
  compacted.push(e.instructions);
2547
2606
  // A compaction leaves the summary in the conversation's place.
@@ -2561,6 +2620,7 @@ test("LEAN-3 when a milestone finishes, the person is asked to compact the lead'
2561
2620
  w.refs += ' m1 refs/heads/main-moved\n';
2562
2621
  await clock.advance(2000);
2563
2622
  w.answer = 'Compact now';
2623
+ w.contextPercent = 62;
2564
2624
  await leadTurn($, 't-2');
2565
2625
  await settle(clock);
2566
2626
  expect(w.asked.at(-1)?.question).toMatch(
@@ -2903,6 +2963,17 @@ test('SET-4 with no lane model set, lane agents start as Claude Code decides', a
2903
2963
  expect(w.spawned[0].model ?? null).toBe(null);
2904
2964
  });
2905
2965
 
2966
+ test("LEAN-7 the security reviewer starts on the reviewers' model, Sonnet unless set, and its own file's model wins", async ($, on) => {
2967
+ const w = project();
2968
+ await start($, on, w);
2969
+ await dispatch($, 'security-reviewer');
2970
+ await dispatch($, 'security-reviewer', 'opus');
2971
+ w.agentFiles['/work/.claude/agents/security-reviewer.md'] =
2972
+ '---\nname: security-reviewer\nmodel: opus\n---\n';
2973
+ await dispatch($, 'security-reviewer');
2974
+ expect(w.spawned.map((e) => e.model ?? null)).toEqual(['sonnet', 'opus', null]);
2975
+ });
2976
+
2906
2977
  // --- the reviewer -----------------------------------------------------------------------------------
2907
2978
 
2908
2979
  /** ST-4 finished on web/st-4, the reviewer on. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
5
5
  "license": "MIT",
6
6
  "author": "Warren Langeveldt",
@@ -48,6 +48,8 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" team claim ST-<n> --lease "<glob>,
48
48
 
49
49
  The lease is the paths the story will create or change: its lane's folders for this feature, and any contract it changes. A claim someone else holds is refused with who holds it; pick another story or ask them. Stories in a lane another person owns are theirs to dispatch. Making the story's branch also claims it, and each commit on it adds what it changed to the lease; merging it ends the claim.
50
50
 
51
+ One agent per story: never send a running lane agent a second story, or a send-back. A story that goes on (sent back, or handed back at its context limit) gets a fresh agent: first run `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" worktrees release <branch>`, which keeps the branch's commits and moves its report to the main checkout, then start the agent as below with the brief and "Read your earlier report first: `.claude/state/reports/<branch with / as _>.md` in the main checkout". A long-lived agent rereads its whole context on every step, which is most of what a build costs.
52
+
51
53
  Start each story with the Agent tool: `subagent_type` is the lane's agent, `isolation: "worktree"`, and run them in the background, with a `description` that starts with the story's id ("ST-12 booking form"): the loop reads it to know the story is in flight, and won't ask for it again while the agent works. Start every story in the batch in the same message so they run in parallel.
52
54
 
53
55
  The brief is the agent's whole context. Write it so the agent needs nothing else:
@@ -38,7 +38,7 @@ Any problem it reports means the branch goes back. Don't fix a lane's code yours
38
38
 
39
39
  ## 3. Check the work against the story
40
40
 
41
- Read the story, every requirement it cites, and the full diff (`git diff <base>...<branch>`). Read each changed file in full where the diff alone doesn't show the behaviour. Then, for each requirement:
41
+ Read the story and every requirement it cites. Then start from what's already been worked out, not the whole diff: the lane's report (`.claude/state/reports/<branch with / as _>.md`, in its worktree or the main checkout), the reviewer's findings when the background reviewer ran, and `git diff --stat <base>...<branch>`. Open the diff one file at a time (`git diff <base>...<branch> -- <file>`) for the files each requirement depends on and the files a finding names, and a changed file in full only where its diff doesn't show the behaviour. Everything you open stays in your context for the rest of the build, so open what the verdict needs. Then, for each requirement:
42
42
 
43
43
  | Requirement | Acceptance criterion | Built (where) | Test (which) | Verdict |
44
44
  | ----------- | -------------------- | ------------- | ------------ | ------- |
@@ -84,7 +84,7 @@ Report the table, `verify`'s result, the security review's findings (or that non
84
84
  node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" sent-back <branch> --reason "<the problems, in one line>"
85
85
  ```
86
86
 
87
- Send the list to the same lane agent if it's still running (SendMessage), or dispatch the story again with the list added to the brief. Set the story's status to `in progress`.
87
+ Dispatch the story again to a fresh agent with the list added to the brief (`/code-kit:dispatch`, which first releases the branch's worktree), never to the agent that built it: its context is already large. Set the story's status to `in progress`.
88
88
 
89
89
  - **Ask the person.** When the right answer is a product or scope decision (the spec is silent, or the lane found a real conflict), put the question to the person with your recommendation. Don't merge around it.
90
90
 
@@ -12,7 +12,7 @@ The project's specs are in the folder named by `docs.specs` in `.claude/code-kit
12
12
  1. **Find the section.** The task should name its requirement IDs (`BOOK-4`, headings in the specs) or a spec section. If it doesn't, search the specs folder for the domain nouns. Read the whole section, not just the matching lines, plus every section it references.
13
13
  2. **Read the principles.** Re-read the parts of `docs.principles` that apply: layers, ports and providers, events, testing and the rules for agents. Note this lane's layer rules from `layers` in the config.
14
14
  3. **Make a checklist.** One line per requirement: behaviour, fields, error cases, permissions, states, tests.
15
- 4. **Compare with the code.** For each line, search for where it's implemented, then read those files in full.
15
+ 4. **Compare with the code.** For each line, search for where it's implemented, then read what implements it: the functions and types the requirement touches, not every file around them.
16
16
  5. **Report** as a table:
17
17
 
18
18
  | Requirement | Status (done / partial / missing / conflicts) | Where | Note |
@@ -19,7 +19,7 @@ Always:
19
19
 
20
20
  - Work on the branch the brief names (`<lane>/st-<n>`). Start every task with the spec-check skill and save its report to .claude/state/spec-check/<branch>.md; the path guard blocks code until it exists.
21
21
  - Follow `{{docs.principles}}`, including the rules for AI agents. Hooks enforce ownership, layers and the project's checks; if one blocks you, fix the cause, never work around it.
22
- - Read every file you change in full, plus its imports and its callers.
22
+ - Read every file you change in full. Of what it imports and what calls it, read what your change needs: the definitions it uses and the call sites it changes. A context tool's fresh card for a file stands for reading it whole; read the source where the card is missing, stale or not enough.
23
23
  - Search before you build; reuse existing services, providers, components and schemas.
24
24
  - Write only in your owned paths. For anything else, write a short change request for the lead and stop.
25
25
  - You answer to the lead, never the person. A question the spec doesn't settle (a product choice, UX the spec is silent on, an account): write it for the lead, with the choices and what each does, and carry on with what doesn't depend on it, or stop. The lead decides, or asks the person.
@@ -28,7 +28,9 @@ Always:
28
28
  - Prove each acceptance criterion with a test whose title names the requirement ID, e.g. `BOOK-4 …`.
29
29
  - Fixing a bug: first write a test that fails because of the bug, through the code that's already there, then fix it. `verify` runs your test without the fix, and it must fail on an assertion, not because it calls something the fix adds.
30
30
  - Never weaken a test to get to green: no deleting, skipping or focusing tests, cutting cases or assertions, or rewriting snapshots. The commit is refused without the person's approval. Fix the code; if the test itself is wrong, say why in a change request for the lead.
31
+ - Run the narrowest tests that prove each step, with failures only in the output (a quiet reporter, or `| tail -40` on failure); run the full suite once, before you commit. Long output stays in your context and is paid for on every step after it.
31
32
  - Commit your work on your branch before finishing; the stop hook runs the checks and requires it.
32
33
  - Don't message the lead while you work: it hears from you once, when you finish. If you're blocked on something only the lead or the person can settle, finish and say so.
34
+ - One story per agent. If you're asked to wrap up because your context is large, finish the step you're on, commit, write your report and hand back: a fresh agent carries the story on from your report.
33
35
  - Before you finish, write your full report to `.claude/state/reports/<branch>.md` (the branch with `/` as `_`, as for spec-check): what you did and why, the spec section, the commands you ran with their output, what's left, and anything you need decided, with the choices and any text you'd otherwise paste. The stop hook requires it.
34
36
  - Finish with a short hand-back, at most ten lines: the story and branch, done or blocked, any decision the lead must make, and the report's path. No output or code in it: the lead reads the report when it reviews.