@warren-dean/code-kit 0.17.0 → 0.18.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.17.0",
12
+ "version": "0.18.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.17.0",
3
+ "version": "0.18.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,17 @@
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.18.0 (2026-10-17)
6
+
7
+ 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.
8
+
9
+ - Re-running `/code-kit:init` never updated code-kit's section of CLAUDE.md or the lane agents' `Always:` rules, so the rules from 0.16.0 and 0.17.0 never reached projects already set up. `code-kit instructions` now renders both from the templates and your config, between `<!-- code-kit:begin vN -->` and `<!-- code-kit:end -->` markers, and says whether each file is current, behind or written before the markers. `--write` brings marked parts up to date and never touches anything outside them. A part written before the markers, often tailored by an earlier init, is merged once by the lead, keeping your own lines. While any is behind, `code-kit next` says so before any other step and the loop asks the lead to update them. Init writes them with the same command.
10
+ - With Claude Code 2.1.301 or later, code-kit raises a system notification when something new needs you, such as an approval, a question from the lead, a stalled lane or a merge that's yours: "code-kit: the lead asks you". Each decision is notified once, and `notify.on` turns it off.
11
+ - **w** on a working lane in the Lanes pane puts that lane agent's transcript in the main view (Claude Code 2.1.301 or later).
12
+ - The loop no longer prompts the same story over and over. A story waiting on another wakes only when that story changes state, not on each of its commits, and a story the lead's turn left where it stood is parked whatever the reply said. In one build, "Review ST-160" had been prompted 31 times.
13
+ - A lane's hand-back of more than ten lines or 1,200 characters is refused by the stop hook, which also finds the report in the main checkout.
14
+ - Lanes may write scratch files in Claude Code's own temporary folder, and `git diff`, `log` and `show` may read the audit log's history.
15
+
5
16
  ## 0.17.0 (2026-10-17)
6
17
 
7
18
  The lead's context stays lean: lanes report to a file, and the lead's context is compacted when a milestone finishes.
package/README.md CHANGED
@@ -326,15 +326,17 @@ What waits for you there:
326
326
 
327
327
  Each button has a digit: type it in an empty prompt to press it. The band keeps one line per kind ("2 approvals waiting"); the Decisions tab says how many of each kind, then lists a short card for each decision, five at a time (**j** and **k** move), with the selected one in full below.
328
328
 
329
+ - **Notifications.** When something new needs you (an approval, a question from the lead, the lead waiting on you, a stalled lane, a call Claude Code refused, a step or a merge that's yours), code-kit raises a system notification through your notification channel, once per decision, from the session that runs the loop: "code-kit: the lead asks you". What was already waiting when the session started isn't announced. It needs Claude Code 2.1.301 or later; turn it off with `notify.on`.
330
+
329
331
  ### The lead loop
330
332
 
331
333
  The loop keeps the build moving between your prompts, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, its first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting.
332
334
 
333
335
  - **Each step.** Once the lead has finished a turn, with the lead idle and nothing typed in the prompt, the loop asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own. It never prompts the same step twice while nothing has changed, and never a story an agent is still on: one its lane agent is building, or a reviewer is reviewing, waits until that agent finishes ("waiting: ST-10 is still with its agent").
334
336
  - **It reads the lead's reply first.** Before prompting again, the loop reads the lead's last reply with a side call over the lead's own conversation. A reply that waits on you (a question, a design to look at, an approval), or a turn you stopped, holds the loop until you answer or speak to the lead: the band says what the lead waits on, and its question is asked in Claude Code's own question dialog. A step the lead didn't take waits in the band with its reason and **Ask again**.
335
- - **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 what it waits on moves: its own branch, the stories the lead named, or the unfinished stories that depend on it. Other lanes' commits don't wake it, and after 30 minutes with nothing moving the lead is asked once more.
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.
336
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**.
337
- - **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, 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.
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.
338
340
  - **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.
339
341
  - **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.
340
342
  - **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.
@@ -357,7 +359,7 @@ The loop keeps the build moving between your prompts, under `harness.autonomy`.
357
359
  - **Trail**: every requirement in the specs as a cell by spec, its glyph and colour saying whether it's done and tested, done but untested, in progress, todo, without a story or removed, with a legend counting each. **h**/**l** and **j**/**k** move between cells and specs (or pick one from the list), showing the requirement's stories, their branches, the code they changed and the tests that name it. Beneath the cells, the spec's trail drawn as a flow, each requirement to its stories, code and tests, with a missing link dashed in red.
358
360
  - **Queue**: the merge queue.
359
361
  - **Usage**.
360
- - **Keys.** **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story.
362
+ - **Keys.** **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **w** (watch: its agent's own transcript in the main view, on Claude Code 2.1.301 or later; the tasks list brings the conversation back), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story.
361
363
  - **A story.** **Enter** (or **o**) opens the selected lane's story: its requirements beside its spec-check report, the reviewer's findings by severity (each a way into the diff), `verify`'s problems, its usage and loop steps, and its diff against the base, file by file, with the acts its state allows. **b** or Escape goes back to the lanes.
362
364
 
363
365
  ### Other views
@@ -417,6 +419,8 @@ Owner backend lane / backend-engineer → jobs lane / jobs-engineer: 14 file(s),
417
419
 
418
420
  Lanes never change silently: only when you approve the diff.
419
421
 
422
+ code-kit's own instructions to your agents, its section of CLAUDE.md and each lane agent's `Always:` rules, are kept between markers and rendered from your config, so a newer code-kit can bring them up to date without touching anything you wrote around them. `code-kit instructions` says where each stands, and `--write` updates them. When they're behind, `code-kit next` says so before any other step and the loop asks the lead to update them. A setup from before the markers is merged once, by the lead, keeping your own lines.
423
+
420
424
  ## The CLI
421
425
 
422
426
  Run from the project root, as `code-kit <command>` once installed (`npm i -g @warren-dean/code-kit`), or `npx @warren-dean/code-kit <command>`. Add `--config .claude/code-kit.draft.json` to any command to read a draft instead. In a session, the `/code-kit:check` skill runs `check` and `unowned` for you. `next` also works in a project without code-kit, or outside a git repository.
@@ -432,6 +436,7 @@ code-kit verify [--base ref] [--branch name] [--no-checks] [--json] # a branch'
432
436
  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
433
437
  code-kit status [--base ref] [--json] # stories and requirements against the spec
434
438
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
439
+ 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
435
440
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
436
441
  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
437
442
  code-kit requests [--json] # approval requests still waiting, numbered, each the lead's or the person's to decide, the approvals in force, and every approval given
@@ -477,33 +482,33 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated [--story ST
477
482
 
478
483
  `.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
479
484
 
480
- | Field | Required | What it is |
481
- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
482
- | `version` | yes | `1` |
483
- | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
484
- | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
485
- | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
486
- | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
487
- | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
488
- | `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. |
489
- | `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. |
490
- | `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`. |
491
- | `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 |
492
- | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
493
- | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
494
- | `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 |
495
- | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
496
- | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
497
- | `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`). |
498
- | `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. |
499
- | `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). |
500
- | `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). |
501
- | `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). |
502
- | `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). |
503
- | `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 |
504
- | `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. |
505
- | `branches.protected` | no | Default `["main", "master"]` |
506
- | `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`) 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`. |
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`. |
507
512
 
508
513
  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.
509
514
 
