@warren-dean/code-kit 0.3.6 → 0.3.7

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.3.6",
12
+ "version": "0.3.7",
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.6",
3
+ "version": "0.3.7",
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/README.md CHANGED
@@ -309,6 +309,7 @@ code-kit verify [--base ref] [--branch name] [--no-checks] # a branch's changes
309
309
  code-kit status [--base ref] [--json] # stories and requirements against the spec
310
310
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
311
311
  code-kit trace <path>... [--json] # what a file is for: requirements, lane, layer rules
312
+ code-kit approve <name>... --reason "…" [--lane name] # record approvals a person gave in chat
312
313
  ```
313
314
 
314
315
  `verify` compares the branch with where it left `--base` (default `origin/main`, then `main`). A branch named `<lane>/…`, or given as `--branch`, is held to that lane. `--no-checks` leaves the project's checks to CI's own steps. `status` reads `docs.specs` and `docs.plan`.
@@ -330,11 +331,12 @@ code-kit trace <path>... [--json] # what a file is for: requirements, lane,
330
331
  | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them |
331
332
  | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
332
333
  | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
333
- | `shell.block[]` | no | `{ pattern, why }`: never run |
334
+ | `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 |
334
335
  | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
335
336
  | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
336
337
  | `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). |
337
338
  | `adapters` | no | `{ "<adapter>": false }` switches off an adapter that would otherwise be detected. See [Adapters](#adapters). |
339
+ | `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). |
338
340
  | `branches.protected` | no | Default `["main", "master"]` |
339
341
 
340
342
  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.
@@ -354,6 +356,20 @@ echo "Date formatting for ST-12" > .claude/approvals/web/dep-dayjs #
354
356
  - **Worktrees:** approvals live in the main checkout, so a lane working in its own worktree sees the ones given at the lead's terminal.
355
357
  - **Lifetime:** 60 minutes. Agents can't create, edit or even mention approvals or the log.
356
358
 
359
+ ### From chat
360
+
361
+ On a phone, or anywhere the `!` prefix arrives as plain text, a person can't run the command. With `"approvals": { "lead": true }` in the config (init asks), the person says what they approve in chat, and the lead records it:
362
+
363
+ ```bash
364
+ code-kit approve dep-dayjs --lane web --reason "Yes, add dayjs for ST-12"
365
+ ```
366
+
367
+ - It writes the same approval a `!` command would, for the same 60 minutes, with `(given in chat, recorded by the lead)` after the reason, so the log shows how it was given.
368
+ - Only the lead may run it. A lane or read-only agent is refused, and so is the lead while the setting is off or the config is invalid.
369
+ - With the setting on, refusals name this command alongside the `!` one.
370
+ - The lead may also run `shell.block` commands marked `person: true`, such as Context Graph's ratification trailer.
371
+ - It trusts the lead to act only on the person's own words. Leave it off when the lead runs without a person watching.
372
+
357
373
  ## Adapters
358
374
 
359
375
  Other tools share a project with code-kit and keep files of their own. An adapter tells code-kit about one such tool, without the core knowing its name. Each adapter lives in `hooks/lib/adapters/`, and declares:
@@ -364,9 +380,9 @@ Other tools share a project with code-kit and keep files of their own. An adapte
364
380
 
365
381
  While the tool is detected, its additions are merged into the effective config, like the kit's own defaults. Run `code-kit adapters` to see them, or switch one off with `"adapters": { "<name>": false }`.
366
382
 
367
- | Adapter | Detected by | Adds |
368
- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
369
- | `context-graph` | `.ctx/` | `.ctx/**` for the lead. `.ctx/decisions.ctx` and `.ctx/cards.ctx` for any actor, since every agent records decisions and file cards. `graph.ctx` and `config.toml` are protected (approval `ctx`). No Claude actor may write a `Ctx-Ratified-By` trailer: ratifying is a person's act. |
383
+ | Adapter | Detected by | Adds |
384
+ | --------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
385
+ | `context-graph` | `.ctx/` | `.ctx/**` for the lead. `.ctx/decisions.ctx` and `.ctx/cards.ctx` for any actor, since every agent records decisions and file cards. `graph.ctx` and `config.toml` are protected (approval `ctx`). No Claude actor may write a `Ctx-Ratified-By` trailer: ratifying is a person's act (with `approvals.lead`, the lead may add it when the person says so in chat). |
370
386
 
