@warren-dean/code-kit 0.15.1 → 0.16.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.15.1",
12
+ "version": "0.16.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.15.1",
3
+ "version": "0.16.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.16.0 (2026-10-16)
6
+
7
+ The lead runs the team inside your guardrails, and lanes ask the lead, never you.
8
+
9
+ - You decide what crosses a guardrail: changes to the specs, the contracts and the architecture's rules, the screens' visual design, CI, weakened tests, and product questions the spec doesn't answer. The lead decides the rest, as `approvals.delegate` names it. `/code-kit:init` proposes that split and asks what else you want to keep; re-run it on an existing project to get the split, and the new wording in CLAUDE.md and the lane agent files.
10
+ - A lane refused something the lead decides is told so, with no `!` command and no mention of you. It hands the lead a change request with what it needs, the story and requirement, and what it checked first. The call isn't held, and nothing reaches your band. `code-kit requests` numbers each request and says whose it is to decide.
11
+ - The new `/code-kit:decide` skill judges each request on its merits. A package is approved only when the story's requirements need it, nothing the project already depends on does the job, and it fits the principles and the lane's layer; otherwise the lead tells the lane what to use instead. Only what crosses a guardrail comes to you, with the lead's recommendation. `code-kit next` returns `decide` while a lane waits on the lead, so the loop prompts it before reviews and dispatches.
12
+ - `code-kit approve --delegated` takes a package for a story: `--story ST-n`, checked against the plan, and recorded in the audit log. The npm registry rules apply only to npm's packages. A Python, Rust, Go or Ruby package used to be looked up on npm and sent to you; it's now the lead's judgement.
13
+ - The mod no longer approves calls within the delegated rules by itself. Every approval the lead gives now comes with its reasons.
14
+ - UX is the lead's to decide from the spec and the principles you agreed, such as what a screen does on an error or an empty list. A screen's visual design is still yours to approve.
15
+
5
16
  ## 0.15.1 (2026-10-16)
6
17
 
7
18
  Lanes stop less often to wait for you.
package/README.md CHANGED
@@ -52,12 +52,12 @@ code-kit catches each of these, most at the moment it would happen: the hook ref
52
52
 
53
53
  - **Lanes:** each lane agent writes only its own paths. The main session (the lead) writes contracts, specs and config, and delegates the rest. Any other subagent is read-only. An edit outside those paths is refused before it happens; a file a shell command writes outside them is reported straight after, for the agent to revert, and the finish check refuses to finish until it's gone.
54
54
  - **Layers:** each layer depends only on the layers it lists, and never on packages it's denied, for JavaScript and TypeScript imports. An edit that would add a violation is refused before it's written; the rules are checked again after each edit and before finishing, which is what catches a file a shell command wrote. In brownfield code, existing violations can be recorded in a baseline so only new ones block. Imports are found by pattern, not by parsing: see [the limits](#what-verify-proves-and-its-limits).
55
- - **Protected paths and the approval log:** every committed change to the kit, registers or contracts is appended to `.claude/approval-log.jsonl`, in the same commit. Only the lead writes these paths without a person's approval, and the person reviews them in the pull request.
55
+ - **Protected paths and the approval log:** every committed change to the kit, registers or contracts is appended to `.claude/approval-log.jsonl`, in the same commit. Only the lead writes these paths without an approval, and the person reviews them in the pull request. A lane needs an approval: the lead's for what `approvals.delegate` gives it, otherwise the person's.
56
56
  - **Spec-check before code:** when the project has specs, a lane can't write code on a branch until it has saved a spec-check report for that branch. The hook checks that the report exists; review checks what it says.
57
57
  - **Designs before screens** (optional): a screen file can't be written until the design register lists it.
58
58
  - **Shell rules**, matched against the command's text before it runs:
59
59
  - no force-push, pushing to or committing on protected branches, `--no-verify`, destructive deletes, or piping downloads into a shell;
60
- - no new dependency without a person's approval, for that package and that lane; the approval also covers the manifest and lockfile changes the install makes;
60
+ - no new dependency without an approval, for that package and that lane: the lead's, for a story it judged needs it, where dependencies are delegated, otherwise a person's; the approval also covers the manifest and lockfile changes the install makes;
61
61
  - no forged approvals;
62
62
  - no commit that weakens a test (deletes it, skips or focuses it, removes cases or assertions, or rewrites a snapshot) without the person's `tests` approval, which the lead can't delegate;
63
63
  - the project's own blocked commands (deploys, releases) and lane-restricted commands (shared local services).
@@ -315,7 +315,7 @@ The band sits above the prompt. With nothing happening and nothing waiting, it's
315
315
 
316
316
  What waits for you there:
317
317
 
318
- - **A held call.** An agent installing a new package, writing or committing a protected file, or a lane changing the kit. Rather than stopping the agent, the call waits while the band asks ("web wants to install dayjs · 1:53"). **Approve** opens the confirmation and, once you confirm, the same call runs; **Refuse**, or no answer within `harness.hold.minutes` (2 unless set), refuses it as before. Under `autonomy: autonomous`, a call within `approvals.delegate` is approved as delegated without asking. Nothing is held under `claude -p` or the SDK, where nobody can answer.
318
+ - **A held call.** An agent installing a new package, writing or committing a protected file, or a lane changing the kit, where the approval is yours. Rather than stopping the agent, the call waits while the band asks ("web wants to install dayjs · 1:53"). **Approve** opens the confirmation and, once you confirm, the same call runs; **Refuse**, or no answer within `harness.hold.minutes` (2 unless set), refuses it as before. What the lead decides (`approvals.delegate`) is never held for you: the lane asks the lead, which approves it on its merits. Nothing is held under `claude -p` or the SDK, where nobody can answer.
319
319
  - **Approvals agents asked for.** **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite. When it isn't clear what it's for, **Ask about it**: the answer comes from a side call over the lead's conversation and code-kit's facts about the request, shown in the pane and never added to the lead's session, and follow-ups carry on the aside. A request waits until it's answered, its file is committed by someone who could, or it's dropped (at most a week); on its card, **Not needed** drops it.
320
320
  - **Questions from the lead** ("The lead asks: which weather provider?"), asked with `code-kit ask`: what it can't settle and only you can. **Answer…** opens it as a card with each of the lead's options and what it does, and **Something else…** for your own words; your answer is recorded and sent to the lead. A question can hold its story, which then reads `waiting on you`: nothing dispatches or reviews it, and the stories that depend on it wait, until you answer.
321
321
  - **Your brief's open questions**, each with who decides and what it holds up (from the table's Owner and Blocks columns; one owned by the lead alone isn't yours, and doesn't show). **Answer…** sends your answer to the lead, which records it in the brief's decisions, and **Ask about it** in the answer box explains it first, beside the conversation; **Later** puts one off until the next session.
@@ -373,11 +373,13 @@ Once init has switched the rules on, the lead runs the build in a loop:
373
373
 
374
374
  ```text
375
375
  /code-kit:dispatch next # start every ready story on its lane
376
+ /code-kit:decide # decide what lanes asked for: approve, offer something better, or bring it to you
376
377
  /code-kit:review ST-12 # check a finished branch, then merge or send it back
377
378
  /code-kit:status # where the build stands against the spec
378
379
  ```
379
380
 
380
381
  - **Dispatch** picks the stories whose dependencies are done. It checks that no open question blocks them and that their contracts exist, then starts each lane agent in its own worktree on the branch `<lane>/st-<n>`, in parallel. Each brief names the story's requirements and acceptance criteria. Lanes spec-check first, build end to end, name requirement IDs in their test titles, and commit.
382
+ - **Decide** takes what a lane asked for and couldn't do itself: a package, a migration, a protected file, a UX question. The lead judges it against the story's requirements, the spec, the principles and the architecture, and approves it, tells the lane what to use instead, or brings it to you when it crosses a guardrail. See [Who decides what](#who-decides-what).
381
383
  - **Review** runs `verify` on the branch, then checks each requirement: built end to end, proved by a test, matching the spec-check report, and nothing beyond the story. It ends with merge, send back with the list of problems, or a question for you.
382
384
  - **Status** traces every requirement to its story, branch and tests. It shows what's done, in review, ready and blocked, which requirements no test names, and where the plan's status lines are out of date.
383
385
 
@@ -430,7 +432,7 @@ code-kit status [--base ref] [--json] # stories and requirements against the sp
430
432
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
431
433
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
432
434
  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
433
- code-kit requests [--json] # approval requests still waiting, the approvals in force, and every approval given
435
+ 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
434
436
  code-kit requests drop <n>[,<n>…]|all [--reason "…"] # stop waiting on requests nobody needs answered (the lead, or Not needed in the pane)
435
437
  code-kit ask "<question>" [--story ST-n] [--blocks] [--why "…"] [--option "Label: what it does"]... # the lead asks you what it can't settle; it waits in your Decisions tab
436
438
  code-kit ask [--json] | ask answer <id> "<answer>" | ask withdraw <id> --reason "…" # the questions; your answer (given in chat, recorded by the lead); one settled another way
@@ -464,7 +466,7 @@ code-kit map <story>|project [--json] # a story'
464
466
  code-kit trail [--json] # each requirement to its stories, the code they changed and the tests that name it
465
467
  code-kit adapters [--json --session id] # the adapters in use; with --json, what they know of each lane's work in a session
466
468
  code-kit hold <id> [--minutes N] # used by the mod: a held call waits here for the band's answer (--answer approved|refused)
467
- code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pane|<tool>] # record a person's approval from chat, or (--delegated) grant one within the delegated rules; a package name stands for its dep- approval
469
+ code-kit approve <name>... --reason "…" [--lane name] [--delegated [--story ST-n] | --via pane|<tool>] # record a person's approval from chat, or (--delegated) the lead's own, for a story (a package needs --story); a package name stands for its dep- approval
468
470
  ```
469
471
 
470
472
  `verify` compares the branch with where it left `--base` (default `origin/main`, then `main`). A branch named `<lane>/…`, or given as `--branch`, is held to that lane. `--no-checks` leaves the project's checks to CI's own steps. `status` reads `docs.specs` and `docs.plan`.
@@ -483,7 +485,7 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
483
485
  | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
484
486
  | `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. |
