@warren-dean/code-kit 0.3.5 → 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.5",
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.5",
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
@@ -11,6 +11,8 @@ A Claude Code plugin that keeps AI coding agents inside a project's engineering
11
11
 
12
12
  It also carries the work from idea to merged code: spec an idea into requirements, hand stories to lanes, review each branch against its requirements, and track the build against the spec.
13
13
 
14
+ **Pairs with [Context Graph](https://github.com/warrendeanlangeveldt/context-graph).** code-kit decides who may change what, and what has to be proved. Context Graph gives each agent the why behind a file before it edits it: the file's card, its rules and the decisions made on it, kept in git. Each detects the other: Context Graph's cards and slices then carry the file's spec requirement and layer rule. Install both for agents that stay in their lane and know why the code is the way it is.
15
+
14
16
  <p align="center">
15
17
  <img src="https://raw.githubusercontent.com/warrendeanlangeveldt/code-kit/main/assets/workflow.svg" alt="How code-kit works: the specs create the agents, skills and rules; the lead agent delegates stories to lane agents and takes decisions and approvals to you; lanes send scope and dependency requests back to the lead; hooks enforce ownership, layers, approvals and spec-check while each lane implements, tests and commits; the lead reviews against the requirements and CI runs verify." width="820">
16
18
  </p>
@@ -133,6 +135,13 @@ npm i -g @warren-dean/code-kit # or install it, then: code-kit check
133
135
 
134
136
  **From a clone**, for working on code-kit itself: `/plugin marketplace add <path to the clone>`.
135
137
 
138
+ **Its companion, [Context Graph](https://github.com/warrendeanlangeveldt/context-graph)**, installs the same way. Nothing needs configuring between the two:
139
+
140
+ ```text
141
+ /plugin marketplace add warrendeanlangeveldt/context-graph
142
+ /plugin install context-graph@context-graph
143
+ ```
144
+
136
145
  ## One command: `/code-kit:next`
137
146
 
138
147
  Don't want to remember which skill comes when? Run `/code-kit:next`, from a blank folder or at any point in a build:
@@ -300,6 +309,7 @@ code-kit verify [--base ref] [--branch name] [--no-checks] # a branch's changes
300
309
  code-kit status [--base ref] [--json] # stories and requirements against the spec
301
310
  code-kit next [--base ref] [--json] # the step to take now, from the project's state
302
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
303
313
  ```
304
314
 
305
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`.
@@ -321,11 +331,12 @@ code-kit trace <path>... [--json] # what a file is for: requirements, lane,
321
331
  | `designGate` | no | `{ register, screens: [{ glob, not? }], howTo }`: screen files wait until the register (`{ "screens": [{ "file": … }] }`) lists them |
322
332
  | `layers[]` | no | `{ name, paths, mayImport, denyPackages? }`: the first layer whose paths match a file owns it. A layer may always import itself. |
323
333
  | `importAliases` | no | `{ "@app/domain": "packages/domain/src" }`: resolves non-relative imports to repository paths |
324
- | `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 |
325
335
  | `shell.restricted[]` | no | `{ pattern, lanes, why }`: only the lead and these lanes |
326
336
  | `postEdit[]` | no | `{ files, exclude?, run }`: after each edit of a matching file. `{file}` is the file. Exit 127 (tool not installed) is skipped. |
327
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). |
328
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). |
329
340
  | `branches.protected` | no | Default `["main", "master"]` |
330
341
 
331
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.
@@ -345,6 +356,20 @@ echo "Date formatting for ST-12" > .claude/approvals/web/dep-dayjs #
345
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.
346
357
  - **Lifetime:** 60 minutes. Agents can't create, edit or even mention approvals or the log.
347
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
+
348
373
  ## Adapters
349
374
 
350
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:
@@ -355,9 +380,9 @@ Other tools share a project with code-kit and keep files of their own. An adapte
355
380
 
356
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 }`.
357
382
 
358
- | Adapter | Detected by | Adds |
359
- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
360
- | `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). |
361
386
 
362
387
  Its setup:
363
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.5",
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
 
@@ -123,6 +125,8 @@ Once they approve (with any changes):
123
125
 
124
126
  Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions.
125
127
 
128
+ If the project has no `.ctx/` folder, end the report with one line about the companion plugin, and don't install it unless the person asks: "Context Graph can give each lane agent the why behind the files it edits (cards, rules and decisions kept in git), with this spec's requirements and layer rules in them: `/plugin marketplace add warrendeanlangeveldt/context-graph`, then `/plugin install context-graph@context-graph`."
129
+
126
130
  ## 4. Update an existing setup
127
131
 
128
132
  Re-run whenever the build has moved on: new apps or packages, folders that moved, a changed plan, or files `unowned` keeps listing.
@@ -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.