371
387
  Its setup:
372
388
 
package/bin/code-kit.mjs CHANGED
@@ -14,6 +14,8 @@
14
14
  // trace <path> [--json] what a file is for: the requirements it delivers, its lane, its layer rules
15
15
  // next [--base ref] [--json]
16
16
  // the step to take now (spec-design, init, dispatch, review…), from the project's state
17
+ // approve <name>... --reason "…" [--lane name]
18
+ // record approvals a person gave in chat (with approvals.lead on, the lead runs it)
17
19
  // --config <file> reads a draft (.claude/code-kit.draft.json) instead of .claude/code-kit.json.
18
20
  import { execFileSync } from 'node:child_process';
19
21
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -26,7 +28,7 @@ import { ADAPTERS, activeAdapters } from '../hooks/lib/adapters/index.mjs';
26
28
  import { nextStep } from '../hooks/lib/next.mjs';
27
29
  import { traceFile } from '../hooks/lib/trace.mjs';
28
30
  import { buildStatus } from '../hooks/lib/plan.mjs';
29
- import { currentBranch, ownerOf } from '../hooks/lib/rules.mjs';
31
+ import { APPROVAL_MINUTES, currentBranch, grantApprovals, ownerOf } from '../hooks/lib/rules.mjs';
30
32
  import { laneOfBranch, verifyBranch } from '../hooks/lib/verify.mjs';
31
33
 
32
34
  const out = (s) => process.stdout.write(`${s}\n`);
@@ -44,6 +46,8 @@ const file = option('--config') ?? CONFIG_FILE;
44
46
  const depth = Number(option('--depth') ?? 2);
45
47
  const baseRef = option('--base');
46
48
  const branchName = option('--branch');
49
+ const reason = option('--reason');
50
+ const laneName = option('--lane');
47
51
  const FLAGS = ['--write', '--no-checks', '--json'];
48
52
  const flag = (name) => args.includes(name);
49
53
  const writeBaseline = flag('--write');
@@ -375,8 +379,27 @@ function trace(paths) {
375
379
  }
376
380
  }
377
381
 