485
487
  | `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. |
486
- | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs a person's approval (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
488
+ | `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`. |
487
489
  | `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 |
488
490
  | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
489
491
  | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
@@ -495,7 +497,7 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
495
497
  | `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). |
496
498
  | `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). |
497
499
  | `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). |
498
- | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: approvals the lead may grant on its own, with `code-kit approve --delegated`, and with `merge`, merges into a protected branch with `code-kit merge --delegated`. Dependencies are checked against the npm 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). |
500
+ | `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). |
499
501
  | `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 |
500
502
  | `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. |
501
503
  | `branches.protected` | no | Default `["main", "master"]` |
@@ -536,27 +538,29 @@ code-kit approve dep-dayjs --lane web --reason "Yes, add dayjs for ST-12"
536
538
 
537
539
  ### Requests
538
540
 
539
- Every refusal that a person's approval would allow is recorded as a request in `.claude/state/requests.jsonl` in the main checkout: who was refused, the approvals that would allow it, the command or file, and why. A request stays open until approvals for all its names answer it, its file is committed by someone who could, or it's dropped (`code-kit requests drop`), and at most a week. `code-kit requests` lists the open ones, numbered, and the approvals in force, with minutes left, and the code-kit mod shows them above the prompt.
541
+ Every refusal that an approval would allow is recorded as a request in `.claude/state/requests.jsonl` in the main checkout: who was refused, the approvals that would allow it, the command or file, and why. A request stays open until approvals for all its names answer it, its file is committed by someone who could, or it's dropped (`code-kit requests drop`), and at most a week. `code-kit requests` lists the open ones, numbered, and the approvals in force, with minutes left. The code-kit mod shows the person's own above the prompt; the lead decides the rest (`code-kit next` hands them to it as `decide`).
540
542
 
541
543
  `--via pane` and `merge --person` are the person's own acts, which the code-kit mod uses for a press in its pane. The hooks refuse both from every agent.
542
544
 
543
- ### Delegated
545
+ ### Who decides what
544
546
 
545
- For a lead that runs without a person watching, such as an autonomous team, `approvals.delegate` lets the lead approve some things on its own authority, within rules the person sets in the config:
547
+ You set the guardrails and decide what crosses them: the specs, the contracts and the architecture's rules, the screens' visual design, CI, weakened tests, and product questions the spec doesn't answer. The lead decides everything inside them. `approvals.delegate` draws the line: what it names is the lead's, the rest is yours. `/code-kit:init` proposes the split and asks what you want to keep.
546
548
 
547
549
  ```json
548
- "approvals": { "delegate": { "protected": ["design"], "dependencies": { "minWeeklyDownloads": 10000 } } }
550
+ "approvals": { "delegate": { "protected": ["schema", "truth"], "dependencies": {} } }
549
551
  ```
550
552
 
553
+ A lane asks the lead, never you. When a lane is refused something the lead decides, the refusal says so, with no `!` command, and the lane hands the lead a change request and stops: what it needs, the story and requirement, and what it checked first. The call isn't held and nothing reaches your band. `code-kit next` hands the waiting requests to the lead (`decide`), and `/code-kit:decide` judges each on its merits: approve it for the story, tell the lane what to use instead, or bring it to you when it crosses a guardrail, with a recommendation.
554
+
551
555
  ```bash
552
- code-kit approve date-fns --lane web --delegated --reason "Date formatting for ST-12"
556
+ code-kit approve pillow --lane api --delegated --story ST-138 --reason "MED-5 needs HEIC decoding; nothing installed reads HEIC"
553
557
  ```
554
558
 
555
559
  - **Protected approvals:** the lead may grant those named in `protected`, and no others.
