@warren-dean/code-kit 0.15.1 → 0.16.1
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 +19 -0
- package/README.md +20 -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 +80 -9
- package/hooks/mod/register.mjs +85 -32
- package/hooks/mod/register.test.ts +110 -19
- package/hooks/mod/view.mjs +35 -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.1",
|
|
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.1",
|
|
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,25 @@
|
|
|
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.1 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
The lead loop stops asking for stories that are waiting on other work.
|
|
8
|
+
|
|
9
|
+
- When the lead says a story isn't due yet because it waits on work still going, such as a paired story a lane is building, a rebuild or a check running, the loop parks that story. A parked story is left out of every step, so a step that names it with others prompts the lead only for the others. Before, the loop held a declined step only while the step stayed exactly the same, so when one more story became ready, or any lane committed, it prompted the same reviews again.
|
|
10
|
+
- A parked story wakes when what it waits on moves: its own branch or state, the stories the lead named, or the unfinished stories in the plan that depend on it. Other lanes' commits don't wake it. After 30 minutes with nothing moving, the lead is asked once more.
|
|
11
|
+
- The band's next row says what each parked story waits on. A step the lead declined for another reason no longer holds up every other step.
|
|
12
|
+
|
|
13
|
+
## 0.16.0 (2026-10-16)
|
|
14
|
+
|
|
15
|
+
The lead runs the team inside your guardrails, and lanes ask the lead, never you.
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- `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.
|
|
21
|
+
- The mod no longer approves calls within the delegated rules by itself. Every approval the lead gives now comes with its reasons.
|
|
22
|
+
- 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.
|
|
23
|
+
|
|
5
24
|
## 0.15.1 (2026-10-16)
|
|
6
25
|
|
|
7
26
|
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.
|
|
@@ -332,6 +332,7 @@ The loop keeps the build moving between your prompts, under `harness.autonomy`.
|
|
|
332
332
|
|
|
333
333
|
- **Each step.** Once the lead has finished a turn, with the lead idle and nothing typed in the prompt, the loop asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own. It never prompts the same step twice while nothing has changed, and never a story an agent is still on: one its lane agent is building, or a reviewer is reviewing, waits until that agent finishes ("waiting: ST-10 is still with its agent").
|
|
334
334
|
- **It reads the lead's reply first.** Before prompting again, the loop reads the lead's last reply with a side call over the lead's own conversation. A reply that waits on you (a question, a design to look at, an approval), or a turn you stopped, holds the loop until you answer or speak to the lead: the band says what the lead waits on, and its question is asked in Claude Code's own question dialog. A step the lead didn't take waits in the band with its reason and **Ask again**.
|
|
335
|
+
- **Stories waiting on other work.** When the lead says a story isn't due yet because it waits on work still going (a paired story a lane is building, a rebuild, a check running), the loop parks that story and leaves it out of every step, so a step naming it with others prompts only the others. It wakes when what it waits on moves: its own branch, the stories the lead named, or the unfinished stories that depend on it. Other lanes' commits don't wake it, and after 30 minutes with nothing moving the lead is asked once more.
|
|
335
336
|
- **A step that doesn't move.** One the lead was prompted with twice in a row that is still next goes to you instead: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Ask again**.
|
|
336
337
|
- **Designs.** A design story is handed to the lead once a session; while you and the lead design it, the loop leaves it to you.
|
|
337
338
|
- **One session runs it.** The first session whose loop acts runs it for the project. Another session you open there, for any reason, never prompts its lead: its band reads "the loop runs in another session", with **Lead from here** to move the loop to it.
|
|
@@ -373,11 +374,13 @@ Once init has switched the rules on, the lead runs the build in a loop:
|
|
|
373
374
|
|
|
374
375
|
```text
|
|
375
376
|
/code-kit:dispatch next # start every ready story on its lane
|
|
377
|
+
/code-kit:decide # decide what lanes asked for: approve, offer something better, or bring it to you
|
|
376
378
|
/code-kit:review ST-12 # check a finished branch, then merge or send it back
|
|
377
379
|
/code-kit:status # where the build stands against the spec
|
|
378
380
|
```
|
|
379
381
|
|
|
380
382
|
- **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.
|
|
383
|
+
- **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
384
|
- **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
385
|
- **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
386
|
|
|
@@ -430,7 +433,7 @@ code-kit status [--base ref] [--json] # stories and requirements against the sp
|
|
|
430
433
|
code-kit next [--base ref] [--json] # the step to take now, from the project's state
|
|
431
434
|
code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
|
|
432
435
|
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
|
|
436
|
+
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
437
|
code-kit requests drop <n>[,<n>…]|all [--reason "…"] # stop waiting on requests nobody needs answered (the lead, or Not needed in the pane)
|
|
435
438
|
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
439
|
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 +467,7 @@ code-kit map <story>|project [--json] # a story'
|
|
|
464
467
|
code-kit trail [--json] # each requirement to its stories, the code they changed and the tests that name it
|
|
465
468
|
code-kit adapters [--json --session id] # the adapters in use; with --json, what they know of each lane's work in a session
|
|
466
469
|
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)
|
|
470
|
+
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
471
|
```
|
|
469
472
|
|
|
470
473
|
`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 +486,7 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
|
|
|
483
486
|
| `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
|
|
484
487
|
| `anyActor` | no | Paths every writer may change (e.g. the lockfile), read-only agents included. `.claude/state/**` is always included. An entry may be `{ "glob", "reviewer" }` to name the lane (or `"lead"`) that reviews changes to it; otherwise the lead reviews them, and `who` and `trace` say so. |
|
|
485
488
|
| `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude?, owners? }`: the agent name maps a subagent to its lane; `owners` (GitHub handles) are the people whose sessions work it, in a team (needs `team`). A worktree with a `.lane` file holding the lane name makes the main session that lane. |
|
|
486
|
-
| `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs a person's
|
|
489
|
+
| `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs an approval: the lead's where `approvals.delegate.protected` names it, otherwise a person's (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
|
|
487
490
|
| `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them. Init sets it for every lane that owns screens; `/code-kit:design` adds approved designs |
|
|
488
491
|
| `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
|
|
489
492
|
| `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
|
|
@@ -495,7 +498,7 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
|
|
|
495
498
|
| `checks[]` | no | `{ name, files, exclude?, run[], each?, owners?, ifMissing?, timeoutSeconds? }`: before finishing, when matching files changed. `each` runs the commands in every changed folder matching that glob. `owners` limits who may change the files (the lead always may). `ifMissing: "fail"` makes a missing tool an error (the default is to skip the check, which `verify` and a merge list as not run). |
|
|
496
499
|
| `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected; `true` says the project wants that tool, so until it's set up here `next` gives its setup before any code. See [Adapters](#adapters). |
|
|
497
500
|
| `approvals.lead` | no | `true` lets the lead act on what the person says in chat, for when `!` isn't available (on a phone, say): it records their approvals with `code-kit approve`, and runs `shell.block` commands marked `person`. Default `false`. See [Approvals](#approvals). |
|
|
498
|
-
| `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`:
|
|
501
|
+
| `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: what the lead decides, inside the guardrails you keep: the protected approvals it grants, and packages it approves for a story, with `code-kit approve --delegated`; with `merge`, merges into a protected branch with `code-kit merge --delegated`. Lanes ask the lead for these, never you. npm packages are also checked against the registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
|
|
499
502
|
| `setup` | no | The version of init's setup the project has (3 now). Without it, or lower, `next` names what the project is missing (the design gate, design stories before screens, a QA lane, a security reviewer) and starts no new story until re-running init adds it |
|
|
500
503
|
| `team` | no | Preview. For several people in one repository: `{ techLead, staleDays?, intents?, board?, provider? }`. `techLead` is the GitHub handle of the person who owns the plan, specs and contracts; `staleDays` is how many working days without a push make a claim stale (default 3); `intents` is the discussion category intents go in (default `Intents`; create it once in the repository's Settings › Discussions); `board` is `{ owner, number }`, a GitHub Project the plan is mirrored onto; `provider` is `"github"` (default) or `{ command, fallback? }` for another team provider, such as the code-kit teams hub; with `fallback: "github"`, claims go through GitHub whenever that provider can't be reached. See docs/specs/16-teams.md. |
|
|
501
504
|
| `branches.protected` | no | Default `["main", "master"]` |
|
|
@@ -536,27 +539,29 @@ code-kit approve dep-dayjs --lane web --reason "Yes, add dayjs for ST-12"
|
|
|
536
539
|
|
|
537
540
|
### Requests
|
|
538
541
|
|
|
539
|
-
Every refusal that
|
|
542
|
+
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
543
|
|
|
541
544
|
`--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
545
|
|
|
543
|
-
###
|
|
546
|
+
### Who decides what
|
|
544
547
|
|
|
545
|
-
|
|
548
|
+
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
549
|
|
|
547
550
|
```json
|
|
548
|
-
"approvals": { "delegate": { "protected": ["
|
|
551
|
+
"approvals": { "delegate": { "protected": ["schema", "truth"], "dependencies": {} } }
|
|
549
552
|
```
|
|
550
553
|
|
|
554
|
+
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.
|
|
555
|
+
|
|
551
556
|
```bash
|
|
552
|
-
code-kit approve
|
|
557
|
+
code-kit approve pillow --lane api --delegated --story ST-138 --reason "MED-5 needs HEIC decoding; nothing installed reads HEIC"
|
|
553
558
|
```
|
|
554
559
|
|
|
555
560
|
- **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
|
|
561
|
+
- **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.
|
|
562
|
+
- **All or nothing:** if any name is outside the rules, nothing is approved, and the command says which rule each one broke.
|
|
558
563
|
- **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.
|
|
564
|
+
- **Who:** only the lead. Without `approvals.delegate`, `--delegated` is refused, and every approval is yours.
|
|
560
565
|
- **Merges:** with `"merge": true`, the lead merges a reviewed branch into a protected branch with `code-kit merge <branch> --delegated`.
|
|
561
566
|
- 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
567
|
- 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
|
|