382
+ // Each name is an approval a refusal named: a protected path's, `kit`, or `dep-<package>`.
383
+ function approve(names) {
384
+ const { config } = load();
385
+ if (!reason?.trim()) die('approve needs --reason "<what the person approved, in their words>".');
386
+ if (laneName !== undefined && !config.lanes[laneName])
387
+ die(`There is no lane "${laneName}". Lanes: ${Object.keys(config.lanes).join(', ')}.`);
388
+ const bad = names.filter((n) => !/^[a-z0-9@][a-z0-9@+._-]*$/.test(n));
389
+ if (bad.length)
390
+ die(
391
+ `Not an approval name: ${bad.join(', ')}. Use the name the refusal gave, e.g. kit or dep-zod.`,
392
+ );
393
+ grantApprovals(resolve('.'), names, laneName, reason);
394
+ const whom = laneName ? `the ${laneName} lane` : 'any agent';
395
+ out(
396
+ `Approved ${names.join(', ')} for ${whom}, for ${APPROVAL_MINUTES} minutes: ${reason.trim()}`,
397
+ );
398
+ }
399
+
378
400
  const commands = {
379
401
  check,
402
+ approve: () => (rest.length ? approve(rest) : usage()),
380
403
  trace: () => (rest.length ? trace(rest) : usage()),
381
404
  adapters,
382
405
  next,
@@ -392,7 +415,8 @@ function usage() {
392
415
  die(
393
416
  'Usage: code-kit check | who <path>... | unowned | diff | baseline [--write] | graph [--depth N]\n' +
394
417
  ' | verify [--base ref] [--branch name] [--no-checks] | status [--base ref] [--json]\n' +
395
- ' | next [--base ref] [--json] | adapters | trace <path>... [--json] [--config file]',
418
+ ' | next [--base ref] [--json] | adapters | trace <path>... [--json]\n' +
419
+ ' | approve <name>... --reason "…" [--lane name] [--config file]',
396
420
  );
397
421
  }
398
422
  (commands[command] ?? usage)();
@@ -3,7 +3,8 @@
3
3
  // dependencies a person hasn't approved, approval forgery, kit edits through the shell by anyone but
4
4
  // the lead, the project's own blocked and lane-restricted commands; records protected files in the
5
5
  // approval log on commit and scans commits for secrets. Shell writes are also checked afterwards by
6
- // lane-audit.mjs.
6
+ // lane-audit.mjs. With `approvals.lead` on, the lead may act for the person on what they say in chat:
7
+ // record their approvals (`code-kit approve`) and run blocked commands marked `person`.
7
8
  import { appendFileSync } from 'node:fs';
8
9
  import { spawnSync } from 'node:child_process';
9
10
  import { join, resolve } from 'node:path';
@@ -43,8 +44,10 @@ for (const [re, why] of ALWAYS) {
43
44
  if (re.test(cmd)) block(`Blocked: ${why}\nCommand: ${cmd}`);
44
45
  }
45
46
  const packages = addedPackages(cmd);
47
+ const approves = /\bcode-kit(\.mjs)?["']?\s+approve\b/.test(cmd);
46
48
  if (error) {
47
49
  if (/\bgit\s+commit\b/.test(cmd)) block(`Blocked: ${error}\nFix the config before committing.`);
50
+ if (approves) block(`Blocked: ${error}\nApprovals wait until the config is valid.`);
48
51
  if (packages.length)
49
52
  block(`Blocked: ${error}\nNew dependencies wait until the config is valid.\nCommand: ${cmd}`);
50
53
  process.exit(0);
@@ -58,12 +61,29 @@ if (new RegExp(`\\bgit\\s+push\\b.*\\b(${branches})\\b`).test(cmd)) {
58
61
  `Blocked: push to your own branch; a person merges to ${config.branches.protected[0]} after review.\nCommand: ${cmd}`,
59
62
  );
60
63
  }
61
- for (const { pattern, why } of config.shell.block) {
62
- if (new RegExp(pattern).test(cmd)) block(`Blocked: ${why}\nCommand: ${cmd}`);
63
- }
64
64
 
65
65
  const sessionIn = sessionRoot(input, project);
66
66
  const actor = actorFor(input, sessionIn ?? project, config);
67
+ // The lead acts for the person only where the config says the person may approve from chat.
68
+ const forPerson = actor.kind === 'lead' && config.approvals.lead;
69
+
70
+ for (const { pattern, why, person } of config.shell.block) {
71
+ if (!new RegExp(pattern).test(cmd) || (person && forPerson)) continue;
72
+ const hint =
73
+ person && actor.kind === 'lead'
74
+ ? '\nTo let the lead run it when the person says so in chat, a person sets "approvals": { "lead": true } in the config.'
75
+ : '';
76
+ block(`Blocked: ${why}${hint}\nCommand: ${cmd}`);
77
+ }
78
+
79
+ if (approves && actor.kind !== 'lead')
80
+ block(
81
+ `Blocked: only the lead records an approval a person gives in chat. Write a short change request for the lead (what needs approving, and why), and stop.\nCommand: ${cmd}`,
82
+ );
83
+ if (approves && !forPerson)
84
+ block(
85
+ `Blocked: this project doesn't let the lead record approvals ("approvals.lead" is off). Ask the person to run the \`!\` command from the refusal, or to turn "approvals": { "lead": true } on in the config.\nCommand: ${cmd}`,
86
+ );
67
87
 
68
88
  // Each new package needs a person's approval, for this actor; it also covers the manifest and lockfile
69
89
  // changes the install makes (lib/rules.mjs › dependencyChange).
@@ -84,6 +104,7 @@ if (packages.length) {
84
104
  actor,
85
105
  sessionIn ?? project,
86
106
  'installing it, and the manifest and lockfile changes that makes,',
107
+ config,
87
108
  )}\nCommand: ${cmd}`,
88
109
  );
89
110
  }
@@ -127,7 +148,7 @@ const writes =
127
148
  );
128
149
  if (touchesKit && writes && actor.kind !== 'lead' && !approval(root, 'kit', actor)) {
129
150
  block(
130
- `Blocked: the .claude kit is changed only by the lead or with a person's approval. Ask the lead to run\n${approvalHowTo('kit', actor, root)}`,
151
+ `Blocked: the .claude kit is changed only by the lead or with a person's approval. Ask the lead to run\n${approvalHowTo('kit', actor, root, undefined, config)}`,
131
152
  );
132
153
  }
133
154
 
