@warren-dean/code-kit 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "name": "code-kit",
10
10
  "source": "./",
11
11
  "description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
12
- "version": "0.4.1",
12
+ "version": "0.6.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.4.1",
3
+ "version": "0.6.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,37 @@
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.6.0 (2026-10-10)
6
+
7
+ The first parts of the harness, in the mod (Claude Code 2.1.287 or later):
8
+
9
+ - **Harness settings.** A new `harness` section in `.claude/code-kit.json` holds how the lead loop acts (`autonomy`), how long a call waiting on your approval is held, when a stalled lane is nudged and restarted, the reviewer agent and its model, and the plan use at which background agents pause. `code-kit check` validates it; a wrong value gives way to its default.
10
+ - **`code-kit settings`** lists the settings in force; `settings set <key> <value> --reason "…"` changes one, as the person, and logs it as a `kit` approval. Agents are refused.
11
+ - **`/harness`** opens the settings in the session. A change asks your reason before anything is written, and the approval log records it as made in the pane.
12
+ - **Usage per lane.** The mod counts each model request's tokens against the agent that made it, its story and its lane. The Lanes pane shows tokens per lane and story, the background agents' share, and the plan's 5-hour use as a bar.
13
+ - **Outliers in the band:** a story using more than three times the median of the stories finished this session (once three have) is flagged, as "ST-7 has used 3.4× the usual". Nothing is stopped.
14
+ - **The background pause:** the band says when the plan's 5-hour use passes `harness.background.pauseAtPercent` (80% unless set). The background agents that will honour it come in a later release.
15
+ - **Refusals your approval would allow** (new packages, protected files, kit edits) are recognised, including a commit refused for a protected file, ready for the hold that comes next.
16
+ - `code-kit check --json` carries the harness settings in force.
17
+
18
+ ## 0.5.0 (2026-10-07)
19
+
20
+ Fixes for gaps found running code-kit under an autonomous lead:
21
+
22
+ - **The Bash guard recognises commands where they run.** A commit message, a heredoc, a quoted `--text` or a grep pattern that mentions `git commit`, `code-kit merge` or switching to `main` no longer trips its rule. Text a command carries, such as a commit trailer or an approval path, is still read whole.
23
+ - **A dependency approval lasts until its install is committed,** at most 7 days, instead of 60 minutes, so a story that resumes later isn't refused again. The commit that adds the package uses it up. Other approvals still last 60 minutes.
24
+ - **status and next after a send-back:**
25
+ - `code-kit sent-back <branch>` records the commit a review sent back, and the review skill runs it;
26
+ - the story reads as "sent back", and `next` proposes the lane's fix instead of another review, until the branch moves on;
27
+ - a story whose dependency isn't done is blocked on it, even with commits on its branch.
28
+ - **verify holds a `lead/…` branch to the lead's paths** for what its own commits change. Lane work merged in after review isn't counted against it.
29
+ - **A lane merging its base** no longer needs fresh approvals for what the base already logged, and the ownership audit doesn't ask it to revert the base's changes.
30
+ - **A delegated merge follows the target branch's config,** so a branch can't grant itself a merge.
31
+ - **init's update keeps project skills in step with the docs' recipes:** a new skill per new recipe, updates, removals, and each skill listed on every lane agent whose paths it touches.
32
+ - **verify scans the branch's whole history for secrets:** a secret file committed and deleted again still fails, and gitleaks scans every commit when it's installed.
33
+ - **Install scripts are told apart** in the delegated dependency rule: `allowInstallScripts` takes `true`, `false` or the kinds allowed: `binary` (prebuilt binaries, as esbuild and sharp fetch), `build` or `other`.
34
+ - **Files any agent may write have a reviewer:** the lead, or a lane named in `{ "glob", "reviewer" }`. `who` and `trace` name it.
35
+
5
36
  ## 0.4.1 (2026-10-07)
6
37
 
7
38
  - **The band keeps other plugins' lines:** code-kit's lines go above whatever else the band holds, such as Context Graph's "N proposals to ratify", instead of replacing it.
package/README.md CHANGED
@@ -65,7 +65,7 @@ code-kit stops each of these at the moment it would happen. The hook refuses the
65
65
  - a `<lane>/…` branch stays in that lane's paths, apart from manifest and lockfile changes logged under a dependency approval;
