@warren-dean/code-kit 0.16.1 → 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.16.1",
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.16.1",
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,26 @@
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
+
16
+ ## 0.17.0 (2026-10-17)
17
+
18
+ The lead's context stays lean: lanes report to a file, and the lead's context is compacted when a milestone finishes.
19
+
20
+ - A lane on a story's branch now writes its full report to `.claude/state/reports/<branch>.md` before it finishes: what it did, the commands it ran with their output, what's left, and anything it needs decided. It hands the lead at most ten lines naming that file. The stop hook won't let a lane finish without the report, and the review, dispatch and decide skills read it when they need the detail. Before, every lane pasted its whole report, output included, into the lead's context; in one project that was about 236k of the lead's 780k tokens.
21
+ - Lanes don't message the lead while they work. They hand back once, when they finish or when they're blocked.
22
+ - When a milestone finishes during the session, the lead's context is compacted before the loop's next step, as the new `harness.compact.milestone` setting says: `ask` (the default) asks you in Claude Code's dialog, `auto` compacts at once, and `off` leaves it alone. The summary keeps the stories in flight and what waits on you, and drops the finished stories' detail. It's skipped when the context is under 25% full.
23
+ - Re-run `/code-kit:init` to update your lane agent files with the new reporting rules. Until then, the stop hook tells each lane what to do.
24
+
5
25
  ## 0.16.1 (2026-10-16)
6
26
 
7
27
  The lead loop stops asking for stories that are waiting on other work.
package/README.md CHANGED
@@ -326,14 +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**.
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.
337
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.
338
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.
339
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.
@@ -356,7 +359,7 @@ The loop keeps the build moving between your prompts, under `harness.autonomy`.
356
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.
357
360
  - **Queue**: the merge queue.
358
361
  - **Usage**.
359
- - **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.
360
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.
361
364
 
362
365
  ### Other views
@@ -416,6 +419,8 @@ Owner backend lane / backend-engineer → jobs lane / jobs-engineer: 14 file(s),
416
419
 
417
420
  Lanes never change silently: only when you approve the diff.
418
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
+
419
424
  ## The CLI
420
425
 
421
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.
@@ -431,6 +436,7 @@ code-kit verify [--base ref] [--branch name] [--no-checks] [--json] # a branch'
431
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
432
437
  code-kit status [--base ref] [--json] # stories and requirements against the spec
433
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
434
440
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
435
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
436
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
@@ -476,33 +482,33 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated [--story ST
476
482
 
477
483
  `.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
478
484
 
479
- | Field | Required | What it is |
480
- | ------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
481
- | `version` | yes | `1` |
482
- | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
483
- | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
484
- | `docs.decisions` | no | Where agreed intents are recorded as numbered decision records (`code-kit intend record`); default `docs/decisions` |
485
- | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
486
- | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
487
- | `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. |
488
- | `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. |
489
- | `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`. |
490
- | `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 |
491
- | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
492
- | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
493
- | `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 |
494
- | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
495
- | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
496
- | `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`). |
497
- | `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. |
498
- | `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). |
499
- | `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). |
500
- | `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). |
501
- | `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). |
502
- | `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 |
503
- | `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. |
504
- | `branches.protected` | no | Default `["main", "master"]` |
505
- | `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) 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`. |
506
512
 
507
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.
508
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}`,
@@ -6,6 +6,9 @@ export const AUTONOMY = ['autonomous', 'propose', 'off'];
6
6
  export const MODELS = ['haiku', 'sonnet', 'opus'];
7
7
 
8
8
  /** Each setting by its dotted key: its default, and why a value is wrong (or null when it's right). */
9
+ /** LEAN-3: what the loop does with the lead's context when a milestone finishes. */
10
+ export const COMPACT = ['ask', 'auto', 'off'];
11
+
9
12
  export const SETTINGS = {
10
13
  autonomy: {
11
14
  default: 'autonomous',
@@ -66,6 +69,18 @@ export const SETTINGS = {
66
69
  Number.isFinite(v) && v > 0 && v <= 100 ? null : 'must be a percentage from 1 to 100',
67
70
  about: "The plan's 5-hour use at which background agents pause",
68
71
  },
72
+ 'compact.milestone': {
73
+ default: 'ask',
74
+ problem: (v) => (COMPACT.includes(v) ? null : `must be one of ${COMPACT.join(', ')}`),
75
+ about:
76
+ "When a milestone finishes, compact the lead's context before the next: ask you, do it, or leave it",
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
+ },
69
84
  'report.on': {
70
85
  default: false,
71
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
  );
@@ -815,6 +821,14 @@ export function ownerOf(rel, config) {
815
821
  return matchesAny(rel, config.lead.paths) ? 'lead' : null;
816
822
  }
817
823
 
824
+ /**
825
+ * LEAN-1: a lane's full report for its branch: what it did, the commands it ran and their output, its
826
+ * questions. The lead's context gets a short hand-back naming this file, and reads it when it reviews.
827
+ */
828
+ export function reportPath(root) {
829
+ return `.claude/state/reports/${currentBranch(root).replace(/[^\w.-]+/g, '_')}.md`;
830
+ }
831
+
818
832
  // Lane work starts with the spec-check skill: its report for the branch must exist first.
819
833
  export function specCheckPath(root) {
820
834
  return `.claude/state/spec-check/${currentBranch(root).replace(/[^\w.-]+/g, '_')}.md`;