@@ -179,7 +200,7 @@ function recordApprovals() {
179
200
  const byDependency = actor.kind === 'lane' && dependencyChange(actor, file, root, config);
180
201
  if (entry && !granted && actor.kind !== 'lead' && !byDependency) {
181
202
  block(
182
- `Blocked: ${file} is ${entry.why}; committing it needs a person's approval in force. Ask the lead to run\n${approvalHowTo(entry.approval, actor, root)}`,
203
+ `Blocked: ${file} is ${entry.why}; committing it needs a person's approval in force. Ask the lead to run\n${approvalHowTo(entry.approval, actor, root, undefined, config)}`,
183
204
  );
184
205
  }
185
206
  if (granted)
@@ -3,7 +3,8 @@
3
3
  // each file's card (its why) in cards.ctx. Every agent records decisions and cards for the files it
4
4
  // edits, so those two files are writable by any actor; the rules and settings
5
5
  // are the lead's, and changes to them go through the approval log. Ratifying a rule is a person's act,
6
- // signed with a commit trailer, so no Claude actor may write that trailer.
6
+ // signed with a commit trailer, so no Claude actor may write that trailer, unless the project lets the
7
+ // lead act on what the person says in chat (`approvals.lead`).
7
8
  import { existsSync } from 'node:fs';
8
9
  import { join } from 'node:path';
9
10
 
@@ -23,6 +24,7 @@ export default {
23
24
  {
24
25
  pattern: 'Ctx-Ratified-By\\s*[:=]',
25
26
  why: "Ratifying a Context Graph rule is a person's act: they add the Ctx-Ratified-By trailer themselves, with a `!` git command.",
27
+ person: true,
26
28
  },
27
29
  ],
28
30
  },
@@ -4,7 +4,8 @@
4
4
  // name the tool, and the key that switches it off in the config: "adapters": { "<name>": false }
5
5
  // detect (root) => true when the tool is in use in this project
6
6
  // config { lead?, anyActor?, protected? } merged into the effective config, like the kit's defaults
7
- // shell { block? } commands refused for every Claude actor, merged into shell.block
7
+ // shell { block? } commands refused for every Claude actor, merged into shell.block; `person: true`
8
+ // marks a person's act the lead may do for them under approvals.lead
8
9
  // setup lines for init to apply or tell the person: ignore files, merge drivers, instructions
9
10
  //
10
11
  // The core only ever calls applyAdapters() and activeAdapters(); it never names a tool.
@@ -47,6 +47,7 @@ export function withDefaults(c) {
47
47
  postEdit: c.postEdit ?? [],
48
48
  checks: c.checks ?? [],
49
49
  branches: { protected: c.branches?.protected ?? ['main', 'master'] },
50
+ approvals: { lead: c.approvals?.lead === true },
50
51
  };
51
52
  }
52
53
 
@@ -114,6 +115,11 @@ export function validate(c) {
114
115
  Object.entries(c.adapters).every(([k, v]) => known.includes(k) && typeof v === 'boolean')),
115
116
  `"adapters" must map adapter names (${known.join(', ')}) to true or false`,
116
117
  );
