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