package/bin/code-kit.mjs CHANGED
@@ -77,6 +77,13 @@ import {
77
77
  import { storyTimes } from '../hooks/lib/timeline.mjs';
78
78
  import { PROJECT, projectMap, storyMap, trail } from '../hooks/lib/storymap.mjs';
79
79
  import { journal } from '../hooks/lib/journal.mjs';
80
+ import {
81
+ INSTRUCTIONS_VERSION,
82
+ agentRules,
83
+ instructionsState,
84
+ leadSection,
85
+ writeInstructions,
86
+ } from '../hooks/lib/instructions.mjs';
80
87
  import { dequeue, enqueue, lastMerged, readQueue, settle, waiting } from '../hooks/lib/queue.mjs';
81
88
  import { SETTINGS, parseSetting, settingOf, withSetting } from '../hooks/lib/harness.mjs';
82
89
  import { claimLead, readLoop, releaseLead, writeLoop } from '../hooks/lib/loop-state.mjs';
@@ -1884,8 +1891,48 @@ function loopCommand() {
1884
1891
  );
1885
1892
  }
1886
1893
 
1894
+ /**
1895
+ * ONB-8: code-kit's own instructions (the lead's section of CLAUDE.md, each lane agent's "Always:" rules):
1896
+ * where each stands, `--write` to bring the marked ones up to date, `--show lead|lane` to print one.
1897
+ */
1898
+ function instructionsCommand() {
1899
+ const { config } = load();
1900
+ const root = resolve('.');
1901
+ if (rest[0] === 'show') {
1902
+ if (!['lead', 'lane'].includes(rest[1])) die('code-kit instructions show lead|lane');
1903
+ return out(rest[1] === 'lead' ? leadSection(root, config) : agentRules(root, config));
1904
+ }
1905
+ const changed = flag('--write') ? writeInstructions(root, config) : [];
1906
+ const files = instructionsState(root, config);
1907
+ if (flag('--json'))
1908
+ return out(
1909
+ JSON.stringify(
1910
+ {
1911
+ version: INSTRUCTIONS_VERSION,
1912
+ files: files.map(({ file, kind, state, version }) => ({ file, kind, state, version })),
1913
+ written: changed,
1914
+ },
1915
+ null,
1916
+ 2,
1917
+ ),
1918
+ );
1919
+ const said = {
1920
+ current: 'up to date',
1921
+ stale: 'from an older code-kit or config; --write brings it up to date',
1922
+ missing: 'none yet; --write adds it',
1923
+ legacy: 'written before code-kit marked its part; merge it once, by hand (below)',
1924
+ };
1925
+ if (changed.length) out(`Brought up to date: ${changed.join(', ')}.`);
1926
+ for (const f of files) out(` ${f.file}: ${said[f.state]}`);
1927
+ if (files.some((f) => f.state === 'legacy'))
1928
+ out(
1929
+ '\nTo merge one: replace its old code-kit part (in CLAUDE.md from `## Enforced, not advised` to the line about changing the rules; in an agent file from `Always:` to the end of its list) with what `code-kit instructions show lead` or `show lane` prints. Keep every line the project wrote itself (its own rules, its layer order, its docs) outside the markers, under a heading of its own, so a later refresh never touches them.',
1930
+ );
1931
+ }
1932
+
1887
1933
  const commands = {
1888
1934
  check,
1935
+ instructions: instructionsCommand,
1889
1936
  loop: loopCommand,
1890
1937
  approve: () => (rest.length ? approve(rest) : usage()),
1891
1938
  merge: () => (rest.length === 1 ? merge(rest[0]) : usage()),
@@ -1922,6 +1969,7 @@ function usage() {
1922
1969
  ' | verify [--base ref] [--branch name] [--no-checks] | status [--base ref] [--json]\n' +
1923
1970
  ' | security [--base ref] [--branch name] [--json]\n' +
1924
1971
  ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
1972
+ ' | instructions [--write] [--json] | instructions show lead|lane\n' +
1925
1973
  ' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
1926
1974
  ' | merge <branch> --delegated|--person [--into branch] | requests [--json] | requests drop <n>|all\n' +
1927
1975
  ' | ask "<question>" [--story ST-n] [--blocks] [--why "…"] [--option "Label: effect"]... | ask [--json]\n' +
@@ -58,6 +58,21 @@ const RUNS = [
58
58
  for (const [re, why] of RUNS) {
59
59
  if (runs(cmd, re, { quoted: false })) block(`Blocked: ${why}\nCommand: ${cmd}`);
60
60
  }
61
+ /**
62
+ * Whether `cmd` only reads git's history, with what it prints piped into read-only filters: `git diff
63
+ * main...HEAD -- .claude/approval-log.jsonl`, `git log -p -- … | head`. No redirect, no tee, nothing else.
64
+ */
65
+ function gitReadsOnly(cmd) {
66
+ const code = codeOf(cmd);
67
+ if (/>|\btee\b|`|\$\(/.test(code)) return false;
68
+ const parts = commandsOf(cmd);
69
+ return (
70
+ parts.length > 0 &&
71
+ new RegExp(String.raw`^${GIT}(?:diff|log|show|blame)\b`).test(parts[0].code) &&
72
+ parts.slice(1).every((c) => /^(?:grep|head|tail|wc|cat|sort|uniq|less)\b/.test(c.code))
73
+ );
74
+ }
75
+
61
76
  // These read the command whole: a pipe spans commands, and an approval or the log can be written
62
77
  // through a quoted path.
63
78
  const ALWAYS = [
@@ -73,8 +88,8 @@ const ALWAYS = [
73
88
  ],
74
89
  [
75
90
  /approval-log\.jsonl/,
76
- "The approval audit log is appended only by the commit hook; nobody edits it. A shell command may not name it, even to read it; read it with the Read tool, which can't write.",
77
- cmd,
91
+ "The approval audit log is appended only by the commit hook; nobody edits it. A shell command may not name it except to read its history with git (git diff, log or show, piped only into grep, head, tail or wc); read the file itself with the Read tool, which can't write.",
92
+ gitReadsOnly(cmd) ? '' : cmd,
78
93
  ],
79
94
  ];
80
95
  for (const [re, why, text] of ALWAYS) {
@@ -221,6 +236,14 @@ if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+settings\s+set\b`)))
221
236
  block(
222
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}`,
223
238
  );
239
+ // ONB-8: code-kit's instructions are the lead's to bring up to date, like the rest of the kit.
240
+ if (
241
+ runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+instructions\b.*\s--write\b`)) &&
242
+ actor.kind !== 'lead'
243
+ )
244
+ block(
245
+ `Blocked: code-kit's instructions in CLAUDE.md and the agent files are the lead's to update. Tell the lead they're behind, and carry on.\nCommand: ${cmd}`,
246
+ );
224
247
  if (queues && actor.kind !== 'lead')