118
+ need(
119
+ c.approvals === undefined ||
120
+ (isObj(c.approvals) && [undefined, true, false].includes(c.approvals.lead)),
121
+ '"approvals.lead" must be true or false',
122
+ );
117
123
  need(
118
124
  c.branches === undefined || isList(c.branches?.protected),
119
125
  '"branches.protected" must be a list of branch names',
@@ -176,9 +182,10 @@ function validateLayers(c, need, each) {
176
182
  function validateShell(s, lanes, need) {
177
183
  if (!need(isObj(s), '"shell" must be an object')) return;
178
184
  const rule = (r) => isObj(r) && isRegExp(r.pattern) && isStr(r.why);
185
+ const blockRule = (r) => rule(r) && [undefined, true, false].includes(r.person);
179
186
  need(
180
- s.block === undefined || (Array.isArray(s.block) && s.block.every(rule)),
181
- '"shell.block" must be a list of { pattern, why }',
187
+ s.block === undefined || (Array.isArray(s.block) && s.block.every(blockRule)),
188
+ '"shell.block" must be a list of { pattern, why, person? }',
182
189
  );
183
190
  const restricted =
184
191
  s.restricted === undefined ||
@@ -10,11 +10,22 @@
10
10
  //
11
11
  // Approvals live in the project's main checkout, so a lane working in its own worktree sees the ones a
12
12
  // person gives at the lead's terminal. One for a lane (`<lane>/<name>`) serves that lane only; one
13
- // without a lane serves every actor.
14
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
13
+ // without a lane serves every actor. A person gives one with a `!` shell command, or, where the config
14
+ // sets `approvals.lead` (a person on a phone has no `!`), by saying so in chat for the lead to record
15
+ // with `code-kit approve`.
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readFileSync,
20
+ readdirSync,
21
+ realpathSync,
22
+ statSync,
23
+ writeFileSync,
24
+ } from 'node:fs';
15
25
  import { execFileSync } from 'node:child_process';
16
26
  import { homedir } from 'node:os';
17
27
  import { dirname, join, relative, resolve } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
18
29
  import { DEPENDENCY_APPROVAL, isDependencyFile } from './dependencies.mjs';
19
30
  import { globToRegExp, matchesAny } from './glob.mjs';
20
31
 
@@ -24,12 +35,20 @@ export const APPROVAL_MINUTES = 60;
24
35
  // Marks a worktree as a lane's, for a person running that lane; the lead creates it, and it is never work.
25
36
  export const LANE_FILE = '.lane';
26
37
  const NEVER_WRITABLE = [`${APPROVALS_DIR}/**`, APPROVAL_LOG];
38
+ export const CLI = fileURLToPath(new URL('../../bin/code-kit.mjs', import.meta.url));
39
+ // Marks an approval the lead recorded from chat, so the log shows how it was given.
40
+ export const RELAYED = '(given in chat, recorded by the lead)';
41
+
42
+ /** The command the lead runs to record approvals a person gave in chat. */
43
+ export const relayCommand = (names, lane) =>
44
+ `node "${CLI}" approve ${[names].flat().join(' ')}${lane ? ` --lane ${lane}` : ''} --reason "<the person's words>"`;
27
45
 
28
46
  /**
29
47
  * The commands a person runs to approve `names` for `actor`, and what that allows. For a lane the
30
- * approval is that lane's; the folder is created here so the command works as written.
48
+ * approval is that lane's; the folder is created here so the command works as written. With
49
+ * `approvals.lead` on, the lead may record it instead, once the person approves in chat.
31
50
  */
32
- export function approvalHowTo(names, actor, root, allows = 'these edits') {
51
+ export function approvalHowTo(names, actor, root, allows = 'these edits', config) {
33
52
  const dir = approvalsDir(root);
34
53
  const scoped = actor?.kind === 'lane' ? join(dir, actor.lane) : dir;
35
54
  try {
@@ -41,12 +60,26 @@ export function approvalHowTo(names, actor, root, allows = 'these edits') {
41
60
  const where = shown.startsWith('..') ? scoped : shown;
42
61
  const lines = [names].flat().map((n) => ` ! echo "<what you are approving>" > ${where}/${n}`);
43
62
  const whom = actor?.kind === 'lane' ? `the ${actor.lane} lane` : 'any agent';
63
+ const relay = config?.approvals?.lead
64
+ ? `\nOr, if the person approves in chat (no \`!\` on a phone), the lead records their words:\n ${relayCommand(names, actor?.kind === 'lane' ? actor.lane : undefined)}`
65
+ : '';
44
66
  return (
45
67
  `${lines.join('\n')}\n` +
46
- `(a person runs it; the reason is recorded in ${APPROVAL_LOG}, and it allows ${allows} for ${whom}, for ${APPROVAL_MINUTES} minutes)`
68
+ `(a person runs it; the reason is recorded in ${APPROVAL_LOG}, and it allows ${allows} for ${whom}, for ${APPROVAL_MINUTES} minutes)${relay}`
47
69
  );
48
70
  }
49
71
 
72
+ /** Records approvals a person gave in chat: the same files a `!` command writes, marked as relayed. */
73
+ export function grantApprovals(root, names, lane, reason) {
74
+ const dir = lane ? join(approvalsDir(root), lane) : approvalsDir(root);
75
+ mkdirSync(dir, { recursive: true });
76
+ return names.map((name) => {
77
+ const file = join(dir, name);
78
+ writeFileSync(file, `${reason.trim()} ${RELAYED}\n`);
79
+ return file;
80
+ });
81
+ }
82
+
50
83
  /** `path` with symlinks resolved, even if it doesn't exist yet (macOS /var → /private/var). */
51
84
  export function realPath(path) {
52
85
  let dir = resolve(path);
@@ -234,7 +267,7 @@ export function writeProblem(actor, rel, root, config) {
234
267
  // The lead writes protected files without an approval: the person reviews them in the pull request,
235
268
  // and every committed change is in the audit log. Anyone else needs a person's approval as well.
236
269
  if (guarded && actor.kind !== 'lead' && !approval(root, guarded.approval, actor)) {
237
- return `${rel} is ${guarded.why} and needs a person's approval. Ask the lead (a person) to run\n${approvalHowTo(guarded.approval, actor, root)}`;
270
+ return `${rel} is ${guarded.why} and needs a person's approval. Ask the lead (a person) to run\n${approvalHowTo(guarded.approval, actor, root, undefined, config)}`;
238
271
  }
239
272
  if (actor.kind === 'readonly') return `${actor.label} is read-only; it may not write ${rel}.`;
240
273
  if (matchesAny(rel, config.anyActor)) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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",
@@ -67,6 +67,7 @@ Write `.claude/code-kit.draft.json`. Follow `${CLAUDE_PLUGIN_ROOT}/README.md`
67
67
  - **`shell.restricted`:** commands that affect a shared local service, limited to the lanes that own it.
68
68
  - **`postEdit`:** the project's existing per-file linters and formatters, with `{file}`.
69
69
  - **`checks`:** the typecheck, test, architecture and database commands, each scoped by `files`. Only include commands that pass on the current code, or ones the person chose to keep.
70
+ - **`approvals.lead`:** ask the person whether they'll ever approve from a phone or another chat-only client, where a `!` command arrives as plain text. If so, set it to `true`: the lead then records approvals they give in chat (`code-kit approve`), and runs the `shell.block` commands marked `person: true` when they say so. Mark a `shell.block` command `person: true` only if the person wants to be able to have the lead run it on their say-so. Leave the setting out if the lead will run unattended.
70
71
 
71
72
  Prove the draft:
72
73
 
@@ -102,6 +103,7 @@ Show the person:
102
103
  1. the layers and what each may depend on, and for brownfield the existing violations the baseline will record;
103
104
  2. the lanes, their agents and their paths;
104
105
  3. protected paths, blocked and restricted commands, and checks (including any left out because they fail today);
106
+ and whether the lead may record approvals given in chat (`approvals.lead`);
105
107
  4. the active adapters and what each adds, from `adapters`;
106
108
  5. every **question**: where the docs or the code are silent, ambiguous or contradictory. Don't guess; a wrong owner or layer blocks real work.
107
109
 
@@ -44,7 +44,7 @@ Run the named skill with the Skill tool: `code-kit:<step>`, with `args` (and the
44
44
  After a step finishes, run `next --json` again and carry on with the new step, without asking, as long as nothing needs the person. Stop and hand over when:
45
45
 
46
46
  - a skill asks the person questions (spec-design's rounds, spec-check's blocking questions, review's scope questions);
47
- - something needs the person's approval (init's draft, an approval for a protected path);
47
+ - something needs the person's approval (init's draft, an approval for a protected path). If the config has `approvals.lead`, ask for it in chat: when they approve in their own message, record it with the `code-kit approve` command the refusal names (their words as the reason) and carry on;
48
48
  - a person has to act: merge a pull request to a protected branch, add a CI secret;
49
49
  - the step is `wait` or `done`;
50
50
  - the same step comes back twice in a row with nothing changed. Report what's stuck instead of looping.
@@ -7,6 +7,7 @@ The code-kit plugin enforces the rules in `.claude/code-kit.json` on every tool
7
7
  - spec-check before lane code{{; designs approved before screens}};
8
8
  - an audit log of every change to protected paths (`.claude/approval-log.jsonl`), which the person reviews in the pull request;
9
9
  - a person's approval for every new dependency, per package and per lane. When a lane asks for one, give the person the package, the reason and the approval command from the refusal, and resume the lane once they've run it;
10
+ {{If approvals.lead is on: - approvals from chat. When the person approves in their own message (a `!` command doesn't work on a phone), record it with the `code-kit approve` command from the refusal, with their words as the reason. Never record an approval they haven't given, and never on a lane's say-so;}}
10
11
  - the project's checks before any agent finishes.
11
12
 
12
13
  If a hook blocks you, fix the cause; never work around it.