@sabaiway/agent-workflow-kit 3.0.0 → 3.2.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +1 -0
  3. package/SKILL.md +7 -3
  4. package/bin/install.mjs +10 -1
  5. package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
  7. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +189 -18
  9. package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
  10. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +77 -41
  11. package/bridges/antigravity-cli-bridge/capability.json +4 -2
  12. package/bridges/antigravity-cli-bridge/references/driving-agy.md +5 -0
  13. package/bridges/codex-cli-bridge/SKILL.md +8 -4
  14. package/bridges/codex-cli-bridge/bin/codex-exec.sh +129 -11
  15. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +278 -23
  16. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
  17. package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
  18. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +112 -17
  19. package/bridges/codex-cli-bridge/capability.json +19 -4
  20. package/bridges/codex-cli-bridge/references/driving-codex.md +11 -0
  21. package/capability.json +1 -1
  22. package/package.json +1 -1
  23. package/references/modes/bootstrap.md +3 -2
  24. package/references/modes/recommendations.md +1 -0
  25. package/references/modes/upgrade.md +9 -4
  26. package/references/modes/velocity.md +2 -0
  27. package/references/modes/worktrees.md +120 -0
  28. package/references/scripts/archive-decisions.mjs +2 -2
  29. package/references/scripts/archive-decisions.test.mjs +5 -5
  30. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  31. package/references/scripts/check-docs-size.mjs +46 -29
  32. package/references/shared/command-shapes.md +22 -0
  33. package/references/shared/composition-handoff.md +7 -2
  34. package/references/templates/agent_rules.md +10 -1
  35. package/tools/autonomy-doctor.mjs +1 -1
  36. package/tools/bridge-settings-read.mjs +25 -5
  37. package/tools/changed-surface.mjs +8 -8
  38. package/tools/commands.mjs +9 -0
  39. package/tools/delegation.mjs +2 -2
  40. package/tools/detect-backends.mjs +10 -0
  41. package/tools/grounding.mjs +13 -13
  42. package/tools/inject-methodology.mjs +71 -40
  43. package/tools/lens-region.mjs +113 -36
  44. package/tools/migrate-adr-store.mjs +3 -3
  45. package/tools/procedures.mjs +1 -1
  46. package/tools/recipes.mjs +2 -2
  47. package/tools/recommendations.mjs +34 -7
  48. package/tools/release-scan.mjs +12 -2
  49. package/tools/review-state.mjs +3 -3
  50. package/tools/sandbox-masks.mjs +13 -13
  51. package/tools/set-recipe.mjs +1 -1
  52. package/tools/velocity-profile.mjs +7 -7
  53. package/tools/worktrees.mjs +2292 -0
@@ -0,0 +1,41 @@
1
+ // check-docs-size-cli.test.mjs — runCli branch pins the subprocess smokes cannot reach
2
+ // in-process (Phase-5 coverage fill; the main spec file is parity-frozen, so these ride a
3
+ // colocated file): the unknown-argument refusal and the written-empty-index guard.
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { runCli } from './check-docs-size.mjs';
10
+
11
+ const cli = async (argv) => {
12
+ const { code, stdout, stderr } = await runCli(argv);
13
+ return { code, stdout, stderr };
14
+ };
15
+
16
+ describe('check-docs-size runCli — refusal branches', () => {
17
+ it('an unknown argument exits 2 naming it', async () => {
18
+ const { code, stderr } = await cli(['--bogus']);
19
+ assert.equal(code, 2);
20
+ assert.match(stderr, /Unknown argument: --bogus/);
21
+ });
22
+
23
+ it('--write-index landing on a sink path (index stat size 0) is the loud written-empty refusal', async () => {
24
+ const root = mkdtempSync(join(tmpdir(), 'cds-cli-'));
25
+ try {
26
+ mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
27
+ writeFileSync(
28
+ join(root, 'docs', 'ai', 'a.md'),
29
+ '---\ntype: state\nlastUpdated: 2026-07-18\nscope: session\nstaleAfter: never\nowner: none\nmaxLines: 10\n---\n\n# a\n',
30
+ );
31
+ // The index path is a symlink into /dev/null: the write lands, the stat reads size 0 —
32
+ // the guard must refuse loudly instead of reporting a written index.
33
+ symlinkSync('/dev/null', join(root, 'docs', 'ai', 'index.md'));
34
+ const { code, stderr } = await cli(['--write-index', `--root=${root}`]);
35
+ assert.equal(code, 2);
36
+ assert.match(stderr, /index\.md was written empty/);
37
+ } finally {
38
+ rmSync(root, { recursive: true, force: true });
39
+ }
40
+ });
41
+ });
@@ -103,25 +103,19 @@ export const discoverMeta = async (root = ROOT) => {
103
103
  return { projectName, hierarchicalLinks, onDemandLinks };
104
104
  };
105
105
 