556
- - **Dependencies:** each package is checked against the npm registry. Its licence must be in `licences`; it must have at least `minWeeklyDownloads` downloads last week and a release within `maxMonthsSinceRelease` months; and installing it must run no scripts, unless `allowInstallScripts` allows them: `true` for any, or a list of the kinds allowed, such as `["binary"]` for packages that only download a prebuilt binary for the platform (esbuild, sharp). The kinds are `binary`, `build` (compiles native code with node-gyp) and `other` (runs the package's own code). Packages from other ecosystems, and anything the registry can't confirm, go to the person.
557
- - **All or nothing:** if any name is outside the rules, nothing is approved, and the command says which rule each one broke, so the lead asks the person.
560
+ - **Dependencies:** approved for a story, with `--story`: the lead approves a package only when the story's requirements need it, nothing the project already depends on does the job, and it fits the principles and the lane's layer. An npm package must also pass the registry rules: its licence must be in `licences`; it must have at least `minWeeklyDownloads` downloads last week and a release within `maxMonthsSinceRelease` months; and installing it must run no scripts, unless `allowInstallScripts` allows them: `true` for any, or a list of the kinds allowed, such as `["binary"]` for packages that only download a prebuilt binary for the platform (esbuild, sharp). The kinds are `binary`, `build` (compiles native code with node-gyp) and `other` (runs the package's own code). A package from another ecosystem (pip, uv, Poetry, cargo, go, bundler) is the lead's judgement alone.
561
+ - **All or nothing:** if any name is outside the rules, nothing is approved, and the command says which rule each one broke.
558
562
  - **The record:** the approval reads `(approved by the lead within the delegated rules: <rule>)` after the reason. The approval log keeps it when the change is committed.
559
- - **Who:** only the lead. A lane asks the lead with a change request, and its refusals say the lead may approve within the rules. Without `approvals.delegate`, `--delegated` is refused.
563
+ - **Who:** only the lead. Without `approvals.delegate`, `--delegated` is refused, and every approval is yours.
560
564
  - **Merges:** with `"merge": true`, the lead merges a reviewed branch into a protected branch with `code-kit merge <branch> --delegated`.
561
565
  - It runs `code-kit verify` on the branch first, checks included, and merges only if it passes. The checks run where the branch is checked out; in a temporary worktree without the project's install, checks that need one fail rather than being skipped.
562
566
  - The merge commit says it was verified and delegated.
package/bin/code-kit.mjs CHANGED
@@ -66,7 +66,7 @@ import {
66
66
  withdrawQuestion,
67
67
  } from '../hooks/lib/questions.mjs';
68
68
  import { traceFile } from '../hooks/lib/trace.mjs';
69
- import { buildStatus, specCheckRows } from '../hooks/lib/plan.mjs';
69
+ import { buildStatus, readStories, specCheckRows } from '../hooks/lib/plan.mjs';
70
70
  import { recordSentBack } from '../hooks/lib/reviews.mjs';
71
71
  import {
72
72
  SECURITY_AGENT,
@@ -96,6 +96,7 @@ import {
96
96
  PANE,
97
97
  approvalHistory,
98
98
  approvalsInForce,
99
+ decidedBy,
99
100
  delegatedMark,
100
101
  grantApprovals,
101
102
  dropRequests,
@@ -968,7 +969,7 @@ function story(id) {
968
969
 
969
970
  // Open approval requests and the approvals in force, for the person and the mod's band.
970
971
  function requests() {
971
- load();
972
+ const { config } = load();
972
973
  // CARD-7: a request nobody needs answered any more is dropped: the person's Dismiss, or the lead.
973
974
  if (rest[0] === 'drop') {
974
975
  if (!rest[1])
@@ -982,15 +983,20 @@ function requests() {
982
983
  `Dropped ${r.dropped.length} request${r.dropped.length === 1 ? '' : 's'}: ${r.dropped.map((d) => `${d.names.join(', ')} for ${d.lane ? `the ${d.lane} lane` : d.actor}`).join('; ')}.`,
983
984
  );
984
985
  }
985
- const open = openRequests(resolve('.'));
986
+ // HELM-2: each request's number for `requests drop`, and whose decision it is.
987
+ const open = openRequests(resolve('.')).map((r, i) => ({
988
+ ...r,
989
+ n: i + 1,
990
+ by: decidedBy(r.names, config),
991
+ }));
986
992
  const granted = approvalsInForce(resolve('.'));
987
993
  // CARD-6: every approval given, newest first.
988
994
  const history = approvalHistory(resolve('.')).slice(0, 100);
989
995
  if (flag('--json')) return out(JSON.stringify({ open, inForce: granted, history }, null, 2));
990
996
  out(open.length ? `${open.length} approval request(s) open:` : 'No approval requests open.');
991
- open.forEach((r, i) =>
997
+ open.forEach((r) =>
992
998
  out(
993
- ` ${i + 1}. ${r.names.join(', ')} for ${r.lane ? `the ${r.lane} lane` : r.actor}: ${r.what} (${r.why}; asked ${r.at.slice(0, 16).replace('T', ' ')})`,
999
+ ` ${r.n}. ${r.names.join(', ')} for ${r.lane ? `the ${r.lane} lane` : r.actor}, ${r.by === 'lead' ? "the lead's to decide" : "the person's to decide"}: ${r.what} (${r.why}; asked ${r.at.slice(0, 16).replace('T', ' ')})`,
994
1000
  ),
995
1001
  );
996
1002
  out(granted.length ? `\n${granted.length} approval(s) in force:` : '\nNo approvals in force.');
@@ -1366,6 +1372,19 @@ async function approve(given) {
1366
1372
  die(
1367
1373
  'This project delegates no approvals to the lead ("approvals.delegate" isn\'t set). Ask the person.',
1368
1374
  );
1375
+ // HELM-3: the lead approves a package for a story it has read, not by default.
1376
+ const packages = names.filter((n) => n.startsWith(DEPENDENCY_APPROVAL));
1377
+ if (packages.length && !storyGiven)
1378
+ die(
1379
+ `Nothing was approved. Name the story the package is for, with --story ST-n: approve a dependency only when that story's requirements need it, nothing the project already depends on does the job, and it fits the principles and the layer it goes in.`,
1380
+ );
1381
+ const stories = storyGiven && config.docs?.plan ? readStories('.', config.docs.plan) : [];
1382
+ const story = storyGiven
1383
+ ? (stories.find((st) => st.id.toUpperCase() === storyGiven.toUpperCase()) ??
1384
+ (config.docs?.plan
1385
+ ? die(`Nothing was approved: ${config.docs.plan} has no story ${storyGiven}.`)
1386
+ : { id: storyGiven }))
1387
+ : null;
1369
1388
  const outside = [];
1370
1389
  for (const name of names) {
1371
1390
  if (name === TESTS_APPROVAL) {
@@ -1378,10 +1397,22 @@ async function approve(given) {
1378
1397
  continue;
1379
1398
  }
1380
1399
  const pkg = packageOf(name);
1381
- const problems = dependencyRuleProblems(await npmFacts(pkg), rules.dependencies);
1400
+ // HELM-3: the registry rules are npm's; a package from another ecosystem is the lead's judgement.
1401
+ const registries = new Set(
1402
+ openRequests(resolve('.'))
1403
+ .filter((r) => r.names.includes(name) && (r.lane ?? undefined) === laneName)
1404
+ .map((r) => r.registries?.[pkg])
1405
+ .filter(Boolean),
1406
+ );
1407
+ const npm = !registries.size || registries.has('npm');
1408
+ const problems = npm ? dependencyRuleProblems(await npmFacts(pkg), rules.dependencies) : [];
1382
1409
  if (problems.length) outside.push(`${name}: ${problems.join('; ')}`);
1383
- else marks[name] = delegatedMark(`dependencies (${pkg})`);
1384
- } else if (rules.protected.includes(name)) marks[name] = delegatedMark(`protected: ${name}`);
1410
+ else
1411
+ marks[name] = delegatedMark(
1412
+ `dependencies (${pkg}${npm ? '' : `, from ${[...registries].join(', ')}, judged by the lead`}) for ${story.id}`,
1413
+ );
1414
+ } else if (rules.protected.includes(name))
1415
+ marks[name] = delegatedMark(`protected: ${name}${story ? ` for ${story.id}` : ''}`);
1385
1416
  else
1386
1417
  outside.push(
1387
1418
  `${name}: not among the delegated approvals (${rules.protected.join(', ') || 'none'})`,
@@ -8,7 +8,12 @@
8
8
  import { appendFileSync, existsSync, readFileSync } from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { join, resolve } from 'node:path';
11
- import { addedPackages, dependencyApproval, isDependencyFile } from './lib/dependencies.mjs';
11
+ import {
12
+ addedPackages,
13
+ dependencyApproval,
14
+ ecosystemOf,
15
+ isDependencyFile,
16
+ } from './lib/dependencies.mjs';
12
17
  import { CODE_KIT, GIT, codeOf, commandsOf, runs } from './lib/shell.mjs';
13
18
  import { block, start } from './lib/hook.mjs';
14
19
  import { CONFIG_FILE } from './lib/config.mjs';
@@ -22,7 +27,9 @@ import {
22
27
  arrivesWithMerge,
23
28
  approval,
24
29
  approvalHowTo,
30
+ askHowTo,
25
31
  currentBranch,
32
+ decidedBy,
26
33
  dependencyChange,
27
34
  dependencyGrants,
28
35
  useApproval,
@@ -242,8 +249,8 @@ if (approves && !delegating && !forPerson)
242
249
  `Blocked: this project doesn't let the lead record approvals ("approvals.lead" is off).${config.approvals.delegate ? ' Within the delegated rules, approve it yourself with --delegated;' : ''} Ask the person to run the \`!\` command from the refusal, or to turn "approvals": { "lead": true } on in the config.\nCommand: ${cmd}`,
243
250
  );
244
251
 
245
- // Each new package needs a person's approval, for this actor; it also covers the manifest and lockfile
246
- // changes the install makes (lib/rules.mjs › dependencyChange).
252
+ // Each new package needs an approval for this actor (the lead's where dependencies are delegated, HELM-3,
253
+ // otherwise a person's); it also covers the manifest and lockfile changes the install makes (lib/rules.mjs › dependencyChange).
247
254
  if (packages.length) {
248
255
  if (actor.kind === 'readonly')
249
256
  block(`Blocked: ${actor.label} is read-only; it may not add dependencies.\nCommand: ${cmd}`);
@@ -256,22 +263,17 @@ if (packages.length) {
256
263
  actor,
257
264
  unapproved.map(dependencyApproval),
258
265
  cmd,
259
- "A new dependency needs a person's approval.",
266
+ 'A new dependency needs an approval.',
267
+ Date.now(),
268
+ // HELM-3: where each package comes from, for the registry rules (npm's only).
269
+ { registries: Object.fromEntries(unapproved.map((p) => [p, ecosystemOf(cmd, p)])) },
260
270
  );
261
- const ask =
262
- actor.kind === 'lead'
263
- ? 'Ask the person to approve it by running'
264
- : config.approvals.delegate
265
- ? "Write a short change request for the lead (the package, why, and the lane's story), and stop. The lead approves it within the project's delegated rules, or asks the person to run"
266
- : "Write a short change request for the lead (the package, why, and the lane's story), and stop. The lead asks the person to run";
271
+ // HELM-2, HELM-3: a lane asks the lead, which judges the package on its merits where it decides.
272
+ const names = unapproved.map(dependencyApproval);
273
+ const whose =
274
+ decidedBy(names, config) === 'lead' ? "is the lead's decision" : "needs a person's approval";
267
275
  block(
268
- `Blocked: a new dependency (${unapproved.join(', ')}) needs a person's approval. ${ask}\n${approvalHowTo(
269
- unapproved.map(dependencyApproval),
270
- actor,
271
- sessionIn ?? project,
272
- 'installing it, and the manifest and lockfile changes that makes,',
273
- config,
274
- )}\nCommand: ${cmd}`,
276
+ `Blocked: a new dependency (${unapproved.join(', ')}) ${whose}. ${askHowTo(names, actor, sessionIn ?? project, config, { allows: 'installing it, and the manifest and lockfile changes that makes,' })}\nCommand: ${cmd}`,
275
277
  );
276
278
  }
277
279
  }
@@ -392,7 +394,7 @@ if (kitWrite && actor.kind !== 'lead' && !approval(root, 'kit', actor)) {
392
394
  "The .claude kit is changed only by the lead or with a person's approval.",
393
395
  );
394
396
  block(
395
- `Blocked: the .claude kit is changed only by the lead or with a person's approval. Ask the lead to run\n${approvalHowTo('kit', actor, root, undefined, config)}`,
397
+ `Blocked: the .claude kit is changed only by the lead or with a person's approval. ${askHowTo('kit', actor, root, config, { need: 'the change to the kit, and what the story needs it for' })}`,
396
398
  );
397
399
  }
398
400
 
@@ -447,7 +449,7 @@ function recordApprovals() {
447
449
  if (entry && !granted && actor.kind !== 'lead' && !byDependency) {
448
450
  recordRequest(root, actor, entry.approval, file, entry.why);
449
451
  block(
450
- `Blocked: ${file} is ${entry.why}; committing it needs a person's approval in force. Ask the lead to run\n${approvalHowTo(entry.approval, actor, root, undefined, config)}`,
452
+ `Blocked: ${file} is ${entry.why}; committing it needs ${decidedBy(entry.approval, config) === 'lead' ? "the lead's" : "a person's"} approval in force. ${askHowTo(entry.approval, actor, root, config)}`,
451
453
  );
452
454
  }
453
455
  if (granted)
@@ -93,6 +93,24 @@ MANAGERS.pip3 = MANAGERS.pip;
93
93
  * except a shell's `-c` script, which runs.
94
94
  */
95
95
  export function addedPackages(cmd) {
96
+ return [...new Set(packagesWithManagers(cmd).map((p) => p.name))];
97
+ }
98
+
99
+ /** The package managers that read the npm registry: their packages are npm's. */
100
+ const NPM_MANAGERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
101
+
102
+ /**
103
+ * HELM-3: the registry `pkg` comes from, by the command that adds it: `npm` for npm, pnpm, yarn and
104
+ * bun, otherwise the manager's name (`pip`, `uv`, `cargo`…), or null when the command doesn't add it.
105
+ */
106
+ export function ecosystemOf(cmd, pkg) {
107
+ const found = packagesWithManagers(cmd).find((p) => p.name === pkg);
108
+ if (!found) return null;
109
+ return NPM_MANAGERS.has(found.manager) ? 'npm' : found.manager;
110
+ }
111
+
112
+ /** Each package `cmd` adds, with the manager that adds it: `{ name, manager }`. */
113
+ function packagesWithManagers(cmd) {
96
114
  const found = [];
97
115
  for (const { text, code } of commandsOf(cmd)) {
98
116
  // Words split where the quote-masked code has spaces, so a quoted word stays one word.
@@ -103,18 +121,18 @@ export function addedPackages(cmd) {
103
121
  words.forEach(({ word, quoted }, i) => {
104
122
  if (quoted) return;
105
123
  if (SHELLS.has(basename(word)) && words[i + 1]?.word === '-c' && words[i + 2])
106
- found.push(...addedPackages(words[i + 2].word));
124
+ found.push(...packagesWithManagers(words[i + 2].word));
107
125
  const manager = MANAGERS[basename(word)];
108
126
  if (manager)
109
127
  found.push(
110
128
  ...packagesAfter(
111
129
  words.slice(i + 1).map((w) => w.word),
112
130
  manager,
113
- ),
131
+ ).map((name) => ({ name, manager: basename(word).replace(/3$/, '') })),
114
132
  );
115
133
  });
116
134
  }
117
- return [...new Set(found)];
135
+ return found;
118
136
  }
119
137
 
120
138
  const SHELLS = new Set(['sh', 'bash', 'zsh']);
@@ -9,7 +9,7 @@ import { buildStatus, readStories, DESIGN, LEAD } from './plan.mjs';
9
9
  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
- import { laneOf } from './rules.mjs';
12
+ import { decidedBy, laneOf, openRequests } from './rules.mjs';
13
13
  import { SECURITY_AGENT_FILE } from './security.mjs';
14
14
 
15
15
  export const DRAFT_FILE = '.claude/code-kit.draft.json';
@@ -253,6 +253,16 @@ export function nextStep(root, base) {
253
253
  attention.push(
254
254
  `${unbuildable.join(', ')} can't be dispatched until the plan's gaps are fixed.`,
255
255
  );
256
+ // HELM-4: a lane waiting on something the lead decides goes first: the lane can't go on without it.
257
+ const forLeadToDecide = openRequests(root).flatMap((r, i) =>
258
+ r.lane && decidedBy(r.names, config) === 'lead' ? [{ ...r, n: i + 1 }] : [],
259
+ );
260
+ if (forLeadToDecide.length)
261
+ return result(
262
+ 'decide',
263
+ `${forLeadToDecide.map((r, i) => `${i ? 'the' : 'The'} ${r.lane} lane asks for ${r.names.map((n) => n.replace(/^dep-/, '').replaceAll('+', '/')).join(', ')}`).join('; ')}: yours to decide, against the story's requirements, the spec and the principles.`,
264
+ { args: forLeadToDecide.map((r) => r.n).join(' ') },
265
+ );
256
266
  // The merge queue (spec 09): branches the lead's review passed. Under autonomy, where the base's
257
267
  // committed rules delegate merges, the lead merges the head first, so dependents see it; otherwise
258
268
  // the person does, and always the first merge, which brings code-kit's rules onto the base.
@@ -71,9 +71,51 @@ export const GIVEN_FILE = '.claude/state/approvals-given.jsonl';
71
71
  /** Marks an approval the lead granted on its own, within `approvals.delegate`; `rule` names the rule. */
72
72
  export const delegatedMark = (rule) => `(approved by the lead within the delegated rules: ${rule})`;
73
73
 
74
- /** The command the lead runs to approve `names` itself, within the delegated rules. */
74
+ /** The command the lead runs to approve `names` itself, within the delegated rules (HELM-3: for a story). */
75
75
  export const delegateCommand = (names, lane) =>
76
- `node "${CLI}" approve ${[names].flat().join(' ')}${lane ? ` --lane ${lane}` : ''} --delegated --reason "<why, for this story>"`;
76
+ `node "${CLI}" approve ${[names].flat().join(' ')}${lane ? ` --lane ${lane}` : ''} --delegated --story <ST-n> --reason "<${
77
+ [names].flat().some((n) => String(n).startsWith(DEPENDENCY_APPROVAL))
78
+ ? 'what the story needs it for, what the project already has, how it fits'
79
+ : 'what the story needs the change for'
80
+ }>"`;
81
+
82
+ /**
83
+ * HELM-1: whether the lead grants approval `name` itself, by the project's delegated rules. Weakening a
84
+ * test is always the person's; a package is the lead's where dependencies are delegated.
85
+ */
86
+ export function leadDecides(name, config) {
87
+ const rules = config?.approvals?.delegate;
88
+ if (!rules || name === 'tests') return false;
89
+ if (String(name).startsWith(DEPENDENCY_APPROVAL)) return Boolean(rules.dependencies);
90
+ return (rules.protected ?? []).includes(name);
91
+ }
92
+
93
+ /** HELM-2: whose decision approvals `names` are: the lead's when it may grant every one, else the person's. */
94
+ export const decidedBy = (names, config) =>
95
+ [names].flat().every((n) => leadDecides(n, config)) ? 'lead' : 'person';
96
+
97
+ /**
98
+ * HELM-2: what an actor refused for `names` does next, after the refusal's first line. A lane asks the
99
+ * lead, never the person: with no `!` command where the lead decides. The lead approves its own call
100
+ * on its merits. What the person decides is brought to them by the lead.
101
+ */
102
+ export function askHowTo(names, actor, root, config, { allows, need } = {}) {
103
+ const lane = actor?.kind === 'lane' ? actor.lane : undefined;
104
+ const pkg = [names].flat().some((n) => String(n).startsWith(DEPENDENCY_APPROVAL));
105
+ const brief =
106
+ need ??
107
+ (pkg
108
+ ? 'the package, the story and requirement that need it, what the project already has that you checked, and anything lighter that would do'
109
+ : 'the file, the story and requirement that need the change, and what you checked first');
110
+ if (decidedBy(names, config) === 'lead') {
111
+ if (actor?.kind === 'lead')
112
+ return `You decide this as the lead. Approve it only if the story's requirements need it${pkg ? ', nothing the project already depends on does the job, and it fits the principles and the layer it goes in' : ' and it keeps to the spec and the architecture'}:\n Decided by the lead: ${delegateCommand(names)}`;
113
+ return `The lead decides this, not the person. Hand the lead a change request and stop: ${brief}. The lead approves it, or tells you what to use instead.\n Decided by the lead: ${delegateCommand(names, lane)}`;
114
+ }
115
+ if (actor?.kind === 'lead')
116
+ return `This is the person's decision: ask them, with what it's for and your recommendation. They approve it with\n${approvalHowTo(names, actor, root, allows, config)}`;
117
+ return `The person decides this, and the lead brings it to them. Hand the lead a change request and stop: ${brief}. Their approval is\n${approvalHowTo(names, actor, root, allows, config)}`;
118
+ }
77
119
 
78
120
  export const relayCommand = (names, lane) =>
79
121
  `node "${CLI}" approve ${[names].flat().join(' ')}${lane ? ` --lane ${lane}` : ''} --reason "<the person's words>"`;
@@ -370,7 +412,7 @@ export function dropRequests(root, which, { by, reason = null }, now = Date.now(
370
412
  * actor, names and command or file while one is still open count as one request. Recording never
371
413
  * changes the refusal, so any failure here is ignored.
372
414
  */
373
- export function recordRequest(root, actor, names, what, why, now = Date.now()) {
415
+ export function recordRequest(root, actor, names, what, why, now = Date.now(), extra = {}) {
374
416
  // A read-only agent (a reviewer) is never approved to write: what it changed goes back, not to the
375
417
  // person as a request.
376
418
  if (actor?.kind === 'readonly') return;
@@ -382,6 +424,7 @@ export function recordRequest(root, actor, names, what, why, now = Date.now()) {
382
424
  names: [names].flat(),
383
425
  what: String(what).slice(0, 200),
384
426
  why: String(why).slice(0, 300),
427
+ ...extra,
385
428
  };
386
429
  if (openRequests(root, now).some((r) => sameRequest(r, request))) return;
387
430
  const file = requestsFile(root);
@@ -715,7 +758,11 @@ export function writeProblem(actor, rel, root, config) {
715
758
  // and every committed change is in the audit log. Anyone else needs a person's approval as well.
716
759
  if (guarded && actor.kind !== 'lead' && !approval(root, guarded.approval, actor)) {
717
760
  recordRequest(root, actor, guarded.approval, rel, guarded.why);
718
- return `${rel} is ${guarded.why} and needs a person's approval. Ask the lead (a person) to run\n${approvalHowTo(guarded.approval, actor, root, undefined, config)}`;
761
+ const who =
762
+ decidedBy(guarded.approval, config) === 'lead'
763
+ ? "the lead's approval"
764
+ : "a person's approval";
765
+ return `${rel} is ${guarded.why} and needs ${who}. ${askHowTo(guarded.approval, actor, root, config)}`;
719
766
  }
720
767
  if (matchesAny(rel, config.anyActor)) return null;
721
768
  if (actor.kind === 'readonly') return readonlyProblem(actor, rel);
@@ -227,9 +227,7 @@ function headlineOf(line, story) {
227
227
  function whyOf(line, check) {
228
228
  if (line.kind === 'held') {
229
229
  const c = line.held;
230
- const rule = c.refusal ? firstSentence(c.refusal) : 'It needs an approval only you can give.';
231
- const limits = limitsWhy(c.outside);
232
- return limits ? `${rule} ${limits}` : rule;
230
+ return c.refusal ? firstSentence(c.refusal) : 'It needs an approval only you can give.';
233
231
  }
234
232
  if (line.kind === 'approvals')
235
233
  return line.request?.why ?? 'It needs an approval only you can give.';
@@ -279,23 +277,6 @@ function whyOf(line, check) {
279
277
  /** A refusal's first sentence: what the rule is, without the instructions meant for the agent. */
280
278
  const firstSentence = (text) => text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text;
281
279
 
282
- /**
283
- * Why the lead couldn't approve it itself, from `code-kit approve --delegated`'s refusal: nothing
284
- * delegated, or the delegated rules each name it missed ("dep-x: fewer than 1000 weekly downloads").
285
- */
286
- export function limitsWhy(outside) {
287
- if (!outside) return null;
288
- if (/delegates no approvals/.test(outside))
289
- return "You haven't let the lead approve these itself.";
290
- const misses = outside
291
- .split('\n')
292
- .filter((l) => /^\s{2}\S/.test(l))
293
- .map((l) => l.trim());
294
- return misses.length
295
- ? `It's outside the limits you set for the lead: ${misses.join('; ')}.`
296
- : null;
297
- }
298
-
299
280
  /** The story a line belongs to: the held call's or request's lane's story, the stalled story, the queued branch's. */
300
281
  function storyOf(line, state) {
301
282
  const stories = state?.stories ?? [];
@@ -13,6 +13,8 @@ const HOLDABLE = new Set(['dependency', 'approval', 'kit']);
13
13
  export function holdable(resultText) {
14
14
  const card = refusalCard(resultText);
15
15
  if (!card || !HOLDABLE.has(card.kind) || !card.request?.names.length) return null;
16
+ // HELM-6: what the lead decides is never held for the person; the lane asks the lead.
17
+ if (card.request.by === 'lead') return null;
16
18
  return { kind: card.kind, title: card.title, ...card.request };
17
19
  }
18
20
 
@@ -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
+ 'decide',
7
8
  'dispatch',
8
9
  'review',
9
10
  'lead',
@@ -80,6 +81,11 @@ export function leadPrompt(step, { cli = 'code-kit' } = {}) {
80
81
  // SETUP-2: the base has no rules yet; the lead puts the approved ones there, then merges as usual.
81
82
  if (step.step === 'rules')
82
83
  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.`;
84
+ // HELM-4: a lane waits on the lead's decision; the decide skill takes each request on its merits.
85
+ if (step.step === 'decide')
86
+ return ids.length
87
+ ? `${step.why ?? 'A lane waits on your decision.'} Run /code-kit:decide ${ids.join(' ')}.`
88
+ : null;
83
89
  // ONB-3: a companion set up before any coding.
84
90
  if (step.step === 'setup')
85
91
  return ids.length
@@ -115,6 +121,7 @@ export function stepLabel(step) {
115
121
  const ids = (step.args ?? '').split(/\s+/).filter(Boolean).join(', ');
116
122
  const verb =
117
123
  {
124
+ decide: 'Decide requests',
118
125
  dispatch: 'Dispatch',
119
126
  review: 'Review',
120
127
  lead: 'Build',
@@ -67,7 +67,7 @@ import {
67
67
  withoutInFlight,
68
68
  } from './loop.mjs';
69
69
  import { goalOf, nextText } from './mission.mjs';
70
- import { contextOf, decisionCards, limitsWhy, nativeChoice } from './decisions.mjs';
70
+ import { contextOf, decisionCards, nativeChoice } from './decisions.mjs';
71
71
  import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
72
72
  import { glowOf, mapStoryOf, touchOf } from './codemap.mjs';
73
73
  import { harnessSettings } from '../lib/harness.mjs';
@@ -268,7 +268,11 @@ export function register(on) {
268
268
  const valid = Boolean(check?.valid);
269
269
  const status = valid ? await json('status') : null;
270
270
  const nextStep = valid ? await json('next') : null;
271
- const requests = valid ? await json('requests') : null;
271
+ const all = valid ? await json('requests') : null;
272
+ // HELM-6: the person sees only their own decisions; the lead decides the rest.
273
+ const requests = all
274
+ ? { ...all, open: (all.open ?? []).filter((r) => r.by !== 'lead') }
275
+ : null;
272
276
  const stops = valid ? ((await json('stops', '--session', session)) ?? []) : [];
273
277
  const queue = valid ? ((await json('queue')) ?? []) : [];
274
278
  // VIEW-2: when each story's work happened, for the lanes' timelines.
@@ -532,7 +536,6 @@ export function register(on) {
532
536
  reason: prefilledReason(line.request),
533
537
  error: null,
534
538
  heldId: line.heldId ?? null,
535
- outside: line.outside ?? null,
536
539
  aside: { thread: [], draft: '' },
537
540
  };
538
541
  await $.ui.open({ id: APPROVE_ID, title: 'Approve', focus: true, closeOnEscape: true });
@@ -581,7 +584,7 @@ export function register(on) {
581
584
  const same = (r) =>
582
585
  [...r.names].sort().join() === [...line.request.names].sort().join() &&
583
586
  (r.lane ?? r.actor) === (line.request.lane ?? line.request.actor);
584
- const numbers = open.flatMap((r, i) => (same(r) ? [i + 1] : []));
587
+ const numbers = open.flatMap((r, i) => (same(r) ? [r.n ?? i + 1] : []));
585
588
  if (!numbers.length) return;
586
589
  const ran = await $.process.run(
587
590
  ['node', cli, 'requests', 'drop', numbers.join(','), '--via', 'pane'],
@@ -734,7 +737,6 @@ export function register(on) {
734
737
  why: line.held.refusal ?? null,
735
738
  },
736
739
  heldId: line.held.id,
737
- outside: line.held.outside ?? null,
738
740
  }),
739
741
  // ACT-5: a side call over the lead's own conversation (it shares the prompt cache, and nothing is
740
742
  // added to the conversation); before the lead's first reply, from the prompt's facts alone.
@@ -785,7 +787,6 @@ export function register(on) {
785
787
  const prompt = approvalAsidePrompt({
786
788
  request: asked.request,
787
789
  story: row?.story ?? null,
788
- outside: limitsWhy(asked.outside) ?? null,
789
790
  thread,
790
791
  question: q,
791
792
  });
@@ -1757,26 +1758,6 @@ export function register(on) {
1757
1758
  // HOLD-6: nobody to ask (claude -p, the SDK), or hold switched off: refused as today.
1758
1759
  if (!ask || !where?.interactive || minutes <= 0 || model.state?.kind !== 'ok') return res;
1759
1760
  const id = e.tool_use_id;
1760
- let outsideLimits = null;
1761
- // HOLD-5: under autonomy, the project's delegated rules decide first, without asking.
1762
- if ((settings?.autonomy ?? 'autonomous') === 'autonomous') {
1763
- const delegated = await $.process.run(
1764
- [
1765
- 'node',
1766
- where.cli,
1767
- 'approve',
1768
- ...ask.names,
1769
- ...(ask.lane ? ['--lane', ask.lane] : []),
1770
- '--delegated',
1771
- '--reason',
1772
- `Held call within the delegated rules: ${ask.what}`,
1773
- ],
1774
- { cwd: where.cwd, timeoutMs: 60000 },
1775
- );
1776
- if (delegated.exitCode === 0) return next(e);
1777
- // DECIDE-2: why it's outside the limits the person set, for the decision's card.
1778
- outsideLimits = (delegated.stderr || delegated.stdout || '').trim();
1779
- }
1780
1761
  const agent = e.agentId ? agentsSeen.get(e.agentId) : null;
1781
1762
  held.set(id, {
1782
1763
  id,
@@ -1788,10 +1769,9 @@ export function register(on) {
1788
1769
  what: ask.what,
1789
1770
  since: await $.clock.now(),
1790
1771
  minutes,
1791
- // DECIDE-2: the call itself, the rule that refused it, and why the lead couldn't approve it.
1772
+ // DECIDE-2: the call itself, and the rule that refused it.
1792
1773
  command: e.command ?? e.file_path ?? null,
1793
1774
  refusal: text.split('\n')[0],
1794
- outside: outsideLimits,
1795
1775
  });
1796
1776
  $.ui.invalidate('ui.render');
1797
1777
  // The wait is a process the band's answer ends (`code-kit hold`), so it isn't the hook's own time.
@@ -97,10 +97,7 @@ function project() {
97
97
  setExit: 0,
98
98
  allowed: false, // an approval in force: the hooks beneath let the call through
99
99
  bashRuns: 0,
100
- delegateExit: 1,
101
- delegateRefusal:
102
- 'This project delegates no approvals to the lead ("approvals.delegate" isn\'t set). Ask the person.',
103
- delegated: [] as string[][],
100
+ delegated: [] as string[][], // HELM-6: the mod never approves for the lead, so this stays empty
104
101
  holds: [] as string[][],
105
102
  draft: '', // what the person has typed in the prompt
106
103
  heads: { 'web/st-4': 'abc123def4567890\n' } as Record<string, string>,
@@ -284,8 +281,7 @@ function stub(on: any, w: World) {
284
281
  if (sub === 'queue') return ran(0, JSON.stringify(w.queue));
285
282
  if (sub === 'approve' && argv.includes('--delegated')) {
286
283
  w.delegated.push([...argv.slice(2)]);
287
- if (w.delegateExit === 0) w.allowed = true;
288
- return ran(w.delegateExit, '', w.delegateExit ? w.delegateRefusal : '');
284
+ return ran(1, '', 'Nothing was approved.');
289
285
  }
290
286
  if (sub === 'hold') {
291
287
  const id = argv[3];
@@ -653,6 +649,24 @@ test('BAND-2 and BAND-4 one open request reads "1 approval waiting", with Approv
653
649
  await ui.unmount();
654
650
  });
655
651
 
652
+ test("HELM-6 a request the lead decides isn't the person's: only theirs waits in the band", async ($, on) => {
653
+ const w = project();
654
+ w.requests = [
655
+ { ...dayjs, n: 1, by: 'lead' },
656
+ { ...dayjs, names: ['contract'], what: 'packages/contracts/a.ts', n: 2, by: 'person' },
657
+ ];
658
+ await start($, on, w);
659
+ const ui = await bandUi($);
660
+ expect(await ui.find({ type: 'Text', text: '1 approval waiting' })).toBeDefined();
661
+ await press($, 'band-decide');
662
+ await ui.unmount();
663
+ // Not needed drops it by the number the CLI gave it, past the lead's request it doesn't show.
664
+ const decisions = await pane($, PANE);
665
+ await press($, 'decide-dropRequest', PANE);
666
+ expect(w.acts).toContainEqual(['requests', 'drop', '2', '--via', 'pane']);
667
+ await decisions.unmount();
668
+ });
669
+
656
670
  test('ACT-1 and BAND-3 approving with the prefilled reason grants it for the lane, and the line goes', async ($, on) => {
657
671
  const w = project();
658
672
  w.requests = [dayjs];
@@ -1412,9 +1426,8 @@ test('DECIDE-2 a decision in the band says which story it is for, and d opens it
1412
1426
  expect(card?.text).toMatch(/BOOK-1 Request a booking/);
1413
1427
  expect(card?.text).toMatch(/npm install dayjs/);
1414
1428
  expect(card?.text).toMatch(/ST-4 Booking form \(BOOK-1\) was handed to the web lane/);
1415
- expect(card?.text).toMatch(
1416
- /Blocked: a new dependency \(dayjs\) needs a person's approval\. You haven't let the lead approve these itself\./,
1417
- );
1429
+ expect(card?.text).toMatch(/Blocked: a new dependency \(dayjs\) needs a person's approval\./);
1430
+ expect(card?.text).not.toMatch(/limits you set/);
1418
1431
  expect((await ui.find({ key: 'decision-option-approveHeld' }))?.text).toMatch(
1419
1432
  /The held call runs now, and the agent carries on/,
1420
1433
  );
@@ -1972,10 +1985,12 @@ test('HOLD-4 Refuse, or no answer in the hold time, lets the refusal through and
1972
1985
  await ui.unmount();
1973
1986
  });
1974
1987
 
1975
- test('HOLD-5 within the delegated rules the call is approved as delegated and runs, without asking', async ($, on) => {
1988
+ const leadInstall =
1989
+ 'Blocked: a new dependency (dayjs) is the lead\'s decision. The lead decides this, not the person. Hand the lead a change request and stop: the package, the story and requirement that need it, what the project already has that you checked, and anything lighter that would do. The lead approves it, or tells you what to use instead.\n Decided by the lead: node "/x/bin/code-kit.mjs" approve dep-dayjs --lane web --delegated --story <ST-n> --reason "<what the story needs it for, what the project already has, how it fits>"\nCommand: npm install dayjs';
1990
+
1991
+ test("HOLD-5 and HELM-6 a lane's call the lead decides isn't held, and the mod doesn't approve it for the lead", async ($, on) => {
1976
1992
  const w = project();
1977
- w.refusal = webInstall;
1978
- w.delegateExit = 0;
1993
+ w.refusal = leadInstall;
1979
1994
  await start($, on, w);
1980
1995
  const res: any = await $.tool.call({
1981
1996
  tool: 'Bash',
@@ -1983,18 +1998,17 @@ test('HOLD-5 within the delegated rules the call is approved as delegated and ru
1983
1998
  command: 'npm install dayjs',
1984
1999
  agentId: 'a1',
1985
2000
  } as any);
1986
- expect(refused(res)).toBe(false);
1987
- expect(w.delegated[0]).toEqual(
1988
- expect.arrayContaining(['approve', 'dep-dayjs', '--lane', 'web', '--delegated']),
1989
- );
2001
+ expect(refused(res)).toBe(true);
1990
2002
  expect(w.holds).toEqual([]);
2003
+ expect(w.delegated).toEqual([]);
2004
+ const band = await bandUi($);
2005
+ expect(await band.find({ type: 'Text', text: /wants to install/ })).toBeUndefined();
2006
+ await band.unmount();
1991
2007
  });
1992
2008
 
1993
- test('HOLD-5 with autonomy propose the delegated rules are not tried; the call is held', async ($, on) => {
2009
+ test("HOLD-5 a call the person decides is held for them, without trying the lead's rules", async ($, on) => {
1994
2010
  const w = project();
1995
2011
  w.refusal = webInstall;
1996
- w.delegateExit = 0;
1997
- (w.check as any).harness = { autonomy: 'propose', hold: { minutes: 2 } };
1998
2012
  const clock = await start($, on, w);
1999
2013
  const { call } = await heldCall($, w, clock);
2000
2014
  expect(w.delegated).toEqual([]);
@@ -1231,13 +1231,7 @@ export function questionAsidePrompt({ q, thread = [], question }) {
1231
1231
  * names, the lane, what was refused and why, its story, the limits it missed), the aside so far, and the
1232
1232
  * question. It answers in the pane, never in the conversation, and acts on nothing.
1233
1233
  */
1234
- export function approvalAsidePrompt({
1235
- request,
1236
- story = null,
1237
- outside = null,
1238
- thread = [],
1239
- question,
1240
- }) {
1234
+ export function approvalAsidePrompt({ request, story = null, thread = [], question }) {
1241
1235
  return [
1242
1236
  "The person is deciding whether to approve something code-kit stopped, and asks you about it in code-kit's approval pane. Answer in a few short sentences, in plain words: what it is, why it's needed for the work, what approving it lets happen, and anything risky about it. Use what you know from this conversation and the facts below. If you don't know, say so; don't guess. This answer goes to that pane, not into the conversation: don't act on it, and don't change your plans because of it.",
1243
1237
  '',
@@ -1251,7 +1245,6 @@ export function approvalAsidePrompt({
1251
1245
  `- story: ${story.id} ${story.title ?? ''}${story.requirements?.length ? ` (${story.requirements.join(', ')})` : ''}`,
1252
1246
  ]
1253
1247
  : []),
1254
- ...(outside ? [`- outside the limits the person set for the lead: ${outside}`] : []),
1255
1248
  ...(thread.length
1256
1249
  ? [
1257
1250
  '',
@@ -1350,6 +1343,8 @@ export function resultPane(result, { Box, Text }) {
1350
1343
 
1351
1344
  const APPROVAL_LINE =
1352
1345
  /^\s*! echo "<what you are approving>" > .*?\.claude\/approvals\/(?:([^/\s]+)\/)?([^/\s]+)\s*$/gm;
1346
+ const LEAD_LINE =
1347
+ /^\s*Decided by the lead: node "[^"]*" approve (.+?)(?: --lane (\S+))? --delegated\b/m;
1353
1348
 
1354
1349
  /**
1355
1350
  * A code-kit refusal read back from the text its hooks print (CARD-1): what was refused, the rule and
@@ -1363,10 +1358,23 @@ export function refusalCard(raw) {
1363
1358
  const [first, ...rest] = text.split('\n');
1364
1359
  const approvals = [...text.matchAll(APPROVAL_LINE)];
1365
1360
  const command = text.match(/^Command: (.+)$/m)?.[1] ?? null;
1361
+ // HELM-2: what the lead decides carries its own approve command, never a person's `!` lines.
1362
+ const byLead = text.match(LEAD_LINE);
1366
1363
  const request = (what) =>
1367
- approvals.length
1368
- ? { names: approvals.map((m) => m[2]), lane: approvals[0][1] ?? null, what }
1369
- : null;
1364
+ byLead
1365
+ ? {
1366
+ names: byLead[1].split(/\s+/),
1367
+ lane: byLead[2] ?? null,
1368
+ what,
1369
+ by: 'lead',
1370
+ }
1371
+ : approvals.length
1372
+ ? { names: approvals.map((m) => m[2]), lane: approvals[0][1] ?? null, what, by: 'person' }
1373
+ : null;
1374
+ const todoFor = (lasts) =>
1375
+ byLead
1376
+ ? 'The lead decides it on its merits: it approves it, or tells the lane what to use instead.'
1377
+ : `A person approves it, here or with the \`!\` command, ${lasts}.`;
1370
1378
  let m = first.match(/^The (.+?) may not write (\S+?)(?: \(owned by the (.+)\))?\.$/);
1371
1379
  if (m)
1372
1380
  return {
@@ -1377,23 +1385,25 @@ export function refusalCard(raw) {
1377
1385
  request: null,
1378
1386
  raw: text,
1379
1387
  };
1380
- m = first.match(/^(\S+) is (.+) and needs a person's approval\./);
1388
+ m = first.match(/^(\S+) is (.+) and needs (?:a person's|the lead's) approval\./);
1381
1389
  if (m)
1382
1390
  return {
1383
1391
  kind: 'approval',
1384
1392
  title: `Approval needed: ${m[1]}`,
1385
1393
  why: `It's ${m[2]}.`,
1386
- todo: 'A person approves it, here or with the `!` command, for 60 minutes.',
1394
+ todo: todoFor('for 60 minutes'),
1387
1395
  request: request(`write ${m[1]}`),
1388
1396
  raw: text,
1389
1397
  };
1390
- m = first.match(/^(\S+) is (.+); committing it needs a person's approval in force\./);
1398
+ m = first.match(
1399
+ /^(\S+) is (.+); committing it needs (?:a person's|the lead's) approval in force\./,
1400
+ );
1391
1401
  if (m)
1392
1402
  return {
1393
1403
  kind: 'approval',
1394
1404
  title: `Commit refused: ${m[1]}`,
1395
1405
  why: `It's ${m[2]}.`,
1396
- todo: 'A person approves it, here or with the `!` command, for 60 minutes.',
1406
+ todo: todoFor('for 60 minutes'),
1397
1407
  request: request(`commit ${m[1]}`),
1398
1408
  raw: text,
1399
1409
  };
@@ -1402,17 +1412,21 @@ export function refusalCard(raw) {
1402
1412
  kind: 'kit',
1403
1413
  title: 'Kit edit refused',
1404
1414
  why: "The .claude kit is changed only by the lead or with a person's approval.",
1405
- todo: 'A person approves it, here or with the `!` command, for 60 minutes.',
1415
+ todo: todoFor('for 60 minutes'),
1406
1416
  request: request('change the .claude kit'),
1407
1417
  raw: text,
1408
1418
  };
1409
- m = first.match(/^a new dependency \((.+?)\) needs a person's approval\./);
1419
+ m = first.match(
1420
+ /^a new dependency \((.+?)\) (?:needs a person's approval|is the lead's decision)\./,
1421
+ );
1410
1422
  if (m)
1411
1423
  return {
1412
1424
  kind: 'dependency',
1413
1425
  title: `Install refused: ${m[1]}`,
1414
- why: "A new dependency needs a person's approval.",
1415
- todo: 'A person approves it, here or with the `!` command, until the install is committed (at most 7 days).',
1426
+ why: byLead
1427
+ ? "A new dependency is the lead's decision."
1428
+ : "A new dependency needs a person's approval.",
1429
+ todo: todoFor('until the install is committed (at most 7 days)'),
1416
1430
  request: request(command ?? `install ${m[1]}`),
1417
1431
  raw: text,
1418
1432
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.15.1",
3
+ "version": "0.16.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",
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: decide
3
+ description: The lead decides what a lane asked for and couldn't do itself (a new package, a migration, a protected file, a UX question) against the story's requirements, the spec, the principles and the architecture. Approves it, answers the lane with something that fits better, or takes it to the person only when it crosses a guardrail. Use when code-kit's loop or `code-kit next` says a lane waits on your decision, when a lane hands back a change request, or when the person runs /code-kit:decide [request numbers].
4
+ ---
5
+
6
+ # Decide a lane's request
7
+
8
+ The person sets the guardrails: the specs, the architecture (contracts, layers, the rules in `.claude/code-kit.json`), the screens' visual design, CI, weakened tests, and product questions the spec doesn't answer. Inside them, you run the team. A lane that needs something it can't do itself asks you, and you decide on the merits. Passing a request to the person because you could is not deciding: they'd approve it by default, and the check would be gone.
9
+
10
+ The CLI is `node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs"`.
11
+
12
+ ## 1. Find the requests
13
+
14
+ `requests --json` lists each open request with its number (`n`), the lane, what it asked for (`names`, `what`), and whose decision it is (`by`). The argument, if given, names the numbers to take. Take every request `by: "lead"`. A request `by: "person"` is theirs: see step 4.
15
+
16
+ For each, read the lane's change request (its last report, or ask the lane with SendMessage if it's still running), its story in the plan, and the requirements the story cites, in the specs.
17
+
18
+ ## 2. Judge it
19
+
20
+ **A package.** Approve it only when all of these hold:
21
+
22
+ - The story's requirements need what it does. Name the requirement.
23
+ - Nothing the project already depends on does the job: read the manifest (`package.json`, `pyproject.toml`, `Cargo.toml`…) and search the code for an existing helper.
24
+ - It fits the principles (`docs.principles`) and the lane's layer: a layer's `denyPackages` in the config, and anything the principles say about dependencies, such as no SDK in the domain layer, or a port before a vendor.
25
+ - It's the lightest thing that does the job. A whole framework for one function is the wrong answer; the standard library or a few lines of code may be the right one.
26
+ - It's maintained, and its licence is one the project can ship. For npm packages, `approve` checks the registry against `approvals.delegate.dependencies`. For other ecosystems the check is yours: look at the package's page.
27
+
28
+ **A protected file** (a migration, a validator, anything in `protected` that's yours to approve). Approve it when the change carries out what the story's requirements ask, and nothing more: a migration that adds the columns the spec names, a validator change the spec's rule requires. A change the spec doesn't call for is scope creep; send it back.
29
+
30
+ **A UX question** (what a screen does on an error, an empty list, a slow load, a second tap, the wording the spec implies). Answer it from the spec and the principles the person agreed, and write the answer where the lane will read it: the story in the plan, or the spec section's notes. Ask the person only when the spec and the principles don't settle it. A screen's visual design is theirs: it's approved before the screen is built.
31
+
32
+ ## 3. Act on it
33
+
34
+ - **Yes.** Approve it for the lane and the story, saying why in the reason:
35
+
36
+ ```bash
37
+ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" approve <package or approval> --lane <lane> --delegated --story ST-n --reason "<the requirement it serves, what the project already has, how it fits>"
38
+ ```
39
+
40
+ Then resume the lane: SendMessage to its agent if it's still running, or dispatch the story again with "approved: <what>" in the brief.
41
+
42
+ - **Something fits better.** Drop the request with the reason, and tell the lane what to use instead:
43
+
44
+ ```bash
45
+ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" requests drop <n> --reason "<use X instead, because …>"
46
+ ```
47
+
48
+ - **It crosses a guardrail.** A package that would break a principle, change the architecture or need a spec change; a change to the spec or a contract; a product question the spec doesn't answer. Take it to the person with your recommendation, and carry on with what doesn't depend on it:
49
+
50
+ ```bash
51
+ node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" ask "<the question, in a sentence>" --story ST-n --blocks --why "<which guardrail it crosses>" --option "<choice>: <what it does>" --option "…"
52
+ ```
53
+
54
+ ## 4. The person's requests
55
+
56
+ A request `by: "person"` is a guardrail the lane hit: a spec, a contract, a design, CI, or a weakened test. Check the lane really needs it first: often the story can be done inside the guardrail, and you tell the lane how and drop the request. If it does need it, the request is already in the person's Decisions tab. Add your recommendation with `ask` when the choice isn't obvious from the request alone.
57
+
58
+ ## 5. Report
59
+
60
+ One line per request: what was asked, what you decided, and why. "ST-138: pillow approved for api (MED-5 needs HEIC decoding; nothing installed reads HEIC)." Then carry on with the build.
@@ -89,4 +89,4 @@ Give them the same brief. Claude Code opened in that worktree then acts as the l
89
89
 
90
90
  - Set each dispatched story's `**Status:**` in the plan to `in progress`, and commit it on the lead's branch.
91
91
  - Tell the person, in a few lines: what was dispatched, to which lane and branch, what's still blocked and on what, and what's ready next.
92
- - As each agent finishes, read its report. If it came back blocked on questions, bring the questions to the person. If it came back done, run the review skill on its branch.
92
+ - As each agent finishes, read its report. If it came back with a change request or a question, decide it yourself with `/code-kit:decide`, and bring to the person only what crosses a guardrail. If it came back done, run the review skill on its branch.
@@ -73,7 +73,8 @@ Write `.claude/code-kit.draft.json`. Follow `${CLAUDE_PLUGIN_ROOT}/README.md`
73
73
  - **`shell.restricted`:** commands that affect a shared local service, limited to the lanes that own it.
74
74
  - **`postEdit`:** the project's existing per-file linters and formatters, with `{file}`.
75
75
  - **`checks`:** the typecheck, test, architecture and database commands, each scoped by `files`. Only include commands that pass on the current code, or ones the person chose to keep.
76
- - **`harness.autonomy`:** ask the person how hands-off they want the build. With the code-kit mod (Claude Code 2.1.287 or later), once the plan has stories, a lead loop can keep the work moving between their prompts: it prompts the lead to dispatch ready stories, review finished ones and merge them where merges are delegated. Offer the three choices in plain words: `autonomous` (it keeps going on its own; they can pause it from the band), `propose` (each step waits in the band for their Go), or `off`. Put their answer in the draft as `"harness": { "autonomy": "…" }`. If they'd rather decide later, leave it out: the loop then asks them in the band before its first step. With `autonomous`, the lead owns delivery, so also draft `approvals.delegate`: `"merge": true` (the lead merges a reviewed branch once every check passes) and `"dependencies": {}` (it approves a new package within the default limits on licence, popularity and age), so neither comes to the person. Say these are the limits; anything outside them still comes to them as a decision.
76
+ - **`harness.autonomy`:** ask the person how hands-off they want the build. With the code-kit mod (Claude Code 2.1.287 or later), once the plan has stories, a lead loop can keep the work moving between their prompts: it prompts the lead to dispatch ready stories, review finished ones and merge them where merges are delegated. Offer the three choices in plain words: `autonomous` (it keeps going on its own; they can pause it from the band), `propose` (each step waits in the band for their Go), or `off`. Put their answer in the draft as `"harness": { "autonomy": "…" }`. If they'd rather decide later, leave it out: the loop then asks them in the band before its first step. With `autonomous`, the lead owns delivery, so also draft `"merge": true` in `approvals.delegate`: the lead merges a reviewed branch once every check passes.
77
+ - **`approvals.delegate`, who decides what (HELM-1):** the person sets the guardrails and decides what crosses them; the lead decides everything inside them, so lanes ask the lead, never the person. Draft `"dependencies": {}` (the lead approves a package only when the story's requirements need it and it fits the spec, the principles and the architecture; npm packages must also pass the default registry limits on licence, popularity and age) and `"protected"` listing every protected approval except those guarding the specs, contracts and the architecture's rules, the screens' designs (`design`), and CI. Show the person the split in two lines, what they keep and what the lead decides, and ask if they want to keep anything else. `tests` (weakening a test) is always theirs.
77
78
  - **`harness.agents.lanes.model`:** ask which model the lane agents run on. Each lane agent builds one story inside its own paths, with its requirements and acceptance criteria in its brief, and the hooks, `verify` and the lead's review catch what it gets wrong, so Sonnet usually does the work well for much less than the session's model; recommend `sonnet`, and say the lead, the reviewer and the security reviewer keep the session's model. The mod sets it as each lane agent starts; a lane's agent file with its own `model:` keeps that. Put their answer in `harness` as `"agents": { "lanes": { "model": "sonnet" } }`, or leave it out for the session's model.
78
79
  - **`harness.report.on`:** ask whether agents may draft bug reports for code-kit itself when code-kit gets in their way (a refusal that shouldn't have happened, a crash). Each report holds only code-kit's own facts (its version, the rule, what it said, the command), never the project's code, paths or data, and waits in their Decisions tab: they read it and send it to code-kit's public GitHub under their own account, or discard it. Off unless they say yes; put `"report": { "on": true }` in `harness` if they do.
79
80
  - **`approvals.lead`:** ask the person whether they'll ever approve from a phone or another chat-only client, where a `!` command arrives as plain text. If so, set it to `true`: the lead then records approvals they give in chat (`code-kit approve`), and runs the `shell.block` commands marked `person: true` when they say so. Mark a `shell.block` command `person: true` only if the person wants to be able to have the lead run it on their say-so. Leave the setting out if the lead will run unattended.
@@ -143,7 +144,7 @@ Once they approve (with any changes):
143
144
  9. **A team, only if there is one.** Not every developer works in a team. Ask once: "Do other developers work in this repository with their own Claude Code sessions?" If yes, run `/code-kit:team-init` now (it asks who the tech lead is and who owns which lanes, and writes the `team` section). If no, or not yet, leave it out and say `/code-kit:team-init` sets it up any time.
144
145
  10. Set up each companion the config now wants (`"adapters": { "<name>": true }`) by running its init skill, for example `/context-graph:init`, which asks nothing. Do it before reporting, so it's in place before the coding loop starts; `code-kit next` also gives `setup` until it is.
145
146
 
146
- Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions. Say who does what from here: the person owns the intent and the rules; the lead owns delivery, so it dispatches, reviews, sends back and merges, and approves within the delegated limits, and only a real decision (scope, something outside those limits, a problem it couldn't solve) comes to the person.
147
+ Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions. Say who does what from here: the person owns the intent and the rules; the lead owns delivery, so it dispatches, reviews, sends back and merges, and decides what lanes ask for inside the guardrails, and only what crosses a guardrail (the specs, the architecture, a design, a product question the spec doesn't answer) comes to the person.
147
148
 
148
149
  If the project has no `.ctx/` folder and Context Graph isn't installed, don't install it unless the person asks: "Context Graph can give each lane agent the why behind the files it edits (cards, rules and decisions kept in git), with this spec's requirements and layer rules in them: `/plugin marketplace add warrendeanlangeveldt/context-graph`, then `/plugin install context-graph@context-graph`."
149
150
 
@@ -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
+ | A lane waits on a request you decide | `decide`: approve it, or answer with an alternative |
29
30
  | Stories finished on their branches | `review`, then `dispatch` what's ready |
30
31
  | Design stories ready (`**Lane:** design`) | `design`, then `dispatch` what needs no design |
31
32
  | Stories ready | `dispatch` |
@@ -51,7 +52,7 @@ Run the named skill with the Skill tool: `code-kit:<step>`, with `args` (and the
51
52
  After a step finishes, run `next --json` again and carry on with the new step, without asking, as long as nothing needs the person. Stop and hand over when:
52
53
 
53
54
  - a skill asks the person questions (spec-design's rounds, spec-check's blocking questions, review's scope questions);
54
- - something needs the person's approval (init's draft, an approval for a protected path). If the config has `approvals.lead`, ask for it in chat: when they approve in their own message, record it with the `code-kit approve` command the refusal names (their words as the reason) and carry on;
55
+ - something needs the person's approval (init's draft, an approval that's theirs: `requests --json` marks it `by: "person"`). What a lane asks for that's yours (`by: "lead"`) you decide with `/code-kit:decide`, without stopping. If the config has `approvals.lead`, ask for it in chat: when they approve in their own message, record it with the `code-kit approve` command the refusal names (their words as the reason) and carry on;
55
56
  - a person has to act: merge a pull request to a protected branch, add a CI secret;
56
57
  - the step is `wait` or `done`;
57
58
  - the same step comes back twice in a row with nothing changed. Report what's stuck instead of looping.
@@ -46,7 +46,7 @@ Read the story, every requirement it cites, and the full diff (`git diff <base>.
46
46
  - **Tested:** a test proves each acceptance criterion and names the requirement ID. The test would fail if the behaviour were broken; one that asserts nothing, or only asserts a mock, doesn't count.
47
47
  - **Its journeys still pass end to end:** where the project has a QA lane, run the end-to-end suites that cover this story's requirements (the qa lane's command, filtered to them) against the branch. A journey that broke fails the review, even when the story's own tests pass. A story the QA lane delivered is reviewed the same way: each journey's test proves the requirements it cites, through the real screens and services, with no mocked responses.
48
48
  - **The spec-check report matches:** its table has no row still `missing` or `conflicts` without an answer, and its questions were answered, not guessed.
49
- - **Only the story:** changes beyond the story (refactors elsewhere, other stories' requirements, new dependencies) are called out. They may be fine, but they're the person's call.
49
+ - **Only the story:** changes beyond the story (refactors elsewhere, other stories' requirements) are sent back, or split into their own story. A new dependency is yours to judge, as in `/code-kit:decide`: the story's requirements need it, nothing the project has does the job, and it fits the principles and the layer. Only what crosses a guardrail (the spec, a contract, the architecture) is the person's call.
50
50
  - **Contracts respected:** the code uses the contracts as written. A lane that needed a contract change should have asked, not worked around it.
51
51
 
52
52
  ## 4. The security review
@@ -22,8 +22,8 @@ Always:
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.
25
- - Something only the person can decide (a product choice, wording, an account): write the question for the lead, with the choices and what each does, and carry on with what doesn't depend on it, or stop. The lead asks the person.
26
- - A new dependency needs a person's approval for your lane. If the install is refused, put the package, why it's needed and the approval command from the refusal in a change request for the lead, and stop.
25
+ - You answer to the lead, never the person. A question the spec doesn't settle (a product choice, UX the spec is silent on, an account): write it for the lead, with the choices and what each does, and carry on with what doesn't depend on it, or stop. The lead decides, or asks the person.
26
+ - A new dependency needs the lead's approval. Before asking, check the project doesn't already have something that does the job. If the install is refused, put in a change request for the lead: the package, the story and requirement that need it, what you checked, and anything lighter that would do; then stop. The lead approves it, or tells you what to use instead.
27
27
  - Build every feature end to end. No mocks, stubs, placeholder screens or "coming soon".
28
28
  - Prove each acceptance criterion with a test whose title names the requirement ID, e.g. `BOOK-4 …`.
29
29
  - Fixing a bug: first write a test that fails because of the bug, through the code that's already there, then fix it. `verify` runs your test without the fix, and it must fail on an assertion, not because it calls something the fix adds.
@@ -6,17 +6,18 @@ The code-kit plugin enforces the rules in `.claude/code-kit.json` on every file
6
6
  - the layer rules ({{layer names, in order}});
7
7
  - spec-check before lane code{{; designs approved before screens}};
8
8
  - an audit log of every change to protected paths (`.claude/approval-log.jsonl`), which the person reviews in the pull request;
9
- - a person's approval for every new dependency, per package and per lane. When a lane asks for one, give the person the package, the reason and the approval command from the refusal, and resume the lane once they've run it;
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
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;}}
11
11
  - the project's checks before any agent finishes.
12
12
 
13
- When something needs the person to decide or do (a product choice, an account only they can create, wording only they can 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.
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
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.}}
15
15
 
16
16
  If a hook blocks you, fix the cause; never work around it.
17
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
19
 
20
+ - `/code-kit:decide` decides what a lane asked for, on its merits;
20
21
  - `/code-kit:dispatch` starts ready stories on their lanes, each on its own `<lane>/st-<n>` branch;
21
22
  - `/code-kit:review` checks a finished branch against its requirements before the lead merges it;
22
23
  - `/code-kit:status` shows what's done, in review, ready and blocked, and what's untested;