@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +19 -15
- package/bin/code-kit.mjs +39 -8
- package/hooks/guard-bash.mjs +21 -19
- package/hooks/lib/dependencies.mjs +21 -3
- package/hooks/lib/next.mjs +11 -1
- package/hooks/lib/rules.mjs +51 -4
- package/hooks/mod/decisions.mjs +1 -20
- package/hooks/mod/hold.mjs +2 -0
- package/hooks/mod/loop.mjs +7 -0
- package/hooks/mod/register.mjs +8 -28
- package/hooks/mod/register.test.ts +33 -19
- package/hooks/mod/view.mjs +33 -19
- package/package.json +1 -1
- package/skills/decide/SKILL.md +60 -0
- package/skills/dispatch/SKILL.md +1 -1
- package/skills/init/SKILL.md +3 -2
- package/skills/next/SKILL.md +2 -1
- package/skills/review/SKILL.md +1 -1
- package/templates/agent.md +2 -2
- package/templates/claude-md.md +3 -2
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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)
|
|
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
|
|
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] } }`:
|
|
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
|
|
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
|
-
###
|
|
545
|
+
### Who decides what
|
|
544
546
|
|
|
545
|
-
|
|
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": ["
|
|
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
|
|
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:**
|
|
557
|
-
- **All or nothing:** if any name is outside the rules, nothing is approved, and the command says which rule each one broke
|
|
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.
|
|
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
|
-
|
|
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
|
|
997
|
+
open.forEach((r) =>
|
|
992
998
|
out(
|
|
993
|
-
` ${
|
|
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
|
-
|
|
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
|
|
1384
|
-
|
|
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'})`,
|
package/hooks/guard-bash.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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(', ')})
|
|
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.
|
|
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.
|
|
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(...
|
|
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
|
|
135
|
+
return found;
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
const SHELLS = new Set(['sh', 'bash', 'zsh']);
|
package/hooks/lib/next.mjs
CHANGED
|
@@ -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.
|
package/hooks/lib/rules.mjs
CHANGED
|
@@ -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 --
|
|
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
|
-
|
|
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);
|
package/hooks/mod/decisions.mjs
CHANGED
|
@@ -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
|
-
|
|
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 ?? [];
|
package/hooks/mod/hold.mjs
CHANGED
|
@@ -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
|
|
package/hooks/mod/loop.mjs
CHANGED
|
@@ -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',
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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([]);
|
package/hooks/mod/view.mjs
CHANGED
|
@@ -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
|
-
|
|
1368
|
-
? {
|
|
1369
|
-
|
|
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: '
|
|
1394
|
+
todo: todoFor('for 60 minutes'),
|
|
1387
1395
|
request: request(`write ${m[1]}`),
|
|
1388
1396
|
raw: text,
|
|
1389
1397
|
};
|
|
1390
|
-
m = first.match(
|
|
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: '
|
|
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: '
|
|
1415
|
+
todo: todoFor('for 60 minutes'),
|
|
1406
1416
|
request: request('change the .claude kit'),
|
|
1407
1417
|
raw: text,
|
|
1408
1418
|
};
|
|
1409
|
-
m = first.match(
|
|
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:
|
|
1415
|
-
|
|
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.
|
|
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.
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
package/skills/init/SKILL.md
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
|
package/skills/next/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
package/skills/review/SKILL.md
CHANGED
|
@@ -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,
|
|
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
|
package/templates/agent.md
CHANGED
|
@@ -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
|
-
-
|
|
26
|
-
- A new dependency needs
|
|
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.
|
package/templates/claude-md.md
CHANGED
|
@@ -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
|
-
-
|
|
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,
|
|
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;
|