106
+ // Pure argv parser (no I/O, no exit): `help` / `error` ride out as data for runCli to render.
106
107
  const parseArgs = (argv) => {
107
108
  const flags = { report: false, writeIndex: false, checkIndex: false, quiet: false };
108
109
  const opts = { today: null, root: null };
109
- for (const arg of argv.slice(2)) {
110
+ for (const arg of argv) {
110
111
  if (arg === '--report') flags.report = true;
111
112
  else if (arg === '--write-index') flags.writeIndex = true;
112
113
  else if (arg === '--check-index') flags.checkIndex = true;
113
114
  else if (arg === '--quiet') flags.quiet = true;
114
115
  else if (arg.startsWith('--today=')) opts.today = arg.slice('--today='.length);
115
116
  else if (arg.startsWith('--root=')) opts.root = arg.slice('--root='.length);
116
- else if (arg === '--help' || arg === '-h') {
117
- console.log(
118
- 'Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]',
119
- );
120
- process.exit(0);
121
- } else {
122
- console.error(`Unknown argument: ${arg}`);
123
- process.exit(2);
124
- }
117
+ else if (arg === '--help' || arg === '-h') return { flags, opts, help: true };
118
+ else return { flags, opts, error: `Unknown argument: ${arg}` };
125
119
  }
126
120
  return { flags, opts };
127
121
  };
@@ -218,7 +212,7 @@ const formatRow = (row) => {
218
212
  return { status, sizeCell, ...row };
219
213
  };
220
214
 
221
- const printReport = (rows, quiet) => {
215
+ const printReport = (rows, quiet, log = console.log) => {
222
216
  const widths = {
223
217
  status: 2,
224
218
  path: Math.max(4, ...rows.map((r) => r.path.length)),
@@ -228,15 +222,15 @@ const printReport = (rows, quiet) => {
228
222
  };
229
223
  const printable = quiet ? rows.filter((r) => r.errors.length || r.warnings.length) : rows;
230
224
  if (printable.length > 0) {
231
- console.log(
225
+ log(
232
226
  `${'S'.padEnd(widths.status)} ${'PATH'.padEnd(widths.path)} ${'SIZE/MAX'.padEnd(widths.size)} ${'TYPE'.padEnd(widths.type)} ${'UPDATED'.padEnd(widths.updated)}`,
233
227
  );
234
228
  for (const row of printable) {
235
- console.log(
229
+ log(
236
230
  `${row.status.padEnd(widths.status)} ${row.path.padEnd(widths.path)} ${row.sizeCell.padEnd(widths.size)} ${(row.frontmatter?.type ?? '').padEnd(widths.type)} ${(row.frontmatter?.lastUpdated ?? '').padEnd(widths.updated)}`,
237
231
  );
238
- for (const err of row.errors) console.log(` - ERROR ${err}`);
239
- for (const warn of row.warnings) console.log(` - WARN ${warn}`);
232
+ for (const err of row.errors) log(` - ERROR ${err}`);
233
+ for (const warn of row.warnings) log(` - WARN ${warn}`);
240
234
  }
241
235
  }
242
236
  };
@@ -361,9 +355,29 @@ export const regenerateIndex = async (root, todayStr = null) => {
361
355
  return { indexPath, files: rows.length };
362
356
  };
363
357
 
364
- const main = async () => {
365
- const { flags, opts } = parseArgs(process.argv);
366
- const { root, docsDir, indexPath } = pathsFor(opts.root ? resolve(opts.root) : ROOT);
358
+ // The return-code entry point (no process.argv / process.exit / console inside): argv[]
359
+ // { code, stdout, stderr }. The thin shell at the bottom is the only process-coupled code.
360
+ export const runCli = async (argv, deps = {}) => {
361
+ const stdoutLines = [];
362
+ const stderrLines = [];
363
+ const log = (line) => stdoutLines.push(line);
364
+ const logError = (line) => stderrLines.push(line);
365
+ const result = (code) => ({
366
+ code,
367
+ stdout: stdoutLines.length > 0 ? `${stdoutLines.join('\n')}\n` : '',
368
+ stderr: stderrLines.length > 0 ? `${stderrLines.join('\n')}\n` : '',
369
+ });
370
+
371
+ const { flags, opts, help, error } = parseArgs(argv);
372
+ if (help) {
373
+ log('Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]');
374
+ return result(0);
375
+ }
376
+ if (error) {
377
+ logError(error);
378
+ return result(2);
379
+ }
380
+ const { root, docsDir, indexPath } = pathsFor(opts.root ? resolve(opts.root) : (deps.root ?? ROOT));
367
381
  const today = computeToday(opts.today);
368
382
  const files = (await walkMarkdownFiles(docsDir)).sort();
369
383
  const inspected = await Promise.all(files.map((f) => inspectFile(f, today, root)));
@@ -373,11 +387,11 @@ const main = async () => {
373
387
 
374
388
  if (flags.writeIndex) {
375
389
  await writeIndex(rows, today, meta, indexPath);
376
- console.log(`Wrote ${relative(root, indexPath)}`);
390
+ log(`Wrote ${relative(root, indexPath)}`);
377
391
  const after = await stat(indexPath);
378
392
  if (after.size === 0) {
379
- console.error('index.md was written empty');
380
- process.exit(2);
393
+ logError('index.md was written empty');
394
+ return result(2);
381
395
  }
382
396
  }
383
397
 
@@ -385,28 +399,31 @@ const main = async () => {
385
399
  const onDisk = existsSync(indexPath) ? await readFile(indexPath, 'utf8') : null;
386
400
  const { fresh } = checkIndexFreshness(rows, onDisk, meta);
387
401
  if (!fresh) {
388
- console.error(
402
+ logError(
389
403
  `[check-docs-size] FAIL: ${relative(root, indexPath)} is stale (out of sync with source frontmatter). Regenerate the index (--write-index) and commit the regenerated file.`,
390
404
  );
391
- process.exit(1);
405
+ return result(1);
392
406
  }
393
- console.log(
407
+ log(
394
408
  `[check-docs-size] OK — ${relative(root, indexPath)} is in sync with source frontmatter.`,
395
409
  );
396
- return;
410
+ return result(0);
397
411
  }
398
412
 
399
- printReport(rows, flags.quiet);
413
+ printReport(rows, flags.quiet, log);
400
414
  const errorCount = rows.reduce((n, r) => n + r.errors.length, 0);
401
415
  const warnCount = rows.reduce((n, r) => n + r.warnings.length, 0);
402
- console.log(
416
+ log(
403
417
  `\n${rows.length} files inspected — ${errorCount} error(s), ${warnCount} warning(s)`,
404
418
  );
405
419
 
406
- if (errorCount > 0 && !flags.report) process.exit(1);
420
+ return result(errorCount > 0 && !flags.report ? 1 : 0);
407
421
  };
408
422
 
409
423
  const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
410
424
  if (isDirectRun) {
411
- await main();
425
+ const { code, stdout, stderr } = await runCli(process.argv.slice(2));
426
+ if (stdout) process.stdout.write(stdout);
427
+ if (stderr) process.stderr.write(stderr);
428
+ process.exitCode = code;
412
429
  }
@@ -0,0 +1,22 @@
1
+ ### Command shapes — the promptless bar for instructed reads & probes
2
+
3
+ When a mode doc tells you to read a file or probe project state WITHOUT prescribing the exact
4
+ command (a recon read, the version-stamp read, any "check X" instruction), the shape is yours —
5
+ and improvised shapes are where approval prompts come from. The bar:
6
+
7
+ - **Reads ride the host's file-read tool** (Read/Grep/Glob in Claude Code, or your agent's
8
+ equivalent) whenever one exists — a file-read tool never fires a shell approval prompt.
9
+ - **No file-read tool → ONE plain undecorated command per probe:** no `;`/`&&` compounds, no
10
+ redirects, no pipes, no command substitution — one probe per invocation. This applies the
11
+ deployed agent-rules lens’s «plain pipeline per call» discipline here as a stricter
12
+ single-command shape. Any decorated or chained form falls outside this contract’s baseline
13
+ guarantee; behavior is then host/config-dependent — it may prompt, an opt-in read-lane may
14
+ auto-approve a vetted compound, and command substitution may even slip past a prefix allow
15
+ rule. The plain single-command shape is the only shell fallback this contract treats as
16
+ promptless by construction.
17
+ - **Improvised file writes ride the host's file-edit tools** (Write/Edit or the equivalent) —
18
+ never an ad-hoc heredoc or shell-redirect write.
19
+
20
+ **Scope — improvised shapes only.** The executable commands a mode doc itself prescribes (the
21
+ `node …/tools/…` dispatch lines, `--apply` lanes, install/symlink steps) are OUTSIDE this
22
+ contract: run them exactly as prescribed, as plain single invocations.
@@ -43,7 +43,7 @@ duplicate anchor → **STOP with an error**, never edit (the file is left byte-f
43
43
  exceed the 100-line cap, the engine is too old to ship its fragment, or (autonomy only) its anchor
44
44
  (the orchestration pair) is absent: every prior pointer still lands and the upgrade continues.
45
45
 
46
- **Agent-rules lens refresh (runs in BOTH paths).** Its precondition is its own target file — run it
46
+ **Agent-rules lens + Communication refresh (runs in BOTH paths).** Its precondition is its own target file — run it
47
47
  only after `<project>/docs/ai/agent_rules.md` exists: after the substrate deploy in the
48
48
  **delegated** path, after the fallback-template copy in the **kit** path (never anchored to the
49
49
  `AGENTS.md` step above). ONE command:
@@ -53,4 +53,9 @@ prior canonical body is refreshed from the installed engine; a current seed repo
53
53
  current* (zero-diff). Relay its outcome in plain language (*refreshed* / *already current* /
54
54
  *custom edit preserved + note* / *file absent — skipped* / *engine too old — skipped* / *over the
55
55
  line cap — refused*); a fully absent/invalid engine is the same loud STOP + one-line install
56
- command as the pointer reconcile.
56
+ command as the pointer reconcile. The same invocation also reconciles the **Communication
57
+ (user-facing messages)** section from the kit's own bundled template canon (no engine involved):
58
+ a body matching the current canon is reported *already current*; a body matching a known prior
59
+ is refreshed; a custom body is preserved + noted; an absent section is a stated note (never an
60
+ insert); an over-cap refresh is refused; and an unreadable bundled template canon is its own
61
+ loud STOP naming the kit reinstall command.
@@ -63,7 +63,16 @@ Before proposing changes or committing, review against:
63
63
  ### 2.4. Quality Gates
64
64
  - Always run type-checker, linter, and all tests before committing.
65
65
 
66
- ### 2.5. Planning, review & process-fidelity invariants
66
+ ### 2.5. Communication (user-facing messages)
67
+ Apply this as part of §2 before any user-facing summary:
68
+ - **Plain language.** User-facing narration is short, clear, plain words of the dialogue language; when the dialogue language is not English, transliterated English jargon is banned — an English term survives only as the NAME of a thing (a flag / command / file / test), glossed in plain words when helpful; plain English stays plain for English-dialogue users.
69
+ - **Deliver the artifact IN the message** — paste the prompt / diff / version / command inline; never "see §X / open the file / run it and you'll see" as a *substitute* for showing what was asked.
70
+ - **Lead with the result**, then the details; show exactly what was asked — no deflection, no "almost done" when the ask was the finished thing.
71
+ - **No condescension, no filler.** Own a miss plainly and fix it in the same message.
72
+ - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
73
+ - **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.
74
+
75
+ ### 2.6. Planning, review & process-fidelity invariants
67
76
  Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
68
77
  - **Fold by code, not prose.** Before folding a code-touching finding into a plan or change, read the cited `file:line` and cite it — a prose fold drifts from the code and seeds the next bug.
69
78
  - **Right altitude.** Pin intent + invariants + acceptance criteria (named tests); leave fine code-mechanics to Execute, where prose cannot diverge from reality.
@@ -254,7 +254,7 @@ const renderDiagnosis = ({ plan, probeResult, log }) => {
254
254
  }
255
255
  };
256
256
 
257
- // The untrusted degrade every lane runs FIRST (codex R1): while any sandbox binary resolves only
257
+ // The untrusted degrade every lane runs FIRST: while any sandbox binary resolves only
258
258
  // outside the trusted dirs, no offer, no consent invitation, and no privileged path may proceed —
259
259
  // an install cannot make such a host verify-ready.
260
260
  const refuseUntrusted = ({ plan, log }) => {
@@ -148,6 +148,17 @@ export const duplicateKeys = (parsed) => [...parsed.byKey.entries()].filter(([,
148
148
 
149
149
  // ── effective-value resolution (env > file > built-in default) ────────────────────────
150
150
 
151
+ // The env keys the wrappers resolver-validate AFTER precedence (AD-061 aw_effective_timeout) —
152
+ // exactly these mirror the wrapper's invalid-env → warn + built-in-default lane.
153
+ const RESOLVER_VALIDATED_ENV = new Set(['CODEX_HARD_TIMEOUT', 'AGY_HARD_TIMEOUT']);
154
+ // A control byte (C0 range + DEL) in a screened knob makes the wrapper EXIT 2 pre-spend — the
155
+ // advisor must report that refusal, never a benign fallback, and never echo the raw byte into
156
+ // the report (a newline could otherwise forge a `review posture:`/status line). safeShow escapes
157
+ // every note value: JSON.stringify handles C0, an explicit pass handles DEL (JSON leaves it raw).
158
+ const hasControlByte = (s) => /[\x01-\x1f\x7f]/.test(s);
159
+ const safeShow = (s) => JSON.stringify(s).slice(1, -1).replace(/\x7f/g, '\\u007f');
160
+ const REFUSED = (key, v) => ({ value: null, source: 'default', note: `env value "${safeShow(v)}" carries control bytes for ${key} — the wrapper REFUSES the run pre-spend` });
161
+
151
162
  // The effective value of one knob + where it comes from. Fact-only; never a model claim.
152
163
  export const effectiveOf = (entry, parsed, getenv) => {
153
164
  const key = entry.key;
@@ -159,13 +170,22 @@ export const effectiveOf = (entry, parsed, getenv) => {
159
170
  // add-dir knobs fall to their real built-in default). Report the manifest default (null ⇒ "wrapper
160
171
  // built-in applies"), not a misleading null-for-everything.
161
172
  if (v === '') return { value: entry.default, source: 'default', note: 'the env KEY= suppresses the file override — the wrapper built-in applies' };
173
+ // A control byte in a SCREENED knob (the tier + the resolver-validated timeouts) is a pre-spend
174
+ // refusal in the wrapper — report it as such (the default value is inert; the run never happens).
175
+ if (hasControlByte(v) && (entry.kind === 'enum' || RESOLVER_VALIDATED_ENV.has(key))) {
176
+ return REFUSED(key, v);
177
+ }
162
178
  // Mirror the wrapper: an ENUM knob (the service tier) validates its ENV value too — codex accepts
163
179
  // any `-c service_tier` string silently, so the wrapper drops an unsupported one to the built-in
164
- // default (codex-exec.sh:188). A non-enum env value is the operator's documented RAW override (e.g.
165
- // a timeout(1) duration like `2h` the wrapper passes straight through) shown as-is (D3 scopes
166
- // typed validation to FILE lines; the Phase-1 council refuted validating non-enum env usage).
180
+ // default (codex-exec.sh:188). The TIMEOUT knobs are resolver-validated env INCLUDED since AD-061
181
+ // (aw_effective_timeout closed the old env bypass, with a 7-digit integer-part overflow bound) —
182
+ // the advisor must never display a dead override as active. Every OTHER non-enum env value stays
183
+ // the operator's RAW override (the wrappers run no resolver on it), shown as-is.
167
184
  if (entry.kind === 'enum' && !settingValueValid(entry, v)) {
168
- return { value: entry.default, source: 'default', note: `env value "${v}" is not a supported ${key} — the wrapper runs the built-in default` };
185
+ return { value: entry.default, source: 'default', note: `env value "${safeShow(v)}" is not a supported ${key} — the wrapper runs the built-in default` };
186
+ }
187
+ if (RESOLVER_VALIDATED_ENV.has(key) && (!settingValueValid(entry, v) || v.match(/^\d*/)[0].length > 7)) {
188
+ return { value: entry.default, source: 'default', note: `env value "${safeShow(v)}" is invalid for ${key} — the wrapper falls back to the built-in default` };
169
189
  }
170
190
  return { value: v, source: 'env' };
171
191
  }
@@ -173,7 +193,7 @@ export const effectiveOf = (entry, parsed, getenv) => {
173
193
  if (fileEntries && fileEntries.length) {
174
194
  const v = fileEntries[fileEntries.length - 1].value; // last occurrence wins
175
195
  if (settingValueValid(entry, v)) return { value: v, source: 'file' };
176
- return { value: entry.default, source: 'default', note: `file value "${v}" is invalid — falls back to the built-in default` };
196
+ return { value: entry.default, source: 'default', note: `file value "${safeShow(v)}" is invalid — falls back to the built-in default` };
177
197
  }
178
198
  return { value: entry.default, source: 'default' };
179
199
  };
@@ -37,7 +37,7 @@ export const classifyChangedPath = (rel) => {
37
37
 
38
38
  // Strip the quotes and decode escapes BYTE-wise (octal escapes are UTF-8 bytes) — an unparsed
39
39
  // quoted path compares unequal to its classifier/testId form and would silently escape the
40
- // coverage/cap surface (agy R5, AD-047).
40
+ // coverage/cap surface (AD-047).
41
41
  const CQUOTE_SIMPLE = { n: 10, t: 9, r: 13, f: 12, v: 11, b: 8, a: 7, '"': 34, '\\': 92 };
42
42
  export const unquoteDiffPath = (p) => {
43
43
  if (!(p.length >= 2 && p.startsWith('"') && p.endsWith('"'))) return p;
@@ -47,7 +47,7 @@ export const unquoteDiffPath = (p) => {
47
47
  const c = inner[i];
48
48
  if (c !== '\\') {
49
49
  // Consume a full CODE POINT — 16-bit-unit iteration would split a surrogate pair (an
50
- // unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes (agy R6).
50
+ // unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes.
51
51
  const ch = String.fromCodePoint(inner.codePointAt(i));
52
52
  for (const b of Buffer.from(ch, 'utf8')) bytes.push(b);
53
53
  i += ch.length - 1;
@@ -89,7 +89,7 @@ export const parseUnifiedDiff = (diffText) => {
89
89
  }
90
90
  if (inHeader && line.startsWith('--- ')) continue;
91
91
  if (inHeader && line.startsWith('+++ ')) {
92
- const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes (agy R6)
92
+ const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes
93
93
  current = p === '/dev/null' ? null : p.startsWith('b/') ? p.slice(2) : p;
94
94
  inHeader = false;
95
95
  continue;
@@ -114,7 +114,7 @@ export const parseUnifiedDiff = (diffText) => {
114
114
  // ── the changed surface (git-driven; the ONE computation both consumers read) ─────────────────────
115
115
 
116
116
  // The one diff-invocation shape every surface pass uses. The a/ b/ prefixes are pinned EXPLICITLY
117
- // (agy R6): a user's global diff.noprefix=true would otherwise drop them and the parsers would eat
117
+ // A user's global diff.noprefix=true would otherwise drop them and the parsers would eat
118
118
  // a real directory named "a" — user git config must never bend the parse.
119
119
  export const DIFF_FLAGS = ['--unified=0', '--no-color', '--no-ext-diff', '--no-renames', '--src-prefix=a/', '--dst-prefix=b/'];
120
120
 
@@ -134,7 +134,7 @@ export const readFileSafe = (path) => {
134
134
  };
135
135
  // A changed LEAF is read only if it is a REGULAR file. lstat (no-follow): a symlinked or non-regular
136
136
  // leaf must NEVER be read/followed — following it could read outside the work tree or HANG on a
137
- // FIFO/device (codex R2, AD-046). A non-regular leaf fails closed (routed to `unsupported`).
137
+ // FIFO/device (AD-046). A non-regular leaf fails closed (routed to `unsupported`).
138
138
  export const isRegularLeaf = (abs) => {
139
139
  try {
140
140
  return lstatSync(abs).isFile();
@@ -150,13 +150,13 @@ export const isRegularLeaf = (abs) => {
150
150
  // pure deletions cost nothing, subtractive folds stay free); an untracked file is wholly new, so all
151
151
  // its lines are "changed". `unsupportedLines` is the D4 cap's view of the SAME pass: unsupported
152
152
  // SOURCE files carry their new-side lines too (excluding them would gift a large-TS-fold bypass,
153
- // codex R2 / BUGFREE-2 D4); an unreadable/non-regular leaf counts 0 lines but stays LISTED (the
153
+ // BUGFREE-2 D4); an unreadable/non-regular leaf counts 0 lines but stays LISTED (the
154
154
  // coverage gate still fails closed on it). excluded-test and out-of-domain never carry lines.
155
155
  export const computeChangedSurface = (root) => {
156
156
  // Unborn branch (no HEAD yet): the plain diff alone misses files STAGED for the initial commit —
157
157
  // they sit in the index, so they are neither worktree-vs-index changes nor untracked. Merge the
158
158
  // --cached diff (index vs the empty tree) with the plain one; parseUnifiedDiff unions per-file
159
- // lines across concatenated diffs (codex R1).
159
+ // lines across concatenated diffs.
160
160
  const trackedDiff = gitStdout(['diff', 'HEAD', ...DIFF_FLAGS], root)
161
161
  ?? `${gitStdout(['diff', '--cached', ...DIFF_FLAGS], root) ?? ''}\n${gitStdout(['diff', ...DIFF_FLAGS], root) ?? ''}`;
162
162
  const trackedLines = parseUnifiedDiff(trackedDiff);
@@ -207,7 +207,7 @@ export const computeChangedSurface = (root) => {
207
207
  // ── the shared fail-closed positive-integer knob parser (AD-047 precedent) ───────────────────────
208
208
 
209
209
  // Zero / negative / fractional / non-numeric values are refusals by name — the parseInt(...)||default
210
- // idiom would silently accept bad truthy values (codex R2, AD-046). Unset → the default; set → a
210
+ // idiom would silently accept bad truthy values (AD-046). Unset → the default; set → a
211
211
  // positive integer, exactly. `makeError` lets each caller throw its OWN typed STOP.
212
212
  export const parsePositiveIntKnob = (env, name, fallback, makeError = (m) => new Error(m)) => {
213
213
  const raw = env[name];
@@ -231,6 +231,15 @@ const CATALOG = [
231
231
  kind: READ_ONLY,
232
232
  oneLine: 'Check that the documented contract tokens still match the live code constants they describe — a read-only lint that fails closed on drift; --check turns it into a gate exit code.',
233
233
  },
234
+ {
235
+ // NEVER `guarded` — that kind promises dry-run-first, which these writers do not have; the
236
+ // honest strongest caution is `writer` with the destructive arm named in the line itself.
237
+ key: 'worktrees',
238
+ invocation: invocationOf('worktrees'),
239
+ group: 'Orchestrate',
240
+ kind: WRITER,
241
+ oneLine: 'Run features in parallel git worktrees: provision an isolated sibling copy, list them, stage a finished one back onto clean main (the commit still asks in dialogue), and remove a live-verified landed one. No preview step; list is read-only; cleanup --abandon destroys unlanded work.',
242
+ },
234
243
  ];
235
244
 
236
245
  // Deep-freeze: freeze the array AND every entry, so the catalog is genuinely immutable at runtime
@@ -89,7 +89,7 @@ export const handoffPlan = (delegate) =>
89
89
  memoryWrites: ['docs/ai/', 'AGENTS.md', 'docs/ai/.memory-version'],
90
90
  // The lens region runs AFTER the substrate deploy (its own precondition: the file exists)
91
91
  // — it converges a stale-memory seed to the installed engine's canon (AD-041).
92
- kitWrites: ['AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens region', 'docs/ai/.workflow-version'],
92
+ kitWrites: ['AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens + Communication regions', 'docs/ai/.workflow-version'],
93
93
  stampsPresent: ['.memory-version', '.workflow-version'],
94
94
  memoryRaisesCommitGate: false,
95
95
  commitGate: 'kit-only-after-injection',
@@ -101,7 +101,7 @@ export const handoffPlan = (delegate) =>
101
101
  // the kit reconciles them (ensure-slot + inject-because-empty) exactly like the delegate
102
102
  // path — so both paths end with FILLED slots, not inline methodology. The lens region
103
103
  // runs after the fallback-template copy of docs/ai (same reconcile, both paths — AD-041).
104
- kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens region', 'docs/ai/.workflow-version'],
104
+ kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens + Communication regions', 'docs/ai/.workflow-version'],
105
105
  stampsPresent: ['.workflow-version'],
106
106
  memoryRaisesCommitGate: false,
107
107
  commitGate: 'kit-only-after-injection',
@@ -82,6 +82,10 @@ const RAW_BACKENDS = [
82
82
  },
83
83
  notes: [
84
84
  'nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket',
85
+ 'exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture — exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=… — from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in any banner field refuse pre-spend',
86
+ 'threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent environment. A hostile parent environment — including exported shell functions or PATH substitution of core/backend commands — is outside the threat model and can substitute the backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental shadowing; it is not an environment security boundary',
87
+ 'the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it is never persisted in a receipt or session sidecar',
88
+ 'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
85
89
  ],
86
90
  },
87
91
  review: {
@@ -92,6 +96,10 @@ const RAW_BACKENDS = [
92
96
  grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
93
97
  continue: [],
94
98
  receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit\'s review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized \'Verdict: <ship|revise|rethink>\' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review',
99
+ notes: [
100
+ 'the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity',
101
+ 'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
102
+ ],
95
103
  },
96
104
  },
97
105
  bin: 'codex',
@@ -128,6 +136,8 @@ const RAW_BACKENDS = [
128
136
  receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
129
137
  notes: [
130
138
  'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
139
+ 'the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration agy-run hands to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity',
140
+ 'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
131
141
  ],
132
142
  },
133
143
  },
@@ -102,7 +102,7 @@ export const assembleGrounding = ({ constraintsText = null, autonomyText = null,
102
102
  // Trim `payload` to `budget` bytes, tail-first, with a loud in-band marker. The budget is a HARD
103
103
  // ceiling: when it is smaller than the marker itself, the marker is truncated too (the output may
104
104
  // never exceed `budget` — a facts file over the reserved share would push the final wrapper prompt
105
- // past the argv ceiling; codex R1 finding). Returns { text, trimmedBytes }. Never a silent cut.
105
+ // past the argv ceiling). Returns { text, trimmedBytes }. Never a silent cut.
106
106
  export const trimToBudget = (payload, budget) => {
107
107
  const bytes = Buffer.byteLength(payload, 'utf8');
108
108
  if (bytes <= budget) return { text: payload, trimmedBytes: 0 };
@@ -164,17 +164,17 @@ const gitLine = (args, cwd) => {
164
164
  // EXISTING in-repo file, even gitignored, is a project file — the .env clobber class). Refused:
165
165
  // tracked paths, in-repo not-ignored paths (a new untracked file would move the fingerprint),
166
166
  // every other outside-repo destination, symlink and existing non-regular leaves, and any
167
- // unverifiable lstat. The check runs on the REAL destination, never the lexical path (codex R1):
167
+ // unverifiable lstat. The check runs on the REAL destination, never the lexical path:
168
168
  // the parent directory is realpath-resolved first, so a symlink can never route the write onto a
169
169
  // tracked/in-repo file. Returns { path, kind: 'temp' | 'repo-fresh' } — a repo-fresh caller MUST
170
- // write with the exclusive flag ('wx'), sealing the guard→write race (codex R11).
170
+ // write with the exclusive flag ('wx'), sealing the guard→write race.
171
171
  export const assertScratchDestination = (outPath, cwd) => {
172
172
  const lexical = isAbsolute(outPath) ? outPath : resolve(cwd, outPath);
173
173
  let leaf = null;
174
174
  try {
175
175
  leaf = lstatSync(lexical);
176
176
  } catch (err) {
177
- // ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (codex R9 — this
177
+ // ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (this
178
178
  // writer is bridge-tier auto-allowable, so an unverifiable leaf must never be written).
179
179
  if (err?.code !== 'ENOENT') {
180
180
  throw fail(1, `--out cannot inspect the destination (${outPath}: ${err?.code ?? err?.message ?? err}) — refusing to write an unverifiable leaf`);
@@ -185,7 +185,7 @@ export const assertScratchDestination = (outPath, cwd) => {
185
185
  throw fail(1, `--out refuses a symlink destination (${outPath}) — the write would follow it onto another file; name the real scratch path`);
186
186
  }
187
187
  if (leaf != null && !leaf.isFile()) {
188
- throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file (codex R9); name a fresh or regular scratch path`);
188
+ throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file; name a fresh or regular scratch path`);
189
189
  }
190
190
  let realParent;
191
191
  try {
@@ -194,8 +194,8 @@ export const assertScratchDestination = (outPath, cwd) => {
194
194
  throw fail(1, `--out parent directory does not exist (${dirname(lexical)}) — create the scratch dir first`);
195
195
  }
196
196
  const full = join(realParent, basename(lexical));
197
- // An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root (codex R8
198
- // blocker): the bridge tier auto-allows this tool with an args wildcard, so "anything outside
197
+ // An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root: the bridge
198
+ // tier auto-allows this tool with an args wildcard, so "anything outside
199
199
  // the repo is scratch" would let an unattended run overwrite e.g. ~/.bashrc promptless.
200
200
  // $TMPDIR / os.tmpdir() / /tmp are the scratch surface; everything else refuses loudly.
201
201
  const assertTempScratch = () => {
@@ -215,7 +215,7 @@ export const assertScratchDestination = (outPath, cwd) => {
215
215
  if (top == null || top.status !== 0) return { path: assertTempScratch().path, kind: 'temp' }; // no git tree → temp-only scratch
216
216
  const root = top.stdout.replace(/\r?\n$/, '');
217
217
  const rel = relative(root, full);
218
- // Segment-safe outside test (the Issue-004 class, codex R5): an in-repo file literally named
218
+ // Segment-safe outside test (the Issue-004 class): an in-repo file literally named
219
219
  // `..facts` has rel `..facts` — `startsWith('..')` would misread it as outside and BYPASS the
220
220
  // tracked/ignored refusals below. Only `..` exactly or a `..${sep}`-prefixed rel is outside.
221
221
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { path: assertTempScratch().path, kind: 'temp' };
@@ -227,11 +227,11 @@ export const assertScratchDestination = (outPath, cwd) => {
227
227
  if (ignored == null || ignored.status !== 0) {
228
228
  throw fail(1, `--out refuses an in-repo path that is not gitignored (${rel}) — a new untracked file would move the review fingerprint; use a gitignored path or a location outside the repo`);
229
229
  }
230
- // An in-repo destination must be a FRESH file (codex R10): even a gitignored existing file is a
230
+ // An in-repo destination must be a FRESH file: even a gitignored existing file is a
231
231
  // project file (.env is the canonical victim) — an auto-allowable writer must never clobber one.
232
232
  // The rewritable scratch surface is the system temp; in-repo gitignored writes are create-only —
233
233
  // the CALLER must open with the exclusive flag ('wx'): the kind seals the pre-check against the
234
- // guard→write race (codex R11).
234
+ // guard→write race.
235
235
  if (leaf != null) {
236
236
  throw fail(1, `--out refuses to OVERWRITE an existing in-repo file (${rel}) — even gitignored, it is a project file (the .env class); write rewritable scratch under $TMPDIR//tmp, or remove the file first`);
237
237
  }
@@ -328,7 +328,7 @@ export const main = (argv, ctx = {}) => {
328
328
  };
329
329
  const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
330
330
  const autonomyText = autonomy ? resolveAutonomyFacts({ cwd }) : null;
331
- // --plan is CONFINED to the git work tree (codex R4): the bridge tier auto-allows this tool
331
+ // --plan is CONFINED to the git work tree: the bridge tier auto-allows this tool
332
332
  // with an args wildcard, so an unattended invocation could otherwise point --plan at ANY
333
333
  // readable file and ship it into the facts payload. Plans are in-repo by contract
334
334
  // (docs/plans); an outside-tree path is a loud refusal, never a silent read.
@@ -360,7 +360,7 @@ export const main = (argv, ctx = {}) => {
360
360
  if (out != null) {
361
361
  const dest = assertScratchDestination(out, cwd);
362
362
  // repo-fresh writes are EXCLUSIVE ('wx'): the create-only pre-check would otherwise race a
363
- // file created between the guard and this write (codex R11); temp scratch stays rewritable.
363
+ // file created between the guard and this write; temp scratch stays rewritable.
364
364
  // ctx.writeFile is a TEST seam (the EEXIST race arm is not constructible without it).
365
365
  const writeFile = ctx.writeFile ?? writeFileSync;
366
366
  try {
@@ -380,7 +380,7 @@ export const main = (argv, ctx = {}) => {
380
380
  const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
381
381
  if (isDirectRun) {
382
382
  const r = main(process.argv.slice(2));
383
- // Exact writes + a natural exit (codex R2): console.log would append a stray newline to the
383
+ // Exact writes + a natural exit: console.log would append a stray newline to the
384
384
  // byte-exact facts payload, and process.exit() can truncate large piped stdout before Node
385
385
  // flushes. The payload already ends with '\n' (sliceSection normalizes); only a non-payload
386
386
  // message (help / the --out report) gains the terminating newline it lacks.