66
66
  - every protected change is in the approval log, and the log is append-only;
67
67
  - no new layer violations, and the baseline doesn't grow;
68
- - no screens without an approved design, and no committed secrets;
68
+ - no screens without an approved design, and no secrets anywhere in the branch's history, including ones committed and deleted again;
69
69
  - the project's checks pass.
70
70
 
71
71
  A project without `.claude/code-kit.json` isn't governed at all. An invalid config fails closed: nothing but the config itself can be written until it's fixed.
@@ -107,7 +107,7 @@ Set it up at the start of a new build, straight from the architecture docs, or b
107
107
 
108
108
  ## Install
109
109
 
110
- **Requirements:** Claude Code, Node 22 or later, and git. [gitleaks](https://github.com/gitleaks/gitleaks) is optional: when it's installed, every commit gets a secret scan.
110
+ **Requirements:** Claude Code, Node 22 or later, and git. [gitleaks](https://github.com/gitleaks/gitleaks) is optional: when it's installed, every commit gets a secret scan, and `verify` scans every commit on the branch.
111
111
 
112
112
  **The plugin**, from this repository's marketplace:
113
113
 
@@ -269,15 +269,18 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
269
269
  - **The band** sits above the prompt whenever something waits for you, and is gone when nothing does. It shows:
270
270
  - approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite;
271
271
  - stories finished and waiting for review: **Review** asks the lead to run `/code-kit:review` on the branch, and **Merge** runs `verify`, checks included, then merges if it passes;
272
- - a finish check failing in this session: **Lanes** opens the pane.
272
+ - a finish check failing in this session: **Lanes** opens the pane;
273
+ - a story using more than three times the median of the stories finished this session (once three have finished): **Lanes** opens the pane;
274
+ - background agents paused, once the plan’s 5-hour use passes `harness.background.pauseAtPercent` (80% unless set).
273
275
 
274
276
  Each button has a digit: type it in an empty prompt to press it.
275
277
 
276
- - **`/lanes`** opens the Lanes pane: each lane with its agent, story, branch and state (building, in review, blocked, idle), a mark while its agent is at work, and the stories ready to start. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
278
+ - **`/lanes`** opens the Lanes pane: each lane with its agent, story, branch and state (building, in review, blocked, idle), a mark while its agent is at work, and the stories ready to start. Beneath them, the session’s usage: tokens per lane and story, the background agents’ share, and the plan’s 5-hour use as a bar. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
277
279
  - **Refusals** a code-kit hook gives an agent are drawn as cards: what was refused, the rule, what to do, and Approve… where your approval would allow it. The full text is a press away.
280
+ - **`/harness`** opens the harness settings: how the lead loop acts, how long a call waiting on your approval is held, when a stalled lane is nudged and restarted, the reviewer agent and its model, and the use at which background agents pause. Changing one asks your reason, writes `.claude/code-kit.json` and logs a `kit` approval. Lanes, paths and rules still change through `/code-kit:init`.
278
281
  - **`/approvals`** lists the requests waiting and the approvals in force, with minutes left. **`/verify-branch`** runs `code-kit verify` on this branch. Neither calls the model.
279
282
 
280
- Approving from the band records `(approved in the code-kit pane)` with your reason, and a merge from it says it was merged by the person from the pane. Both happen only on your press: the hooks refuse `approve --via pane` and `merge --person` from every agent. Outside a project with `.claude/code-kit.json`, the mod draws nothing. Mods don't draw in the VS Code extension's chat panel, the Agent SDK or `claude -p`.
283
+ Approving from the band records `(approved in the code-kit pane)` with your reason, and a merge from it says it was merged by the person from the pane. A settings change is logged as made in the pane. These happen only on your press: the hooks refuse `approve --via pane`, `merge --person` and `settings set` from every agent. Outside a project with `.claude/code-kit.json`, the mod draws nothing. Mods don't draw in the VS Code extension's chat panel, the Agent SDK or `claude -p`.
281
284
 
282
285
  ## Build
283
286
 
@@ -330,6 +333,9 @@ code-kit trace <path>... [--json] # what a file is for: requirements, lane,
330
333
  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
331
334
  code-kit requests [--json] # approval requests the hooks refused, still open, and the approvals in force
332
335
  code-kit stops [--session id] [--json] # finish checks that refused an agent's last stop and still fail
336
+ code-kit sent-back <branch> [--reason "…"] # record a review's send-back: status shows the story as sent back until the branch moves on
337
+ code-kit settings [--json] # the harness settings in force, with their defaults
338
+ code-kit settings set <key> <value> --reason "…" # change one, as the person: written to the config and logged as a kit approval
333
339
  code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pane] # record a person's approval from chat, or (--delegated) grant one within the delegated rules; a package name stands for its dep- approval
334
340
  ```
335
341
 
@@ -339,27 +345,28 @@ code-kit approve <name>... --reason "…" [--lane name] [--delegated | --via pan
339
345
 
340
346
  `.claude/code-kit.json`. Globs use `*` (one segment), `**` (any depth), `**/` (zero or more folders), `?` and `{a,b}`. Patterns under `shell` are regular expressions.
341
347
 
342
- | Field | Required | What it is |
343
- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
344
- | `version` | yes | `1` |
345
- | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
346
- | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
347
- | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
348
- | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
349
- | `anyActor` | no | Paths every writer may change (e.g. the lockfile). `.claude/state/**` is always included. |
350
- | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude? }`: the agent name maps a subagent to its lane. A worktree with a `.lane` file holding the lane name makes the main session that lane. |
351
- | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs a person's approval (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
352
- | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them |
353
- | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
354
- | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
355
- | `shell.block[]` | no | `{ pattern, why, person? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on |
356
- | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
357
- | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
358
- | `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). |
359
- | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected. See [Adapters](#adapters). |
360
- | `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). |
361
- | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts? } }`: approvals the lead may grant on its own, with `code-kit approve --delegated`, and with `merge`, merges into a protected branch with `code-kit merge --delegated`. Dependencies are checked against the npm registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
362
- | `branches.protected` | no | Default `["main", "master"]` |
348
+ | Field | Required | What it is |
349
+ | ------------------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
350
+ | `version` | yes | `1` |
351
+ | `docs.specs` | no | Folder of specs. With it, a lane needs a spec-check report before writing code on a branch |
352
+ | `docs.principles`, `docs.plan` | no | The engineering principles and the implementation plan |
353
+ | `lead.paths` | yes | What the main session writes. `.claude/**` is always added. |
354
+ | `lead.outside` | no | Paths outside the repository the lead may write, e.g. `../shared-lib/**`. Claude's memory and scratchpad are always included. |
355
+ | `anyActor` | no | Paths every writer may change (e.g. the lockfile). `.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. |
356
+ | `lanes.<name>` | yes (may be `{}`) | `{ agent, paths, exclude? }`: the agent name maps a subagent to its lane. A worktree with a `.lane` file holding the lane name makes the main session that lane. |
357
+ | `protected[]` | no | `{ glob, approval, why }`: logged on every commit. Anyone but the lead needs a person's approval (see [Approvals](#approvals)). `.claude/**` is always protected as `kit`. |
358
+ | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them |
359
+ | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
360
+ | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
361
+ | `shell.block[]` | no | `{ pattern, why, person? }`: never run. `person: true` marks a person's act, which the lead may do for them with `approvals.lead` on |
362
+ | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
363
+ | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
364
+ | `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). |
365
+ | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected. See [Adapters](#adapters). |
366
+ | `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). |
367
+ | `approvals.delegate` | no | `{ protected?: [approval names], merge?: true, dependencies?: { licences?, minWeeklyDownloads?, maxMonthsSinceRelease?, allowInstallScripts?: true \| false \| [kinds] } }`: approvals the lead may grant on its own, with `code-kit approve --delegated`, and with `merge`, merges into a protected branch with `code-kit merge --delegated`. Dependencies are checked against the npm registry; the defaults are MIT, Apache-2.0, BSD and ISC, 10,000 weekly downloads, a release within 18 months, and no install scripts. See [Approvals](#approvals). |
368
+ | `branches.protected` | no | Default `["main", "master"]` |
369
+ | `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 30; 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), and `background.pauseAtPercent` (default 80). Change them with `/harness` or `code-kit settings set`. |
363
370
 
364
371
  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.
365
372
 
@@ -376,7 +383,7 @@ echo "Date formatting for ST-12" > .claude/approvals/web/dep-dayjs #
376
383
  - **Scope:** an approval in a lane's folder serves that lane only; one directly in `.claude/approvals/` serves every agent. The refusal a lane gets names its own folder.
377
384
  - **Dependencies:** each new package needs its own approval, `dep-<package>` (`/` becomes `+`, so `@scope/pkg` is `dep-@scope+pkg`). It allows the install, and the changes the install makes to manifests and lockfiles that no other lane owns. When the lane commits a manifest or lockfile outside its own paths, the commit hook logs it as a `dependency` approval, with the packages and reasons, and `code-kit verify` accepts it.
378
385
  - **Worktrees:** approvals live in the main checkout, so a lane working in its own worktree sees the ones given at the lead's terminal.
379
- - **Lifetime:** 60 minutes. Agents can't create, edit or even mention approvals or the log.
386
+ - **Lifetime:** 60 minutes; a dependency approval (`dep-…`) lasts until the lane commits the install it allows, at most 7 days, since a story may resume hours later. Agents can't create, edit or even mention approvals or the log.
380
387
 
381
388
  ### From chat
382
389
 
@@ -411,7 +418,7 @@ code-kit approve date-fns --lane web --delegated --reason "Date formatting for S
411
418
  ```
412
419
 
413
420
  - **Protected approvals:** the lead may grant those named in `protected`, and no others.
414
- - **Dependencies:** each package is checked against the npm registry. Its licence must be in `licences`; it must have at least `minWeeklyDownloads` downloads last week and a release within `maxMonthsSinceRelease` months; and installing it must run no scripts, unless `allowInstallScripts` is true. Packages from other ecosystems, and anything the registry can't confirm, go to the person.
421
+ - **Dependencies:** each package is checked against the npm registry. Its licence must be in `licences`; it must have at least `minWeeklyDownloads` downloads last week and a release within `maxMonthsSinceRelease` months; and installing it must run no scripts, unless `allowInstallScripts` allows them: `true` for any, or a list of the kinds allowed, such as `["binary"]` for packages that only download a prebuilt binary for the platform (esbuild, sharp). The kinds are `binary`, `build` (compiles native code with node-gyp) and `other` (runs the package's own code). Packages from other ecosystems, and anything the registry can't confirm, go to the person.
415
422
  - **All or nothing:** if any name is outside the rules, nothing is approved, and the command says which rule each one broke, so the lead asks the person.
416
423
  - **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.
417
424
  - **Who:** only the lead. A lane asks the lead with a change request, and its refusals say the lead may approve within the rules. Without `approvals.delegate`, `--delegated` is refused.
package/bin/code-kit.mjs CHANGED
@@ -19,6 +19,9 @@
19
19
  // (approvals.delegate.merge) or the person (the code-kit pane)
20
20
  // requests [--json] open approval requests and approvals in force
21
21
  // stops [--session id] [--json] finish checks that refused an agent's last stop, still failing
22
+ // sent-back <branch> [--reason …] records that a review sent the branch back at its current commit
23
+ // settings [--json] | settings set <key> <value> --reason … [--via pane]
24
+ // the harness settings; set is the person's own change
22
25
  // record approvals a person gave in chat (with approvals.lead on, the lead runs it)
23
26
  // --config <file> reads a draft (.claude/code-kit.draft.json) instead of .claude/code-kit.json.
24
27
  import { execFileSync } from 'node:child_process';
@@ -34,8 +37,11 @@ import { ADAPTERS, activeAdapters } from '../hooks/lib/adapters/index.mjs';
34
37
  import { nextStep } from '../hooks/lib/next.mjs';
35
38
  import { traceFile } from '../hooks/lib/trace.mjs';
36
39
  import { buildStatus } from '../hooks/lib/plan.mjs';
40
+ import { recordSentBack } from '../hooks/lib/reviews.mjs';
41
+ import { SETTINGS, parseSetting, settingOf, withSetting } from '../hooks/lib/harness.mjs';
37
42
  import {
38
43
  APPROVAL_MINUTES,
44
+ approvalLifetime,
39
45
  currentBranch,
40
46
  PANE,
41
47
  approvalsInForce,
@@ -43,10 +49,11 @@ import {
43
49
  grantApprovals,
44
50
  openRequests,
45
51
  ownerOf,
52
+ reviewerOf,
46
53
  } from '../hooks/lib/rules.mjs';
47
54
  import { DEPENDENCY_APPROVAL, dependencyApproval } from '../hooks/lib/dependencies.mjs';
48
55
  import { dependencyRuleProblems, npmFacts, packageOf } from '../hooks/lib/registry.mjs';
49
- import { laneOfBranch, verifyBranch } from '../hooks/lib/verify.mjs';
56
+ import { heldTo, laneOfBranch, verifyBranch } from '../hooks/lib/verify.mjs';
50
57
 
51
58
  const out = (s) => process.stdout.write(`${s}\n`);
52
59
  // Piped into `head` and the like: stop quietly when the reader closes.
@@ -115,6 +122,7 @@ function checkJson() {
115
122
  );
116
123
  report.adapters = config.adapters;
117
124
  report.docs = config.docs ?? null;
125
+ report.harness = config.harness;
118
126
  out(JSON.stringify(report, null, 2));
119
127
  }
120
128
 
@@ -135,7 +143,13 @@ function who(paths) {
135
143
  const { config } = load();
136
144
  for (const p of paths) {
137
145
  const layer = layerOf(p, config.layers);
138
- out(`${p}: ${ownerOf(p, config) ?? 'nobody'}${layer ? ` · layer ${layer.name}` : ''}`);
146
+ const reviewer = reviewerOf(p, config);
147
+ const whose =
148
+ ownerOf(p, config) ??
149
+ (reviewer
150
+ ? `any agent may write it; ${reviewer === 'lead' ? 'the lead' : `the ${reviewer} lane`} reviews it`
151
+ : 'nobody');
152
+ out(`${p}: ${whose}${layer ? ` · layer ${layer.name}` : ''}`);
139
153
  }
140
154
  }
141
155
 
@@ -288,6 +302,27 @@ const VERIFY_GROUPS = {
288
302
  // merges, and only when `verify` passes on that branch, checks included. A person merges with git.
289
303
  // With --person, the person merges (the mod passes it for a press in its pane): the same verify first,
290
304
  // with no delegation needed. The hooks refuse --person from every agent.
305
+ /**
306
+ * The config as committed on `ref`: what governs merging into it, so a branch's own config can't grant
307
+ * itself a merge its target doesn't allow.
308
+ */
309
+ function configAt(ref) {
310
+ let raw;
311
+ try {
312
+ raw = JSON.parse(
313
+ execFileSync('git', ['show', `${ref}:${file}`], {
314
+ encoding: 'utf8',
315
+ stdio: ['ignore', 'pipe', 'ignore'],
316
+ }),
317
+ );
318
+ } catch {
319
+ die(`${ref} has no readable ${file}; a merge into it follows the rules committed there.`);
320
+ }
321
+ const problems = validate(raw);
322
+ if (problems.length) die(`${file} on ${ref} is invalid:\n ${problems.join('\n ')}`);
323
+ return effectiveConfig(raw, '.');
324
+ }
325
+
291
326
  function merge(branch) {
292
327
  const { config } = load();
293
328
  const byPerson = flag('--person');
@@ -297,14 +332,16 @@ function merge(branch) {
297
332
  );
298
333
  if (flag('--delegated') && byPerson)
299
334
  die("A merge is either the lead's (--delegated) or the person's (--person), not both.");
300
- if (!byPerson && !config.approvals.delegate?.merge)
335
+ const into = intoName ?? config.branches.protected[0];
336
+ // Whether the lead may merge, and which branches are protected, are the target's rules.
337
+ const target = configAt(into);
338
+ if (!byPerson && !target.approvals.delegate?.merge)
301
339
  die(
302
- 'This project doesn\'t delegate merges to the lead ("approvals.delegate.merge" isn\'t true). A person merges after review.',
340
+ `${into} doesn't delegate merges to the lead ("approvals.delegate.merge" isn't true in its ${file}). A person merges after review.`,
303
341
  );
304
- const into = intoName ?? config.branches.protected[0];
305
- if (!config.branches.protected.includes(into))
342
+ if (!target.branches.protected.includes(into))
306
343
  die(`${into} isn't a protected branch; merge into it with git.`);
307
- if (config.branches.protected.includes(branch)) die(`${branch} is itself a protected branch.`);
344
+ if (target.branches.protected.includes(branch)) die(`${branch} is itself a protected branch.`);
308
345
  const root = resolve('.');
309
346
  const g = (cwd, ...a) =>
310
347
  execFileSync('git', ['-C', cwd, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
@@ -374,7 +411,7 @@ function merge(branch) {
374
411
  '-m',
375
412
  `Merge ${branch} into ${into}`,
376
413
  '-m',
377
- `Verified with code-kit verify (${changed.length} file(s)${lane ? `, held to the ${lane} lane` : ''}) and merged ${byPerson ? 'by the person, from the code-kit pane' : 'by the lead within the delegated rules'}.`,
414
+ `Verified with code-kit verify (${changed.length} file(s)${lane ? `, held to ${heldTo(lane)}` : ''}) and merged ${byPerson ? 'by the person, from the code-kit pane' : 'by the lead within the delegated rules'}.`,
378
415
  branch,
379
416
  );
380
417
  } catch (e) {
@@ -388,7 +425,7 @@ function merge(branch) {
388
425
  );
389
426
  }
390
427
  out(
391
- `Merged ${branch} into ${into}: verify passed on ${changed.length} file(s)${lane ? `, held to the ${lane} lane` : ''}.`,
428
+ `Merged ${branch} into ${into}: verify passed on ${changed.length} file(s)${lane ? `, held to ${heldTo(lane)}` : ''}.`,
392
429
  );
393
430
  } finally {
394
431
  cleanup();
@@ -413,6 +450,63 @@ function requests() {
413
450
  );
414
451
  }
415
452
 
453
+ /**
454
+ * The harness settings (`harness` in the config): listed, or one changed as the person's own act. The
455
+ * change is written to the config and given a kit approval with the person's reason, so the commit that
456
+ * keeps it logs their reason. The hooks refuse `settings set` from every agent.
457
+ */
458
+ function settings() {
459
+ const { raw, config } = load();
460
+ if (rest[0] === 'set') {
461
+ const [, key, value] = rest;
462
+ if (!key || value === undefined)
463
+ die('code-kit settings set <key> <value> --reason "<why>" [--via pane]');
464
+ if (!(key in SETTINGS))
465
+ die(`${key} isn't a harness setting. They are: ${Object.keys(SETTINGS).join(', ')}.`);
466
+ if (!reason?.trim()) die('Give a reason with --reason "…": the approval log keeps it.');
467
+ const changed = withSetting(raw, key, parseSetting(key, value));
468
+ const problems = validate(changed);
469
+ if (problems.length) die(`Nothing changed: ${problems.join('; ')}`);
470
+ writeFileSync(resolve(file), `${JSON.stringify(changed, null, 2)}\n`);
471
+ const mark =
472
+ via === 'pane' ? '(changed in the code-kit pane)' : '(changed with code-kit settings)';
473
+ grantApprovals(resolve('.'), ['kit'], null, reason, { kit: mark });
474
+ return out(
475
+ `Set harness.${key} to ${JSON.stringify(settingOf(changed.harness, key))}. Commit ${file} to keep it; the approval log will carry your reason.`,
476
+ );
477
+ }
478
+ if (rest.length) die('code-kit settings [--json] | settings set <key> <value> --reason "<why>"');
479
+ const rows = Object.entries(SETTINGS).map(([key, s]) => ({
480
+ key,
481
+ value: settingOf(config.harness, key),
482
+ default: s.default,
483
+ about: s.about,
484
+ }));
485
+ if (flag('--json')) return out(JSON.stringify(rows, null, 2));
486
+ for (const r of rows)
487
+ out(
488
+ `${r.key.padEnd(26)} ${JSON.stringify(r.value).padEnd(14)} ${r.value === r.default ? '' : `(default ${JSON.stringify(r.default)}) `}${r.about}`,
489
+ );
490
+ }
491
+
492
+ /** Records the review's send-back of `branch` at its current commit (lib/reviews.mjs). */
493
+ function sentBackCommand(branch) {
494
+ load();
495
+ let sha;
496
+ try {
497
+ sha = execFileSync('git', ['rev-parse', '--verify', `${branch}^{commit}`], {
498
+ encoding: 'utf8',
499
+ stdio: ['ignore', 'pipe', 'ignore'],
500
+ }).trim();
501
+ } catch {
502
+ die(`No branch ${branch} here.`);
503
+ }
504
+ recordSentBack(resolve('.'), branch, sha, reason);
505
+ out(
506
+ `Recorded: ${branch} was sent back at ${sha.slice(0, 7)}. status shows its story as sent back until the branch moves on.`,
507
+ );
508
+ }
509
+
416
510
  /** Finish checks still failing: the stop hook's last refusal for each session and agent. */
417
511
  function stops() {
418
512
  load();
@@ -472,7 +566,7 @@ function verify() {
472
566
  return;
473
567
  }
474
568
  out(
475
- `${changed.length} file(s) changed since ${from}${lane ? `, held to the ${lane} lane (branch ${branch})` : ''}.`,
569
+ `${changed.length} file(s) changed since ${from}${lane ? `, held to ${heldTo(lane)} (branch ${branch})` : ''}.`,
476
570
  );
477
571
  let count = 0;
478
572
  for (const [key, title] of Object.entries(VERIFY_GROUPS)) {
@@ -576,7 +670,10 @@ function adapters() {
576
670
  if (!active.includes(a.name)) continue;
577
671
  const c = a.config ?? {};
578
672
  if (c.lead?.length) out(` lead writes: ${c.lead.join(', ')}`);
579
- if (c.anyActor?.length) out(` anyone writes: ${c.anyActor.join(', ')}`);
673
+ if (c.anyActor?.length)
674
+ out(
675
+ ` anyone writes: ${c.anyActor.map((e) => (typeof e === 'string' ? e : `${e.glob} (reviewed by ${e.reviewer ?? 'the lead'})`)).join(', ')}`,
676
+ );
580
677
  for (const p of c.protected ?? [])
581
678
  out(` protected: ${p.glob} (approval "${p.approval}": ${p.why})`);
582
679
  for (const b of a.shell?.block ?? []) out(` blocked command: /${b.pattern}/ — ${b.why}`);
@@ -591,7 +688,7 @@ function trace(paths) {
591
688
  for (const t of traces) {
592
689
  out(t.path);
593
690
  out(
594
- ` owner: ${t.owner ?? 'nobody'}${t.layer ? ` · layer ${t.layer.name} → ${t.layer.mayImport.join(', ') || 'only itself'}` : ''}`,
691
+ ` owner: ${t.owner ?? (t.reviewer ? `any agent; the ${t.reviewer === 'lead' ? 'lead' : `${t.reviewer} lane`} reviews it` : 'nobody')}${t.layer ? ` · layer ${t.layer.name} → ${t.layer.mayImport.join(', ') || 'only itself'}` : ''}`,
595
692
  );
596
693
  if (!t.requirements.length) out(' no requirements traced (no commit names a story)');
597
694
  for (const r of t.requirements)
@@ -661,7 +758,7 @@ async function approve(given) {
661
758
  const whom = laneName ? `the ${laneName} lane` : 'any agent';
662
759
  const renamed = given.flatMap((g, i) => (g === names[i] ? [] : [`${g} → ${names[i]}`]));
663
760
  out(
664
- `Approved ${names.join(', ')} for ${whom}, for ${APPROVAL_MINUTES} minutes${delegated ? ', within the delegated rules' : ''}: ${reason.trim()}${renamed.length ? `\n(${renamed.join(', ')})` : ''}`,
761
+ `Approved ${names.join(', ')} for ${whom}, ${approvalLifetime(names)}${delegated ? ', within the delegated rules' : ''}: ${reason.trim()}${renamed.length ? `\n(${renamed.join(', ')})` : ''}`,
665
762
  );
666
763
  }
667
764
 
@@ -671,6 +768,8 @@ const commands = {
671
768
  merge: () => (rest.length === 1 ? merge(rest[0]) : usage()),
672
769
  requests,
673
770
  stops,
771
+ settings,
772
+ 'sent-back': () => (rest.length === 1 ? sentBackCommand(rest[0]) : usage()),
674
773
  trace: () => (rest.length ? trace(rest) : usage()),
675
774
  adapters,
676
775
  next,
@@ -689,7 +788,8 @@ function usage() {
689
788
  ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
690
789
  ' | approve <name>... --reason "…" [--lane name] [--config file]\n' +
691
790
  ' | merge <branch> --delegated|--person [--into branch] | requests [--json]\n' +
692
- ' | stops [--session id] [--json]',
791
+ ' | stops [--session id] [--json] | sent-back <branch> [--reason "…"]\n' +
792
+ ' | settings [--json] | settings set <key> <value> --reason "…" [--via pane]',
693
793
  );
694
794
  }
695
795
  await (commands[command] ?? usage)();