225
248
  block(
226
249
  `Blocked: only the lead puts branches in the merge queue, after its review passes them. Finish your story and commit it on your branch; the lead reviews it.\nCommand: ${cmd}`,
@@ -75,6 +75,12 @@ export const SETTINGS = {
75
75
  about:
76
76
  "When a milestone finishes, compact the lead's context before the next: ask you, do it, or leave it",
77
77
  },
78
+ 'notify.on': {
79
+ default: true,
80
+ problem: (v) => (typeof v === 'boolean' ? null : 'must be true or false'),
81
+ about:
82
+ 'A system notification when something new needs you: an approval, a question, a stalled lane',
83
+ },
78
84
  'report.on': {
79
85
  default: false,
80
86
  problem: (v) => (typeof v === 'boolean' ? null : 'must be true or false'),
@@ -0,0 +1,126 @@
1
+ // ONB-8: the parts of a project's instructions that are code-kit's own: the lead's section of CLAUDE.md
2
+ // and the "Always:" rules of each lane agent. Each is rendered from code-kit's templates and the
3
+ // project's config, and kept between markers, so a newer code-kit can tell they're out of date and bring
4
+ // them up to date exactly, leaving everything the project wrote around them as it is.
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ /** The version of code-kit's instructions: raised whenever the templates' rules change. */
10
+ export const INSTRUCTIONS_VERSION = 4;
11
+
12
+ const TEMPLATES = fileURLToPath(new URL('../../templates/', import.meta.url));
13
+ const BEGIN = (v) => `<!-- code-kit:begin v${v} -->`;
14
+ const END = '<!-- code-kit:end -->';
15
+ const MARKED = /<!-- code-kit:begin v(\d+) -->\n[\s\S]*?<!-- code-kit:end -->/;
16
+ const CI_WORKFLOW = '.github/workflows/code-kit.yml';
17
+
18
+ /** `{{if key}}…{{end}}` kept where `facts[key]`, dropped otherwise; `{{key}}` replaced by `facts[key]`. */
19
+ function fill(text, facts) {
20
+ return text
21
+ .replace(/^([ \t]*- )?\{\{if ([\w.]+)\}\}([\s\S]*?)\{\{end\}\}\n?/gm, (_, item, key, body) =>
22
+ facts[key] ? `${item ?? ''}${body}\n` : '',
23
+ )
24
+ .replace(/\{\{([\w.]+)\}\}/g, (whole, key) => (key in facts ? String(facts[key]) : whole))
25
+ .replace(/\n{3,}/g, '\n\n');
26
+ }
27
+
28
+ /** What the templates need from the project: everything the config and the repository say. */
29
+ function factsOf(root, config) {
30
+ const delegated = new Set(config.approvals?.delegate?.protected ?? []);
31
+ const kept = [];
32
+ for (const p of config.protected ?? []) {
33
+ if (p.approval === 'kit' || delegated.has(p.approval)) continue;
34
+ if (!kept.some((k) => k.approval === p.approval)) kept.push(p);
35
+ }
36
+ return {
37
+ layers: (config.layers ?? []).map((l) => l.name).join(', ') || 'none set',
38
+ designGate: config.designGate ? '; designs approved before screens' : '',
39
+ personKeeps: kept.length
40
+ ? kept.map((p) => `\`${p.approval}\` (${p.why})`).join(', ')
41
+ : 'changes to the rules themselves',
42
+ 'approvals.lead': Boolean(config.approvals?.lead),
43
+ 'harness.report.on': Boolean(config.harness?.report?.on),
44
+ ci: existsSync(join(root, CI_WORKFLOW)),
45
+ 'docs.specs': config.docs?.specs ?? 'the specs',
46
+ 'docs.plan': config.docs?.plan ?? 'the plan',
47
+ 'docs.principles': config.docs?.principles ?? "the project's principles",
48
+ };
49
+ }
50
+
51
+ const marked = (body) => `${BEGIN(INSTRUCTIONS_VERSION)}\n${body.trim()}\n${END}`;
52
+
53
+ /** The lead's section of CLAUDE.md, between its markers. */
54
+ export function leadSection(root, config) {
55
+ return marked(fill(readFileSync(join(TEMPLATES, 'claude-md.md'), 'utf8'), factsOf(root, config)));
56
+ }
57
+
58
+ /** A lane agent's "Always:" rules, between their markers. */
59
+ export function agentRules(root, config) {
60
+ const template = readFileSync(join(TEMPLATES, 'agent.md'), 'utf8');
61
+ return marked(fill(template.slice(template.indexOf('Always:')), factsOf(root, config)));
62
+ }
63
+
64
+ /** Whether two renderings say the same, whatever their whitespace and list indentation. */
65
+ const same = (a, b) => a.replace(/\s+/g, ' ').trim() === b.replace(/\s+/g, ' ').trim();
66
+
67
+ const LEAD_LEGACY = /^## Enforced, not advised\s*$/;
68
+ const AGENT_LEGACY = /^Always:\s*$/;
69
+
70
+ /**
71
+ * Where a file's code-kit part stands: `current`; `stale` (marked, from an older code-kit or an older
72
+ * config); `legacy` (written before the markers, often tailored by hand, so merged once by the lead, not
73
+ * overwritten); or `missing` (none yet).
74
+ */
75
+ function stateOf(text, block, legacy) {
76
+ const m = MARKED.exec(text);
77
+ // A formatter that rewraps the project's markdown doesn't make code-kit's part out of date.
78
+ if (m) return { state: same(m[0], block) ? 'current' : 'stale', version: Number(m[1]) };
79
+ if (text.split('\n').some((l) => legacy.test(l))) return { state: 'legacy', version: 0 };
80
+ return { state: 'missing', version: null };
81
+ }
82
+
83
+ /**
84
+ * Each file holding code-kit's instructions: `{ file, kind, state, version, block, next }`, `next` being
85
+ * the file brought up to date (null for a legacy one, which the lead merges). The lead's CLAUDE.md, and
86
+ * the agent file of every lane whose agent file exists.
87
+ */
88
+ export function instructionsState(root, config) {
89
+ const lead = leadSection(root, config);
90
+ const rules = agentRules(root, config);
91
+ const files = [
92
+ { file: 'CLAUDE.md', kind: 'lead', block: lead, legacy: LEAD_LEGACY },
93
+ ...[...new Set(Object.values(config.lanes ?? {}).map((l) => l.agent))]
94
+ .map((agent) => ({
95
+ file: `.claude/agents/${agent}.md`,
96
+ kind: 'lane',
97
+ block: rules,
98
+ legacy: AGENT_LEGACY,
99
+ }))
100
+ .filter((f) => existsSync(join(root, f.file))),
101
+ ];
102
+ return files.map(({ file, kind, block, legacy }) => {
103
+ const path = join(root, file);
104
+ const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
105
+ const { state, version } = stateOf(text, block, legacy);
106
+ const next =
107
+ state === 'stale'
108
+ ? text.replace(MARKED, block)
109
+ : state === 'missing'
110
+ ? `${text.replace(/\s*$/, '')}${text.trim() ? '\n\n' : ''}${block}\n`
111
+ : null;
112
+ return { file, kind, state, version, block, next };
113
+ });
114
+ }
115
+
116
+ /** Brings every marked or missing part of code-kit's instructions up to date; the files it changed. */
117
+ export function writeInstructions(root, config) {
118
+ const changed = [];
119
+ for (const f of instructionsState(root, config)) {
120
+ if (!f.next) continue;
121
+ mkdirSync(dirname(join(root, f.file)), { recursive: true });
122
+ writeFileSync(join(root, f.file), f.next);
123
+ changed.push(f.file);
124
+ }
125
+ return changed;
126
+ }
@@ -10,6 +10,7 @@ import { pendingSetup } from './adapters/index.mjs';
10
10
  import { CONFIG_FILE, SETUP_VERSION } from './config.mjs';
11
11
  import { readQueue, waiting } from './queue.mjs';
12
12
  import { decidedBy, laneOf, openRequests } from './rules.mjs';
13
+ import { instructionsState } from './instructions.mjs';
13
14
  import { SECURITY_AGENT_FILE } from './security.mjs';
14
15
 
15
16
  export const DRAFT_FILE = '.claude/code-kit.draft.json';
@@ -253,6 +254,24 @@ export function nextStep(root, base) {
253
254
  attention.push(
254
255
  `${unbuildable.join(', ')} can't be dispatched until the plan's gaps are fixed.`,
255
256
  );
257
+ // ONB-8: code-kit's own instructions to the lead and the lanes, brought up to date before the next step:
258
+ // a newer code-kit's rules reach the agents only through them.
259
+ const instructions = instructionsState(root, config);
260
+ const behind = instructions.filter((f) => ['stale', 'legacy'].includes(f.state));
261
+ // A file with no code-kit part may be one the project keeps without it: said, not insisted on.
262
+ const none = instructions.filter((f) => f.state === 'missing');
263
+ if (none.length)
264
+ attention.push(
265
+ `${none.map((f) => f.file).join(', ')} ${none.length === 1 ? 'has' : 'have'} none of code-kit's instructions; \`code-kit instructions --write\` adds them.`,
266
+ );
267
+ if (behind.length) {
268
+ const legacy = behind.filter((f) => f.state === 'legacy');
269
+ return result(
270
+ 'instructions',
271
+ `code-kit's instructions in ${behind.map((f) => f.file).join(', ')} are behind this code-kit${legacy.length ? `; ${legacy.length === behind.length ? 'they were' : `${legacy.map((f) => f.file).join(', ')} were`} written before code-kit marked its part, so merge ${legacy.length === 1 ? 'it' : 'them'} once by hand, keeping the project's own lines` : ''}.`,
272
+ { args: legacy.length ? 'merge' : 'write' },
273
+ );
274
+ }
256
275
  // HELM-4: a lane waiting on something the lead decides goes first: the lane can't go on without it.
257
276
  const forLeadToDecide = openRequests(root).flatMap((r, i) =>
258
277
  r.lane && decidedBy(r.names, config) === 'lead' ? [{ ...r, n: i + 1 }] : [],
@@ -205,8 +205,9 @@ export function approvalsGiven(root) {
205
205
  export function realPath(path) {
206
206
  let dir = resolve(path);
207
207
  const rest = [];
208
+ // basename, not a slice after dirname: dirname('/x') is '/', so slicing past it cut the name's first letter.
208
209
  while (!existsSync(dir)) {
209
- rest.unshift(dir.slice(dirname(dir).length + 1));
210
+ rest.unshift(basename(dir));
210
211
  dir = dirname(dir);
211
212
  }
212
213
  return join(realpathSync(dir), ...rest);
@@ -775,9 +776,14 @@ export function writeProblem(actor, rel, root, config) {
775
776
  const readonlyProblem = (actor, rel) =>
776
777
  `${actor.label} is read-only; it may not write ${rel}. If a command you ran changed it (a build, a formatter or a code generator), put it back with git restore -- ${rel} (or delete it if it's new), and run such commands only in a throwaway worktree of the branch.`;
777
778
 
779
+ // Claude Code's own temporary folders (a session's scratchpad, its tool output): no repository's files.
780
+ const CLAUDE_TEMP = ['/private/tmp/claude-*/**', '/tmp/claude-*/**'];
781
+
778
782
  function outsideAllowed(actor, rel, root, config) {
779
- if (actor.kind !== 'lead') return false;
780
783
  const target = resolve(root, rel);
784
+ // A lane's scratch files go where Claude Code's own do, outside every repository.
785
+ if (actor.kind === 'lane') return CLAUDE_TEMP.some((g) => globToRegExp(g).test(target));
786
+ if (actor.kind !== 'lead') return false;
781
787
  return config.lead.outside.some((g) =>
782
788
  globToRegExp(resolve(root, g.replace(/^~(?=\/)/, homedir()))).test(target),
783
789
  );
@@ -437,3 +437,32 @@ export function nativeChoice(card) {
437
437
  card.kind === 'question' ? 'answerTyped' : card.kind === 'waiting' ? 'answerLeadTyped' : null,
438
438
  };
439
439
  }
440
+
441
+ /** NOTIFY-1: a decision's identity across refreshes: its kind and what it asks. */
442
+ export const decisionKey = (card) => `${card.kind}|${card.headline}`;
443
+
444
+ /**
445
+ * NOTIFY-1: the decisions that weren't waiting at the last look, each as a system notification: what it
446
+ * asks, headed by what kind of decision it is. `seen` holds the keys from the last look.
447
+ */
448
+ export function newDecisions(cards, seen) {
449
+ return cards
450
+ .filter((c) => NOTIFY_TITLE[c.kind] && !seen.has(decisionKey(c)))
451
+ .map((c) => ({
452
+ key: decisionKey(c),
453
+ title: `code-kit: ${NOTIFY_TITLE[c.kind] ?? 'a decision for you'}`,
454
+ text: `${c.headline}${c.story ? ` (${c.story.id})` : ''}`,
455
+ }));
456
+ }
457
+
458
+ const NOTIFY_TITLE = {
459
+ held: 'an approval to give',
460
+ approvals: 'an approval to give',
461
+ question: 'the lead asks you',
462
+ brief: 'a question in your brief',
463
+ waiting: 'the lead waits on you',
464
+ stalled: 'a lane stalled',
465
+ denied: 'Claude Code refused a call',
466
+ proposal: 'a step to approve',
467
+ queue: 'a merge for you',
468
+ };
@@ -4,6 +4,7 @@
4
4
 
5
5
  /** The steps the loop hands the lead; the rest (spec-design, init, check) wait for the person. */
6
6
  const LEAD_STEPS = new Set([
7
+ 'instructions',
7
8
  'decide',
8
9
  'dispatch',
9
10
  'review',
@@ -96,13 +97,16 @@ export function parkWatch(id, on = [], stories = []) {
96
97
  }
97
98
 
98
99
  /**
99
- * LOOP-13: where the watched stories stand, as one string: each one's state, its branch's commit and
100
- * whether an agent is at work on it. A parked story wakes when this changes.
100
+ * LOOP-13: where the watched stories stand, as one string. The parked story itself (the first) counts
101
+ * its state, its branch's commit and whether an agent is at work on it; each story it waits on counts its
102
+ * state alone, so the commits of a story still being built don't wake the one waiting for it to finish.
103
+ * A parked story wakes when this changes.
101
104
  */
102
105
  export function storiesSignature(ids, stories = [], heads = new Map(), inFlight = new Set()) {
103
106
  return ids
104
- .map((id) => {
107
+ .map((id, i) => {
105
108
  const s = stories.find((x) => x.id === id);
109
+ if (i > 0) return `${id}:${s?.state ?? '?'}`;
106
110
  return `${id}:${s?.state ?? '?'}:${s?.branch ? (heads.get(s.branch) ?? '') : ''}:${inFlight.has(id) ? 'busy' : ''}`;
107
111
  })
108
112
  .join('|');
@@ -134,6 +138,14 @@ export function leadPrompt(step, { cli = 'code-kit' } = {}) {
134
138
  // SETUP-2: the base has no rules yet; the lead puts the approved ones there, then merges as usual.
135
139
  if (step.step === 'rules')
136
140
  return `Put code-kit's approved rules onto ${ids[0] ?? 'the base'}: run node "${cli}" queue rules. It commits the rules as committed on this branch, without switching branches; after it, merge the queue as usual.`;
141
+ // ONB-8: code-kit's instructions brought up to date, then committed like any change to the kit.
142
+ if (step.step === 'instructions') {
143
+ const how =
144
+ step.args === 'merge'
145
+ ? `Run node "${cli}" instructions to see which files, then merge each one: replace its old code-kit part with what node "${cli}" instructions show lead (CLAUDE.md) or show lane (an agent file) prints, and keep every line the project wrote itself outside the markers, under a heading of its own. Then run node "${cli}" instructions --write for the rest.`
146
+ : `Run node "${cli}" instructions --write.`;
147
+ return `${step.why ?? "code-kit's instructions are behind."} ${how} Commit the change on a branch and merge it like your own stories, so new lane agents start with the current rules.`;
148
+ }
137
149
  // HELM-4: a lane waits on the lead's decision; the decide skill takes each request on its merits.
138
150
  if (step.step === 'decide')
139
151
  return ids.length
@@ -175,6 +187,7 @@ export function stepLabel(step) {
175
187
  const verb =
176
188
  {
177
189
  decide: 'Decide requests',
190
+ instructions: 'Update the instructions',
178
191
  dispatch: 'Dispatch',
179
192
  review: 'Review',
180
193
  lead: 'Build',
@@ -120,6 +120,8 @@ export function laneActions(lane, { queue = [], held = [], requests = [] } = {})
120
120
  const head = queue.find((e) => ['waiting', 'merging'].includes(e.state));
121
121
  if (head && head.branch === lane.branch) acts.push({ id: 'merge', label: 'Merge', hotkey: 'm' });
122
122
  if (lane.active && lane.agent) {
123
+ // WATCH-1: the lane agent's own transcript, in the main view.
124
+ acts.push({ id: 'watch', label: 'Watch', hotkey: 'w' });
123
125
  acts.push({ id: 'nudge', label: 'Nudge', hotkey: 'n' });
124
126
  acts.push({ id: 'stop', label: 'Stop', hotkey: 'x' });
125
127
  }
@@ -74,7 +74,7 @@ import {
74
74
  withoutInFlight,
75
75
  } from './loop.mjs';
76
76
  import { goalOf, nextText } from './mission.mjs';
77
- import { contextOf, decisionCards, nativeChoice } from './decisions.mjs';
77
+ import { contextOf, decisionCards, decisionKey, nativeChoice, newDecisions } from './decisions.mjs';
78
78
  import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
79
79
  import { glowOf, mapStoryOf, touchOf } from './codemap.mjs';
80
80
  import { harnessSettings } from '../lib/harness.mjs';
@@ -144,6 +144,8 @@ let turnQuestions = []; // the open questions as the lead's running turn started
144
144
  // DECIDE-4: decisions asked in Claude Code's own question dialog this session, and whether one is open.
145
145
  const offered = new Set();
146
146
  let offering = false;
147
+ // NOTIFY-1: the decisions waiting at the last look, by key; null until the first look.
148
+ let notified = null;
147
149
  /** LOOP-8: the times in a row the lead is prompted with one step before the person is asked. */
148
150
  const ASK_LIMIT = 2;
149
151
  let leadTurn = null; // the lead's running turn, if any
@@ -1063,10 +1065,19 @@ export function register(on) {
1063
1065
  ) => {
1064
1066
  loop.started = true;
1065
1067
  loop.proposal = null;
1066
- loop.sent = { prompt, key, label, kind, target };
1068
+ const sent = { prompt, key, label, kind, target, before: new Map() };
1069
+ loop.sent = sent;
1067
1070
  // LOOP-12: a design story is handed to the lead once; the person designs it with the lead.
1068
1071
  if (kind === 'design')
1069
1072
  for (const id of String(target).split(/\s+/).filter(Boolean)) loop.handedDesign.add(id);
1073
+ // LOOP-13: where the step's stories stood when the lead was prompted, to tell after its turn
1074
+ // whether it moved them.
1075
+ // A design is made with the person over many turns (LOOP-12), so it's never counted as unmoved.
1076
+ const ids = kind === 'design' ? [] : stepStories({ kind, target });
1077
+ if (ids.length) {
1078
+ const seen = await act.storiesNow();
1079
+ for (const id of ids) sent.before.set(id, seen.sig([id]));
1080
+ }
1070
1081
  await act.record(kind, target, prompt);
1071
1082
  $.ui.invalidate('ui.render');
1072
1083
  $.prompt.submit({ text: prompt }).catch((err) => {
@@ -1141,6 +1152,16 @@ export function register(on) {
1141
1152
  }
1142
1153
  $.ui.invalidate('ui.render');
1143
1154
  },
1155
+ // LOOP-13: the stories of the step the lead was prompted with that its turn left where they stood.
1156
+ parkUnmoved: async (sent, reason) => {
1157
+ if (!sent?.before?.size) return;
1158
+ const seen = await act.storiesNow();
1159
+ await act.park(
1160
+ [...sent.before]
1161
+ .filter(([id, sig]) => !loop.parked.has(id) && seen.sig([id]) === sig)
1162
+ .map(([id]) => ({ id, on: [], why: reason ?? "the lead's turn didn't move it" })),
1163
+ );
1164
+ },
1144
1165
  // LOOP-13: a parked story wakes when anything it watches moves, or after PARK_MS with nothing moving.
1145
1166
  unpark: async () => {
1146
1167
  if (!loop.parked.size) return;
@@ -1236,7 +1257,7 @@ export function register(on) {
1236
1257
  $.ui.invalidate('ui.render');
1237
1258
  return;
1238
1259
  }
1239
- if (!reply.text.trim()) return;
1260
+ if (!reply.text.trim()) return act.parkUnmoved(sent, null);
1240
1261
  const said = await act.sideCall(
1241
1262
  outcomePrompt({ prompt: sent?.prompt ?? null, answer: reply.text }),
1242
1263
  );
@@ -1259,6 +1280,10 @@ export function register(on) {
1259
1280
  })),
1260
1281
  );
1261
1282
  }
1283
+ // LOOP-13: a story the lead's turn left exactly where it stood wasn't taken, whatever the reply
1284
+ // reads as; it's parked like one the lead said waits, so the loop doesn't prompt it again until
1285
+ // it moves.
1286
+ if (heard?.outcome !== 'needs-person') await act.parkUnmoved(sent, heard?.reason);
1262
1287
  if (heard?.outcome === 'needs-person')
1263
1288
  loop.waiting = {
1264
1289
  kind: 'person',
@@ -1310,6 +1335,28 @@ export function register(on) {
1310
1335
  // DECIDE-4: the next decision the person settles with a choice, asked in Claude Code's own question
1311
1336
  // dialog while the lead is idle. Each is asked once: dismissed, it stays in the band and the
1312
1337
  // Decisions tab. Never in a session another one runs the loop for, nor over what the person types.
1338
+ // NOTIFY-1: what newly needs the person, as a system notification, from the session that runs the
1339
+ // loop. What waited before this session looked isn't news. On a Claude Code without $.ui.notify,
1340
+ // the call fails and nothing is raised.
1341
+ notifyNew: async () => {
1342
+ if (model.state?.kind !== 'ok' || !where.interactive) return;
1343
+ const cards = cardsNow(await $.clock.now());
1344
+ const fresh = newDecisions(cards, notified ?? new Set());
1345
+ const first = notified === null;
1346
+ notified = new Set(cards.map(decisionKey));
1347
+ if (first || !fresh.length || harness().notify?.on === false) return;
1348
+ if (lease.other && !lease.mine) return;
1349
+ const one =
1350
+ fresh.length === 1
1351
+ ? fresh[0]
1352
+ : {
1353
+ title: `code-kit: ${fresh.length} things need you`,
1354
+ text: fresh.map((f) => f.text).join('; '),
1355
+ };
1356
+ await Promise.resolve()
1357
+ .then(() => $.ui.notify(one.text, { title: one.title }))
1358
+ .catch(() => {});
1359
+ },
1313
1360
  offer: async () => {
1314
1361
  if (offering || leadTurn || loop.sent || !where.interactive) return;
1315
1362
  if (lease.other && !lease.mine) return;
@@ -1631,6 +1678,24 @@ export function register(on) {
1631
1678
  ((await $.agent.list().catch(() => [])) ?? []).find(
1632
1679
  (a) => a.status === 'running' && a.type === lane.agent,
1633
1680
  ) ?? null,
1681
+ // WATCH-1: the person's press puts the lane agent's transcript in the main view, as the tasks list
1682
+ // does; the tasks list (or Escape there) brings the main conversation back.
1683
+ watchLane: async (lane) => {
1684
+ const agent = await act.runningAgentOf(lane);
1685
+ if (!agent) {
1686
+ notice = `No agent is at work in the ${lane.name} lane to watch.`;
1687
+ $.ui.invalidate('ui.render');
1688
+ return;
1689
+ }
1690
+ const r = await Promise.resolve()
1691
+ .then(() => $.ui.view({ agentId: agent.id }))
1692
+ .catch(() => ({
1693
+ deny: 'this Claude Code shows an agent only from its tasks list (2.1.301 or later shows it from here)',
1694
+ }));
1695
+ notice = r?.deny ? `Couldn't show the ${lane.name} lane's agent: ${r.deny}.` : null;
1696
+ if (!r?.deny) await act.record('watch', `${lane.name} · ${agent.type}`);
1697
+ $.ui.invalidate('ui.render');
1698
+ },
1634
1699
  nudgeLane: async (lane) => {
1635
1700
  const agent = await act.runningAgentOf(lane);
1636
1701
  if (!agent) return;
@@ -1737,6 +1802,7 @@ export function register(on) {
1737
1802
  return act.review({ story: { ...lane.story, branch: lane.branch } });
1738
1803
  if (id === 'merge') return act.mergeQueue({ branch: lane.branch });
1739
1804
  if (id === 'sendBack') return act.sendBack(lane);
1805
+ if (id === 'watch') return act.watchLane(lane);
1740
1806
  if (id === 'nudge') return act.nudgeLane(lane);
1741
1807
  if (id === 'stop') return act.stopLane(lane);
1742
1808
  },
@@ -1758,6 +1824,7 @@ export function register(on) {
1758
1824
  // LOOP-10: the lead session renews its lease; DECIDE-4: with the lead idle, a decision waiting.
1759
1825
  if (lease.mine) await act.lead();
1760
1826
  if (!leadTurn) act.offer().catch(() => {});
1827
+ await act.notifyNew().catch(() => {});
1761
1828
  await act.stalls();
1762
1829
  await act.animate();
1763
1830
  const now = await act.fingerprint();
@@ -75,6 +75,8 @@ function project() {
75
75
  agents: [] as { type: string; status: string; id?: string; description?: string }[],
76
76
  limits: [] as { kind: string; percentUsed: number }[],
77
77
  contextPercent: 0, // LEAN-3: how full the lead's context is
78
+ notified: [] as { text: string; title?: string }[], // NOTIFY-1
79
+ viewed: [] as (string | null)[], // WATCH-1
78
80
  approveExit: 0,
79
81
  mergeExit: 0,
80
82
  answer: 'Merge',
@@ -408,6 +410,15 @@ function stub(on: any, w: World) {
408
410
  value: { startedAt: 0, context: { percent: w.contextPercent }, rateLimits: w.limits },
409
411
  }));
410
412
  on('session.measure', ($: any, e: any) => ({ changed: e.changed }));
413
+ // NOTIFY-1: the person's notification channel; WATCH-1: the main view.
414
+ on('ui.notify', ($: any, e: any) => {
415
+ w.notified.push({ text: e.text, title: e.title });
416
+ return { value: { isSent: true, channel: 'iterm2' } };
417
+ });
418
+ on('ui.view', ($: any, e: any) => {
419
+ w.viewed.push(e.agentId ?? null);
420
+ return { value: {} };
421
+ });
411
422
  // A model request, answered with the usage the test gave it.
412
423
  on('turn.step', async function* ($: any, e: any) {
413
424
  return {
@@ -2134,7 +2145,9 @@ test('LOOP-1 nothing is submitted mid-turn, or while the person has a draft, or
2134
2145
  expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
2135
2146
  await leadTurn($, 't-3');
2136
2147
  await settle(clock);
2137
- expect(w.prompts).toHaveLength(1);
2148
+ // LOOP-13: the turn left ST-9 where it stood, so it's parked, and the next step goes instead.
2149
+ expect(w.prompts.filter((p: string) => p.startsWith('Dispatch ST-9'))).toHaveLength(1);
2150
+ expect(w.prompts.at(-1)).toMatch(/^Build ST-6, ST-7 yourself/);
2138
2151
  });
2139
2152
 
2140
2153
  test("LOOP-1 review and the lead's own stories are prompted with their skill", async ($, on) => {
@@ -2566,6 +2579,39 @@ test("LEAN-3 when a milestone finishes, the person is asked to compact the lead'
2566
2579
  expect(compacted).toHaveLength(1);
2567
2580
  });
2568
2581
 
2582
+ test("LOOP-13 a review the lead's turn didn't move isn't prompted again while other lanes commit, whatever the reply reads as", async ($, on) => {
2583
+ const w = project();
2584
+ w.stories.push({
2585
+ id: 'ST-160',
2586
+ title: 'Content security policy',
2587
+ lane: 'web',
2588
+ state: 'in review',
2589
+ branch: 'web/st-160',
2590
+ waitingOn: [],
2591
+ });
2592
+ w.refs += ' p1 refs/heads/web/st-160\n';
2593
+ w.next = { step: 'review', args: 'ST-160', then: [] };
2594
+ w.sideReply = unread;
2595
+ const clock = await start($, on, w);
2596
+ await leadTurn($);
2597
+ await settle(clock);
2598
+ expect(w.prompts).toEqual(['Review ST-160: run /code-kit:review ST-160.']);
2599
+ // The lead says it waits, the reading comes back done, and lanes keep committing.
2600
+ w.sideReply = outcome({ outcome: 'done' });
2601
+ for (const [i, turn] of ['t-2', 't-3', 't-4', 't-5'].entries()) {
2602
+ w.refs += ` x${i} refs/heads/core/st-${i}\n`;
2603
+ await leadSays($, turn, 'ST-160 waits on ST-165; nothing to review yet.');
2604
+ await settle(clock);
2605
+ w.sideReply = unread;
2606
+ }
2607
+ expect(w.prompts).toHaveLength(1);
2608
+ // Its own branch moves: it's prompted again.
2609
+ w.refs = w.refs.replace(' p1 refs/heads/web/st-160', ' p2 refs/heads/web/st-160');
2610
+ await leadTurn($, 't-6');
2611
+ await settle(clock);
2612
+ expect(w.prompts).toHaveLength(2);
2613
+ });
2614
+
2569
2615
  test('LOOP-13 stories the lead says wait on other work are parked, each until what it waits on moves', async ($, on) => {
2570
2616
  const w = project();
2571
2617
  w.stories.push(
@@ -2635,10 +2681,17 @@ test('LOOP-13 stories the lead says wait on other work are parked, each until wh
2635
2681
  await leadTurn($, 't-3');
2636
2682
  await settle(clock);
2637
2683
  expect(w.prompts).toHaveLength(asked);
2638
- // The story building on ST-158 commits: ST-158 wakes, and ST-172 still waits.
2684
+ // The story building on ST-158 commits: that alone doesn't wake it.
2639
2685
  w.refs = w.refs.replace(' s2 refs/heads/api/st-164', ' s9 refs/heads/api/st-164');
2640
2686
  await leadTurn($, 't-4');
2641
2687
  await settle(clock);
2688
+ expect(w.prompts).toHaveLength(asked);
2689
+ // It finishes: ST-158 wakes, and ST-172 still waits.
2690
+ w.stories = w.stories.map((s: any) => (s.id === 'ST-164' ? { ...s, state: 'in review' } : s));
2691
+ w.refs += ' s10 refs/heads/api/st-164-done\n';
2692
+ await clock.advance(2000);
2693
+ await leadTurn($, 't-5');
2694
+ await settle(clock);
2642
2695
  expect(w.prompts.at(-1)).toBe('Review ST-158: run /code-kit:review ST-158.');
2643
2696
  expect(w.prompts).toHaveLength(asked + 1);
2644
2697
  });
@@ -3351,6 +3404,10 @@ test('VIEW-6 tabs on 1 to 7, j and k move between lanes, and the selected lane h
3351
3404
  await press($, 'lane-next', PANE);
3352
3405
  await press($, 'lane-next', PANE);
3353
3406
  expect((await ui.find({ key: 'lane-web' }))?.text).toMatch(/^›/);
3407
+ // WATCH-1: w puts the lane agent's transcript in the main view.
3408
+ expect((await ui.find({ key: 'act-watch' }))?.props.hotkey).toBe('w');
3409
+ await press($, 'act-watch', PANE);
3410
+ expect(w.viewed).toEqual(['a1']);
3354
3411
  expect((await ui.find({ key: 'act-nudge' }))?.props.hotkey).toBe('n');
3355
3412
  await press($, 'act-nudge', PANE);
3356
3413
  expect(w.sent).toEqual([
@@ -3366,6 +3423,30 @@ test('VIEW-6 tabs on 1 to 7, j and k move between lanes, and the selected lane h
3366
3423
  await ui.unmount();
3367
3424
  });
3368
3425
 
3426
+ test('NOTIFY-1 a decision new since the last look raises one notification; what waited before, and notify.on false, raise none', async ($, on) => {
3427
+ const w = project();
3428
+ w.requests = [dayjs];
3429
+ const clock = await start($, on, w);
3430
+ await clock.advance(2000);
3431
+ // The approval waiting when the session first looked isn't news.
3432
+ expect(w.notified).toEqual([]);
3433
+ w.questions = [question()];
3434
+ w.refs += ' q1 refs/heads/web/st-4-q\n';
3435
+ await clock.advance(2000);
3436
+ await clock.advance(2000);
3437
+ expect(w.notified).toHaveLength(1);
3438
+ expect(w.notified[0].title).toBe('code-kit: the lead asks you');
3439
+ await clock.advance(2000);
3440
+ expect(w.notified).toHaveLength(1);
3441
+ // Turned off, a new decision raises nothing.
3442
+ (w.check as any).harness = { notify: { on: false } };
3443
+ w.questions = [question(), question({ id: 'Q-2', question: 'Which payment provider?' })];
3444
+ w.refs += ' q2 refs/heads/web/st-4-q2\n';
3445
+ await clock.advance(2000);
3446
+ await clock.advance(2000);
3447
+ expect(w.notified).toHaveLength(1);
3448
+ });
3449
+
3369
3450
  test('VIEW-6 s sends the selected story back, with the reason the person gives', async ($, on) => {
3370
3451
  const w = project();
3371
3452
  w.stories[0] = { ...w.stories[0], state: 'review' };
@@ -19,6 +19,9 @@ const { input, project, config, error } = start();
19
19
  // reported MAX_REPEATS times in a row means no progress: say so and let the finish through rather than
20
20
  // loop. A different problem means progress, and counts from one again.
21
21
  const MAX_REPEATS = 3;
22
+ // LEAN-1: the most a lane's hand-back holds.
23
+ const HAND_BACK_LINES = 10;
24
+ const HAND_BACK_CHARS = 1200;
22
25
  const memory = join(
23
26
  project,
24
27
  '.claude/state/stop-blocks',
@@ -97,9 +100,17 @@ if (actor.kind === 'lane' && changed.length) {
97
100
  // LEAN-1: the lead's context gets a short hand-back; the detail waits in a file it reads when it reviews.
98
101
  if (actor.kind === 'lane' && storyOfBranch(currentBranch(root))) {
99
102
  const report = reportPath(root);
100
- if (!existsSync(join(root, report)))
103
+ // Written in the lane's worktree, or by its absolute path in the main checkout: either is found.
104
+ if (!existsSync(join(root, report)) && !existsSync(join(project, report)))
101
105
  blockCounted(
102
106
  `Write your full report to ${report} before you finish: 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 have pasted, such as a migration you can't write yet). Then finish with a short hand-back of at most ten lines: the story and branch, done or blocked, any decision the lead must make, and that report's path. Don't paste output or code into the hand-back; the lead reads the file when it reviews.`,
103
107
  );
108
+ // The hand-back is what reaches the lead's context: a few lines, the detail left in the report.
109
+ const handBack = String(input.last_assistant_message ?? '').trim();
110
+ const lines = handBack.split('\n').filter((l) => l.trim()).length;
111
+ if (lines > HAND_BACK_LINES || handBack.length > HAND_BACK_CHARS)
112
+ blockCounted(
113
+ `Your hand-back is ${lines} lines and ${handBack.length} characters, and all of it goes into the lead's context. Finish again with at most ${HAND_BACK_LINES} short lines: the story and branch, done or blocked, any decision the lead must make, and ${report}. Don't restate the report: the lead reads it when it reviews.`,
114
+ );
104
115
  }
105
116
  passed();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",
@@ -102,7 +102,8 @@ Draft, alongside the config:
102
102
  - its layers and what they may depend on;
103
103
  - the specs it reads;
104
104
  - the command that proves its work;
105
- - its lane rules.
105
+ - its lane rules: the rules the docs set for this lane, and anything the project adds to code-kit's own (its principles and architecture docs, its own words for what's banned). These are the project's, so they stay outside code-kit's part.
106
+ - Leave out the template's `Always:` list: it's code-kit's part, which `code-kit instructions --write` adds once the config is written, between its markers.
106
107
  - If an agent already exists, keep its content and add only what's missing.
107
108
  - Drop the spec-check lines if the config has no `docs.specs`.
108
109
  - **A security reviewer:** write `.claude/agents/security-reviewer.md` from `${CLAUDE_PLUGIN_ROOT}/templates/security-reviewer.md`, in the same approval as the lane agents, whenever the product handles sign-in, personal data, payments, input from outside (forms, APIs, webhooks, uploads) or secrets. That's almost every product; leave it out only for one with none of these, such as a static site or a local tool with no accounts, and say why. It is a reviewer, not a lane: no entry in `lanes`, no paths, and only read-only tools (Read, Grep, Glob, and Bash for commands that change nothing), so the hooks treat it as read-only too. Fill it in from the docs (or the code, for brownfield):
@@ -111,7 +112,7 @@ Draft, alongside the config:
111
112
  - the privacy and data residency targets in the principles;
112
113
  - the project's audit commands (for example `npm audit --omit=dev`, `pip-audit`), each run once now; leave out one that isn't installed, and say so.
113
114
  - If the agent already exists, keep its content and add only what's missing.
114
- - **CLAUDE.md:** add the section from `${CLAUDE_PLUGIN_ROOT}/templates/claude-md.md`.
115
+ - **CLAUDE.md:** code-kit's section is rendered from the config, not written by hand: once the config is written, `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" instructions --write` adds it between its markers (`<!-- code-kit:begin vN -->` … `<!-- code-kit:end -->`). Anything the project wants to say beyond it (its layer order in words, its docs, its own rules) goes in a section of its own, outside the markers, so a later code-kit never touches it.
115
116
  - **Project skills, with the agents** (recipes like "new screen", "new migration" or "new endpoint"): one per recipe in the architecture's `## Recipes`, or any other doc that describes one step by step. Draft them now, alongside the agents, and show them in the same approval; the agents and their skills are one piece of work. Brownfield code without recipes in its docs: propose recipes from the patterns the code already follows (how its existing screens, endpoints or migrations are built), as drafts the person approves. Never write an empty one.
116
117
  - Write each as `.claude/skills/<name>/SKILL.md`, saying in its description which recipe in which doc it comes from, so an update can find it again.
117
118
  - List it under **Skills to use** on every lane agent whose paths the recipe touches, not only one: a recipe that adds a migration and the endpoint that uses it belongs to both lanes. A recipe only the lead follows goes in the CLAUDE.md section instead.
@@ -154,6 +155,7 @@ Re-run whenever the build has moved on: new apps or packages, folders that moved
154
155
 
155
156
  1. **Re-read** the docs and the repository as in section 1, including the brownfield steps if the code has grown past the docs.
156
157
  2. **Draft** the whole config again into `.claude/code-kit.draft.json`, starting from the live one. Keep everything that's still right, so the diff shows only real changes. If the live config has no `harness.autonomy`, ask the person about it as in section 2.
158
+ Bring code-kit's own instructions up to date: run `code-kit instructions`. Parts between markers are rewritten with `--write`. A part written before the markers (CLAUDE.md's `## Enforced, not advised` section, an agent's `Always:` list) is merged once: replace it with what `code-kit instructions show lead` or `show lane` prints, and move every line the project wrote itself into a section of its own outside the markers. Show the person the difference with the rest.
157
159
  A config without `"setup": 3` predates what setup now includes; add whichever of these the project is missing, as section 2 and 3 describe, and set `"setup": 3`:
158
160
  - project skills for the recipes in the docs (or, without recipes, the patterns the code follows), listed on the lane agents;
159
161
  - the design gate for every lane that owns screens, with its register; and design stories in the plan for screens not yet built (ask the person before adding stories);
@@ -26,6 +26,7 @@ It returns `step`, `why`, `args`, `then` (steps that can follow straight after)
26
26
  | Config, but no specs, plan or stories | `spec-design` |
27
27
  | A companion the config wants, not set up yet | `setup`: run that tool's init skill (`args`), before code |
28
28
  | An older setup missing what init now adds, nothing in flight | `update`: re-run init, before any new story |
29
+ | code-kit's instructions behind this code-kit | `instructions`: bring CLAUDE.md and the agents up to date |
29
30
  | A lane waits on a request you decide | `decide`: approve it, or answer with an alternative |
30
31
  | Stories finished on their branches | `review`, then `dispatch` what's ready |
31
32
  | Design stories ready (`**Lane:** design`) | `design`, then `dispatch` what needs no design |
@@ -43,6 +44,7 @@ Run the named skill with the Skill tool: `code-kit:<step>`, with `args` (and the
43
44
 
44
45
  - **`setup`:** run the skill named in `args` (for example `context-graph:init`); it's another plugin's, so call it by that full name.
45
46
  - **`update`:** run `code-kit:init`. `why` names what the setup is missing (the design gate, design stories, a QA lane, a security reviewer); init adds those, shows the person the difference, and sets `"setup": 3`. Until then no new story starts; work already started goes on to review and merge.
47
+ - **`instructions`:** there's no skill to run. With `args` `write`, run `code-kit instructions --write`. With `merge`, some files were written before code-kit marked its part: run `code-kit instructions` to list them, replace each old part with what `code-kit instructions show lead` (CLAUDE.md) or `show lane` (an agent file) prints, keep every line the project wrote itself outside the markers, then run `--write` for the rest. Commit it on a branch and merge it like your own stories.
46
48
  - **`lead`:** there's no skill to run. Build each story yourself, on the branch `lead/st-<n>`: spec-check it first if it cites requirements, do the work, commit, then run `code-kit:review` on the branch like any other. A spike's result goes in the docs it names.
47
49
  - **`wait`:** say which stories are being built and on which branches, then stop. When a lane agent finishes, its notification arrives in this session; run this skill again then. If a story has been in progress with no agent running (the session ended, or the agent stopped without committing), dispatch it again.
48
50
  - **`done`:** give the `status` summary in a few lines, then offer to run spec-design for the next milestone or feature.
@@ -18,7 +18,7 @@ Lane rules:
18
18
  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
- - 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.
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
22
  - Read every file you change in full, plus its imports and its callers.
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.
@@ -3,19 +3,20 @@
3
3
  The code-kit plugin enforces the rules in `.claude/code-kit.json` on every file edit, shell command and finish:
4
4
 
5
5
  - who writes where (the lead delegates lane work to the lane agents in `.claude/agents/`);
6
- - the layer rules ({{layer names, in order}});
7
- - spec-check before lane code{{; designs approved before screens}};
6
+ - the layer rules ({{layers}});
7
+ - spec-check before lane code{{designGate}};
8
8
  - an audit log of every change to protected paths (`.claude/approval-log.jsonl`), which the person reviews in the pull request;
9
- - an approval for every new dependency and every protected change, per lane. Inside the guardrails, you decide: a lane asks you, never the person, and you approve it only when the story's requirements need it and it fits the spec, the principles and the architecture, or you tell the lane what to use instead (`/code-kit:decide`). The person decides what crosses a guardrail: {{the approvals the person keeps: those not in approvals.delegate, e.g. "spec changes, contracts, the screens' designs, CI"}}, weakened tests, and product questions the spec doesn't answer;
10
- {{If approvals.lead is on: - approvals from chat. When the person approves in their own message (a `!` command doesn't work on a phone), record it with the `code-kit approve` command from the refusal, with their words as the reason. Never record an approval they haven't given, and never on a lane's say-so;}}
9
+ - an approval for every new dependency and every protected change, per lane. Inside the guardrails, you decide: a lane asks you, never the person, and you approve it only when the story's requirements need it and it fits the spec, the principles and the architecture, or you tell the lane what to use instead (`/code-kit:decide`). The person decides what crosses a guardrail: {{personKeeps}}, weakened tests, and product questions the spec doesn't answer;
10
+ - {{if approvals.lead}}approvals from chat. When the person approves in their own message (a `!` command doesn't work on a phone), record it with the `code-kit approve` command from the refusal, with their words as the reason. Never record an approval they haven't given, and never on a lane's say-so;{{end}}
11
11
  - the project's checks before any agent finishes.
12
12
 
13
13
  UX is yours: what a screen does on an error, an empty list or a slow load, and the wording the spec implies, answered from the spec and the principles the person agreed. A screen's visual design is theirs. When something needs the person to decide or do (a product choice the spec doesn't settle, an account only they can create, a design to sign off), ask it with `code-kit ask "<question>" --option "<choice>: <what it does>"` (`--story ST-n --blocks` holds the story until they answer). It waits in their Decisions tab, and their answer comes back to you; asking only in chat, it scrolls away. Lanes don't ask the person: they write the question for the lead.
14
- {{If harness.report.on: - When code-kit itself is wrong (a refusal that shouldn't have happened, a crash), draft a report with `code-kit report "<title>" --what "…" --expected "…"`: only code-kit's facts, never the project's. The person reads it and sends it, or not.}}
14
+
15
+ - {{if harness.report.on}}When code-kit itself is wrong (a refusal that shouldn't have happened, a crash), draft a report with `code-kit report "<title>" --what "…" --expected "…"`: only code-kit's facts, never the project's. The person reads it and sends it, or not.{{end}}
15
16
 
16
17
  If a hook blocks you, fix the cause; never work around it.
17
18
 
18
- The build runs from the specs in {{docs.specs}} and the stories in {{docs.plan}}. `/code-kit:next` picks the right step from the project's state; the steps are:
19
+ The build runs from the specs in `{{docs.specs}}` and the stories in `{{docs.plan}}`. `/code-kit:next` picks the right step from the project's state; the steps are:
19
20
 
20
21
  - `/code-kit:decide` decides what a lane asked for, on its merits;
21
22
  - `/code-kit:dispatch` starts ready stories on their lanes, each on its own `<lane>/st-<n>` branch;
@@ -23,6 +24,6 @@ The build runs from the specs in {{docs.specs}} and the stories in {{docs.plan}}
23
24
  - `/code-kit:status` shows what's done, in review, ready and blocked, and what's untested;
24
25
  - `/code-kit:spec-design` changes the specs when a decision changes. It updates the spec, the decision log and the plan together.
25
26
 
26
- {{If the CI workflow is installed: pull requests run `code-kit verify`, and it must pass before a person merges to a protected branch.}}
27
+ {{if ci}}Pull requests run `code-kit verify`, and it must pass before a person merges to a protected branch.{{end}}
27
28
 
28
29
  Changing the rules means editing `.claude/code-kit.json` (the lead only), then running the `code-kit:check` skill.