@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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 (107) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/README.md +7 -5
  3. package/SKILL.md +16 -8
  4. package/bin/install.mjs +11 -2
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
  10. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
  11. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  12. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
  14. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
  17. package/bridges/codex-cli-bridge/capability.json +3 -2
  18. package/capability.json +1 -1
  19. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  20. package/migrations/README.md +1 -1
  21. package/package.json +2 -2
  22. package/references/hooks/gate-approve.mjs +1 -1
  23. package/references/modes/bootstrap.md +3 -3
  24. package/references/modes/commit-guard.md +19 -0
  25. package/references/modes/core-evidence.md +15 -0
  26. package/references/modes/coverage-check.md +12 -0
  27. package/references/modes/doc-parity.md +7 -6
  28. package/references/modes/gates.md +7 -6
  29. package/references/modes/grounding.md +1 -2
  30. package/references/modes/hook.md +1 -1
  31. package/references/modes/recommendations.md +1 -0
  32. package/references/modes/review-state.md +3 -3
  33. package/references/modes/upgrade.md +6 -6
  34. package/references/modes/velocity.md +2 -2
  35. package/references/modes/worktrees.md +120 -0
  36. package/references/scripts/archive-decisions.mjs +3 -3
  37. package/references/scripts/archive-decisions.test.mjs +5 -5
  38. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  39. package/references/scripts/check-docs-size.mjs +46 -29
  40. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  41. package/references/scripts/install-git-hooks.mjs +90 -18
  42. package/references/scripts/install-git-hooks.test.mjs +102 -0
  43. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  44. package/references/scripts/migrate-gates.mjs +395 -0
  45. package/references/scripts/migrate-gates.test.mjs +284 -0
  46. package/references/templates/agent_rules.md +2 -2
  47. package/tools/ack-write.mjs +1 -1
  48. package/tools/atomic-write.mjs +2 -2
  49. package/tools/autonomy-config.mjs +1 -1
  50. package/tools/autonomy-doctor.mjs +2 -2
  51. package/tools/autonomy-write.mjs +1 -1
  52. package/tools/bridge-settings-read.mjs +1 -1
  53. package/tools/bridge-settings.mjs +1 -1
  54. package/tools/changed-surface.mjs +16 -77
  55. package/tools/cheap-agents.mjs +2 -2
  56. package/tools/commands.mjs +26 -10
  57. package/tools/commit-guard.mjs +167 -0
  58. package/tools/core-evidence.mjs +914 -0
  59. package/tools/coverage-check.mjs +260 -0
  60. package/tools/delegation.mjs +1 -1
  61. package/tools/detect-backends.mjs +3 -3
  62. package/tools/doc-parity.mjs +11 -27
  63. package/tools/engine-source.mjs +1 -1
  64. package/tools/family-members.mjs +1 -1
  65. package/tools/family-registry.mjs +1 -1
  66. package/tools/fs-safe.mjs +1 -1
  67. package/tools/gate-hook.mjs +1 -1
  68. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  69. package/tools/grounding.mjs +19 -96
  70. package/tools/hide-footprint.mjs +1 -1
  71. package/tools/inject-methodology.mjs +72 -41
  72. package/tools/known-footprint.mjs +1 -1
  73. package/tools/labels.mjs +1 -1
  74. package/tools/lcov.mjs +6 -10
  75. package/tools/lens-region.mjs +1 -1
  76. package/tools/manifest/validate.mjs +19 -0
  77. package/tools/migrate-adr-store.mjs +5 -5
  78. package/tools/orchestration-config.mjs +1 -1
  79. package/tools/orchestration-write.mjs +1 -1
  80. package/tools/presentation.mjs +1 -1
  81. package/tools/procedures.mjs +3 -3
  82. package/tools/recipes.mjs +62 -11
  83. package/tools/recommendations.mjs +97 -14
  84. package/tools/release-scan.mjs +10 -3
  85. package/tools/renderers.mjs +1 -1
  86. package/tools/review-state.mjs +291 -343
  87. package/tools/run-gates.mjs +216 -92
  88. package/tools/sandbox-masks.mjs +14 -14
  89. package/tools/semver-lite.mjs +1 -1
  90. package/tools/set-autonomy.mjs +1 -1
  91. package/tools/set-recipe.mjs +2 -2
  92. package/tools/setup-backends.mjs +1 -1
  93. package/tools/surface.mjs +1 -1
  94. package/tools/uninstall.mjs +1 -1
  95. package/tools/velocity-profile.mjs +9 -9
  96. package/tools/view-model.mjs +1 -1
  97. package/tools/worktrees.mjs +2292 -0
  98. package/references/modes/fold-completeness.md +0 -30
  99. package/references/modes/review-ledger.md +0 -34
  100. package/references/templates/verification-profile.json +0 -10
  101. package/tools/fold-completeness-run.mjs +0 -1120
  102. package/tools/fold-completeness.mjs +0 -672
  103. package/tools/review-ledger-core.mjs +0 -428
  104. package/tools/review-ledger-write.mjs +0 -647
  105. package/tools/review-ledger.mjs +0 -630
  106. package/tools/sarif.mjs +0 -52
  107. package/tools/verification-profile.mjs +0 -219
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // seed-gates.mjs — the consent-gated docs/ai/gates.json seeder (AD-042). Reached ONLY through
3
- // explicitly-consenting prose (the bootstrap accelerators block and the gates.md consent-seed
4
- // section) it is NOT a routable mode token, it sits OUTSIDE every velocity allowlist tier
5
- // (a consent-per-run writer is never pre-approved), and the shipped gates.json TEMPLATE stays
6
- // EMPTY (AD-021/AD-038): a populated declaration is per-entry maintainer consent recorded through
7
- // this preview, never auto-seeding.
2
+ // gates-init.mjs — the consented docs/ai/gates.json FILL preview (D9; the pure derivation carried
3
+ // from the retired standalone seeder, AD-042/AD-052 invariants intact). Reached ONLY through
4
+ // explicitly-consenting prose (the init/bootstrap flows and the gates.md consent-fill section)
5
+ // it is NOT a routable mode token, it sits OUTSIDE every velocity allowlist tier (a
6
+ // consent-per-run writer is never pre-approved), and the shipped gates.json TEMPLATE stays EMPTY
7
+ // (AD-021/AD-038): a populated declaration is per-entry maintainer consent recorded through this
8
+ // preview, never auto-seeding. ONE lifecycle contract: this preview runs at INIT; at UPGRADE the
9
+ // only gates.json writer is the consented legacy migration.
8
10
  //
9
11
  // What it offers (the derivation invariants, test-pinned) — CLOSED-WORLD since AD-052: an entry
10
12
  // is offered only when every axis is proven safe by MEMBERSHIP in a finite, test-guarded set,
@@ -29,7 +31,7 @@
29
31
  // executes project-controlled tooling (a node_modules/.bin PATH shim intercepts under every
30
32
  // form) — the documented residual, disclosed in the preview, bounded by the two consents;
31
33
  // • ids derive kebab-case from script names (build:prod → build-prod) and every offered entry
32
- // passes the runner's validateDeclaration (the seeder imports the validator — NEVER the
34
+ // passes the runner's validateDeclaration (this module imports the validator — NEVER the
33
35
  // reverse: run-gates.mjs stays a runner that writes nothing);
34
36
  // • the review-state candidate appears ONLY when docs/ai/orchestration.json DECLARES
35
37
  // reviewed/council on plan-execution.review — the slot the checker enforces — with the
@@ -44,7 +46,7 @@
44
46
  // .workflow-version == lineage-head stamp gate on --apply only (the velocity/gate-hook precedent).
45
47
  //
46
48
  // Exit codes: 0 done / dry-run; 1 precondition STOP (no deployment, stamp, symlink, malformed
47
- // declaration, id collision); 2 usage. Dependency-free, Node >= 18. No side effects on import.
49
+ // declaration, id collision); 2 usage. Dependency-free, Node >= 22. No side effects on import.
48
50
 
49
51
  import { existsSync, lstatSync, readFileSync } from 'node:fs';
50
52
  import { join, resolve, dirname } from 'node:path';
@@ -58,39 +60,40 @@ const HERE = dirname(fileURLToPath(import.meta.url));
58
60
  const KIT_ROOT = resolve(HERE, '..');
59
61
  const TEMPLATE_PATH = join(KIT_ROOT, 'references', 'templates', 'gates.json');
60
62
  const REVIEW_STATE_TOOL = join(KIT_ROOT, 'tools', 'review-state.mjs');
61
- const REVIEW_LEDGER_TOOL = join(KIT_ROOT, 'tools', 'review-ledger.mjs');
63
+ const COVERAGE_CHECK_TOOL = join(KIT_ROOT, 'tools', 'coverage-check.mjs');
62
64
  const STAMP_REL = join('docs', 'ai', '.workflow-version');
63
65
 
64
66
  const EXIT_OK = 0;
65
67
  const EXIT_PRECONDITION = 1;
66
68
  const EXIT_USAGE = 2;
67
69
 
68
- export const SEED_GATES_STOP = 'SEED_GATES_STOP';
70
+ export const GATES_INIT_STOP = 'GATES_INIT_STOP';
69
71
  const stop = (message) =>
70
- Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'SeedGatesStop', code: SEED_GATES_STOP, exitCode: EXIT_PRECONDITION });
72
+ Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'GatesInitStop', code: GATES_INIT_STOP, exitCode: EXIT_PRECONDITION });
71
73
  const usageFail = (message) =>
72
74
  Object.assign(new Error(`[agent-workflow-kit] ${message}`), { exitCode: EXIT_USAGE });
73
75
 
74
76
  // The trust-chain disclosure (AD-042) — printed with EVERY preview, token-pinned by the tests: a
75
77
  // consent-seeded command becomes auto-approvable only after TWO explicit consents.
76
78
  export const TRUST_CHAIN_DISCLOSURE =
77
- 'Disclosure: once the optional approval hook is wired (/agent-workflow-kit hook), it ' +
78
- 'auto-approves byte-exact declared gate commands from the project root — seeding (this consent) ' +
79
- 'and hook wiring (its own consent) are two separate yeses. A script gate runs the project\'s ' +
80
- 'own tooling, which executes project-controlled code (the seeder does not sandbox it; the two ' +
81
- 'consents bound it).';
82
-
83
- const USAGE = `usage: seed-gates [--dry-run | --apply] [--only <id>]... [--cwd <dir>] [--help]
84
-
85
- Consent-gated seeder for the project's own docs/ai/gates.json. Default is --dry-run: prints the
86
- derived { id, title, cmd } entries and writes NOTHING. --apply APPENDS exactly the consented
87
- entries (--only <id> selects a subset; append-only — existing entries are never modified or
88
- removed, an id collision is refused). The offer is CLOSED-WORLD: only a terminating-class script
89
- name (test / lint / type-check / build — never release/publish/deploy, never watch/serve, never a
90
- write-mode variant) whose BODY is a member of the literal runner allowlist is offered, as the
91
- hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for the detected package manager. A gate-class script whose
92
- body is NOT in the allowlist is screened out with a note naming it (a command you trust can still
93
- be declared by hand); non-gate-class names are excluded silently, as always.
79
+ 'Disclosure: gates.json is a PRIVILEGED file — once the optional approval hook is wired ' +
80
+ '(/agent-workflow-kit hook), it auto-approves byte-exact declared gate commands from the ' +
81
+ 'project root — filling (this consent) and hook wiring (its own consent) are two separate ' +
82
+ 'yeses. A script gate runs the project\'s own tooling, which executes project-controlled code ' +
83
+ '(this preview does not sandbox it; the two consents bound it).';
84
+
85
+ const USAGE = `usage: gates-init [--dry-run | --apply] [--only <id>]... [--cwd <dir>] [--help]
86
+
87
+ The consented FILL preview for the project's own docs/ai/gates.json (D9). Default is --dry-run:
88
+ prints the derived { id, title, cmd } entries and writes NOTHING. --apply APPENDS exactly the
89
+ consented entries (--only <id> selects a subset; append-only — existing entries are never
90
+ modified or removed, an id collision is refused). The offer is CLOSED-WORLD: only a
91
+ terminating-class script name (test / lint / type-check / build — never release/publish/deploy,
92
+ never watch/serve, never a write-mode variant) whose BODY is a member of the literal runner
93
+ allowlist is offered, as the hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for
94
+ the detected package manager. A gate-class script whose body is NOT in the allowlist is screened
95
+ out with a note naming it (a command you trust can still be declared by hand); non-gate-class
96
+ names are excluded silently, as always.
94
97
  ${TRUST_CHAIN_DISCLOSURE}`;
95
98
 
96
99
  // ── candidate classification: the NAME screens (the LOCKED derivation invariants) ──────
@@ -310,12 +313,14 @@ export const reviewStateCandidate = (cwd, deps = {}) => {
310
313
  }
311
314
  };
312
315
 
313
- // The conditional review-LEDGER candidate (AD-045) — the SAME consent + conditional rule as the
314
- // review-state candidate (offered ONLY when plan-execution.review is reviewed/council), keyed on the
315
- // same slot, path resolved + QUOTED. It gates the review-ROUND ledger (converged / resolved-residual);
316
- // review-state gates receipt PRESENCE. Both may be offered together distinct axes.
317
- export const reviewLedgerCandidate = (cwd, deps = {}) => {
318
- const toolPath = deps.reviewLedgerTool ?? REVIEW_LEDGER_TOOL;
316
+ // The conditional COVERAGE-CHECK candidate (D3(a)) — the SAME consent + conditional rule as the
317
+ // review-state candidate (offered ONLY when plan-execution.review is reviewed/council), keyed on
318
+ // the same slot, path resolved + QUOTED. Together they are the canonical core pair `run-gates
319
+ // --final` requires (the checker declared LAST buildOffer appends it last so a whole-offer
320
+ // apply lands final-ready); review-state gates receipt satisfaction, coverage-check verifies the
321
+ // consumed lcov + the red-proof declarations.
322
+ export const coverageCheckCandidate = (cwd, deps = {}) => {
323
+ const toolPath = deps.coverageCheckTool ?? COVERAGE_CHECK_TOOL;
319
324
  try {
320
325
  const { config } = loadConfig(resolve(cwd), deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
321
326
  const declared = config?.['plan-execution']?.review;
@@ -324,15 +329,15 @@ export const reviewLedgerCandidate = (cwd, deps = {}) => {
324
329
  return {
325
330
  candidate: null,
326
331
  note:
327
- `the review-ledger candidate was withheld: the resolved kit path contains shell ` +
332
+ `the coverage-check candidate was withheld: the resolved kit path contains shell ` +
328
333
  `metacharacters that do not survive double-quoting (${toolPath}) — declare the gate ` +
329
- `by hand per references/modes/review-ledger.md`,
334
+ `by hand per references/modes/coverage-check.md`,
330
335
  };
331
336
  }
332
337
  return {
333
338
  candidate: {
334
- id: 'review-ledger',
335
- title: 'Review-round ledger: the in-flight loop is converged or resolved-residual',
339
+ id: 'coverage-check',
340
+ title: 'Changed-line coverage + red-proof verification (the final-run checker)',
336
341
  cmd: `node "${toolPath}" --check`,
337
342
  },
338
343
  note: null,
@@ -340,7 +345,7 @@ export const reviewLedgerCandidate = (cwd, deps = {}) => {
340
345
  } catch (err) {
341
346
  return {
342
347
  candidate: null,
343
- note: `orchestration config unreadable (${err.message}) — the review-ledger candidate was not evaluated`,
348
+ note: `orchestration config unreadable (${err.message}) — the coverage-check candidate was not evaluated`,
344
349
  };
345
350
  }
346
351
  };
@@ -356,22 +361,23 @@ const assertOnlyIdsOffered = (offer, onlyIds = []) => {
356
361
  }
357
362
  };
358
363
 
359
- // The full offer: script entries + the conditional review-state + review-ledger candidates (last),
360
- // plus loud notes. Both review candidates key on the same slot (plan-execution.review reviewed/council)
361
- // but gate distinct axes (receipt presence vs review-round convergence) offered together.
364
+ // The full offer: script entries + the conditional review-state + coverage-check candidates
365
+ // (coverage-check LAST the `run-gates --final` declaration-shape rule requires the checker as
366
+ // the last declared gate, so a whole-offer apply is final-ready by construction). Both key on the
367
+ // same slot (plan-execution.review reviewed/council) but gate distinct axes.
362
368
  export const buildOffer = (cwd, deps = {}) => {
363
369
  const scripts = deriveScripts(cwd, deps);
364
370
  const rs = reviewStateCandidate(cwd, deps);
365
- const rl = reviewLedgerCandidate(cwd, deps);
366
- const candidates = [rs.candidate, rl.candidate].filter(Boolean);
371
+ const cc = coverageCheckCandidate(cwd, deps);
372
+ const candidates = [rs.candidate, cc.candidate].filter(Boolean);
367
373
  return {
368
374
  entries: [...scripts.entries, ...candidates],
369
- notes: [...scripts.notes, rs.note, rl.note].filter(Boolean),
375
+ notes: [...scripts.notes, rs.note, cc.note].filter(Boolean),
370
376
  };
371
377
  };
372
378
 
373
379
  // The RUNNABLE apply invocation for a given project — this tool has no bin and no mode token, so
374
- // the consent step must print the real command, never a bare `seed-gates`. Consent integrity: a
380
+ // the consent step must print the real command, never a bare `gates-init`. Consent integrity: a
375
381
  // previewed --only subset is carried into the hint VERBATIM (ids are shell-safe by construction —
376
382
  // kebabIdOf output / the fixed `review-state`), so following the hint can never widen the consent.
377
383
  // Paths that do not survive double-quoting (the reviewStateCandidate screen, same pattern) make
@@ -386,7 +392,7 @@ export const applyInvocationFor = (cwd, onlyIds = []) => {
386
392
  const GENERIC_APPLY_HINT = 're-run this same command with --apply [--only <id>]...';
387
393
 
388
394
  export const formatPreview = (offer, applyInvocation = null, { explicitOnly = false } = {}) => {
389
- const lines = ['[agent-workflow-kit] gates seeding preview (dry-run — nothing was written):'];
395
+ const lines = ['[agent-workflow-kit] gates fill preview (dry-run — nothing was written):'];
390
396
  if (!offer.entries.length) {
391
397
  lines.push(' nothing to offer — no seedable terminating verification scripts were found.');
392
398
  }
@@ -418,14 +424,14 @@ const loadExistingDeclaration = (cwd, deps = {}) => {
418
424
  try {
419
425
  return JSON.parse(String(read(full, 'utf8')));
420
426
  } catch (err) {
421
- throw stop(`${GATES_REL}: malformed JSON (${err.message}) — fix it by hand; the seeder never writes over a declaration it cannot parse`);
427
+ throw stop(`${GATES_REL}: malformed JSON (${err.message}) — fix it by hand; this preview never writes over a declaration it cannot parse`);
422
428
  }
423
429
  })();
424
430
  const gates = (() => {
425
431
  try {
426
432
  return validateDeclaration(parsed);
427
433
  } catch (err) {
428
- throw stop(`${err.message} — fix it by hand; the seeder never writes over an invalid declaration`);
434
+ throw stop(`${err.message} — fix it by hand; this preview never writes over an invalid declaration`);
429
435
  }
430
436
  })();
431
437
  return { outcome: 'loaded', readme: typeof parsed._README === 'string' ? parsed._README : undefined, gates };
@@ -453,7 +459,7 @@ const readStampValue = (cwd, deps = {}) => {
453
459
  };
454
460
 
455
461
  // ── apply (append exactly the consented entries) ───────────────────────────────────────
456
- export const applySeed = ({ cwd, onlyIds = [] }, deps = {}) => {
462
+ export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
457
463
  assertDocsAiDeployment(cwd, deps, { stop, noun: 'a gate declaration', rel: GATES_REL });
458
464
  const stampValue = readStampValue(cwd, deps);
459
465
  if (stampValue !== EXPECTED_WORKFLOW_VERSION) {
@@ -474,7 +480,7 @@ export const applySeed = ({ cwd, onlyIds = [] }, deps = {}) => {
474
480
  if (collisions.length) {
475
481
  throw stop(
476
482
  `id collision — already declared in ${GATES_REL}: ${collisions.join(', ')} (append-only: the ` +
477
- `seeder never modifies or removes an existing entry; pick the others with --only, or edit by hand)`,
483
+ `fill never modifies or removes an existing entry; pick the others with --only, or edit by hand)`,
478
484
  );
479
485
  }
480
486
 
@@ -539,7 +545,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
539
545
  log(formatPreview(filtered, applyInvocationFor(cwd, args.only), { explicitOnly: args.only.length > 0 }));
540
546
  return EXIT_OK;
541
547
  }
542
- const result = applySeed({ cwd, onlyIds: args.only }, deps);
548
+ const result = applyFill({ cwd, onlyIds: args.only }, deps);
543
549
  if (result.outcome === 'nothing') {
544
550
  log('[agent-workflow-kit] nothing to offer — no seedable terminating verification scripts were found; wrote nothing.');
545
551
  for (const note of result.notes) log(` note: ${note}`); // the user learns WHY (same note as the preview)
@@ -23,7 +23,7 @@
23
23
  // not-ignored path (a new untracked file would itself move the review fingerprint the facts are
24
24
  // about to ground). stdout is the default. It never commits and never runs a subscription CLI.
25
25
  //
26
- // Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
26
+ // Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
27
27
 
28
28
  import { readFileSync, writeFileSync, lstatSync, realpathSync } from 'node:fs';
29
29
  import { resolve, join, relative, isAbsolute, dirname, basename, sep } from 'node:path';
@@ -31,11 +31,6 @@ import { tmpdir } from 'node:os';
31
31
  import { pathToFileURL } from 'node:url';
32
32
  import { spawnSync } from 'node:child_process';
33
33
  import { fail } from './orchestration-config.mjs';
34
- // (e) --ledger-summary (BUGFREE-3 / AD-049): a computed, loop/base-SCOPED review-ledger digest for
35
- // the facts payload. The review ledger is read-only here — grounding never writes it. ORIGINS +
36
- // computeTelemetry give the counts; the segment filter gives the scope.
37
- import { ORIGINS, computeTelemetry, filterSegmentRecords, readLedger, resolveLedgerPath, resolveBase } from './review-ledger.mjs';
38
- import { plansInFlight } from './review-state.mjs';
39
34
  // (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
40
35
  // READ core only — never autonomy-write.mjs (the import-split invariant).
41
36
  import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
@@ -107,7 +102,7 @@ export const assembleGrounding = ({ constraintsText = null, autonomyText = null,
107
102
  // Trim `payload` to `budget` bytes, tail-first, with a loud in-band marker. The budget is a HARD
108
103
  // ceiling: when it is smaller than the marker itself, the marker is truncated too (the output may
109
104
  // never exceed `budget` — a facts file over the reserved share would push the final wrapper prompt
110
- // 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.
111
106
  export const trimToBudget = (payload, budget) => {
112
107
  const bytes = Buffer.byteLength(payload, 'utf8');
113
108
  if (bytes <= budget) return { text: payload, trimmedBytes: 0 };
@@ -119,72 +114,10 @@ export const trimToBudget = (payload, budget) => {
119
114
  return { text, trimmedBytes: bytes - keep };
120
115
  };
121
116
 
122
- // ── (e) --ledger-summary: a loop/base-SCOPED review-ledger digest (read-only, computed) ─────────
123
- // `computeTelemetry` aggregates ALL loops and is not per-round-facts-shaped, so this renders the
124
- // in-flight SEGMENT only (plan-execution, loop, base) — the telemetry COUNTS filtered to that
125
- // segment, plus a terse per-round/triage/override render. Deliberately compact + distinct from the
126
- // review-ledger `--status` human view (that one is indented + segment-grouped): this is a byte-
127
- // budgeted FACTS block that agy/codex review against, so unrelated loops are excluded by construction.
128
-
129
- const baseTag = (base) => (base === null ? '(unborn branch)' : String(base).slice(0, 12));
130
- const countsOf = (obj) => {
131
- const keys = Object.keys(obj).sort();
132
- return keys.length === 0 ? '(none)' : keys.map((k) => `${k}:${obj[k]}`).join(' ');
133
- };
134
- const summaryRoundLine = (r) =>
135
- `round ${r.round} — ${r.backends.map((b) => `${b.backend} ${b.degraded ? `degraded(${b.reason})` : `${b.blockers}/${b.majors}/${b.minors} ${b.verdict}`}`).join(', ')}` +
136
- (r.findings.length ? ` [${r.findings.map((f) => `${f.findingKey}(${f.severity})`).join(', ')}]` : '');
137
- const summaryTriageLine = (t) => `triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}`).join(', ')}`;
138
- const summaryOverrideLine = (o) =>
139
- `override @round ${o.round} [${o.scope}] — ${o.scope === 'oracle-change' ? o.files.join(', ') : o.scope === 'size-cap' ? `sanctioned ${o.sanctionedLines} lines` : o.testId}: ${o.reason}`;
140
- const summaryGateRunLine = (g) => `gate-run — status=${g.summary.status} ${g.summary.passed}/${g.summary.gates} green`;
141
- const summaryRecordLine = (r) =>
142
- r.kind === 'round' ? summaryRoundLine(r) : r.kind === 'triage' ? summaryTriageLine(r) : r.kind === 'override' ? summaryOverrideLine(r) : summaryGateRunLine(r);
143
-
144
- // renderLedgerSummary(records, { loop, base }) → the scoped facts section, or '' when the segment
145
- // holds no records (empty/absent → nothing to ground). Pure: the caller does the I/O.
146
- export const renderLedgerSummary = (records, { loop, base }) => {
147
- const segment = filterSegmentRecords(records, { activity: PLAN_EXECUTION, loop, base });
148
- if (segment.length === 0) return '';
149
- const t = computeTelemetry(segment, []).loops[0];
150
- const lines = [
151
- `## Review-ledger summary — loop ${loop} @ base ${baseTag(base)}`,
152
- '',
153
- `rounds ${t.rounds} · divergence-rounds ${t.divergenceRounds}`,
154
- `finding origins — ${ORIGINS.map((k) => `${k}:${t.origins[k]}`).join(' ')}`,
155
- `classifications — ${countsOf(t.classifications)}`,
156
- `backend verdicts — ${Object.keys(t.backendVerdicts).sort().map((b) => `${b}{${countsOf(t.backendVerdicts[b])}}`).join(' · ') || '(none)'}`,
157
- `overrides — ${countsOf(t.overrides)}`,
158
- '',
159
- ...segment.map(summaryRecordLine),
160
- ];
161
- return `${lines.join('\n')}\n`;
162
- };
163
-
164
- // resolveLedgerSummary({ cwd, env }) → the scoped section text for the SINGLE in-flight plan-execution
165
- // segment. A loud STOP unless exactly one plan is in flight (the family's single-plan discipline —
166
- // never guess which loop to ground). Reads the ledger read-only.
167
117
  const gitTop = (cwd) => {
168
118
  const r = gitLine(['rev-parse', '--show-toplevel'], cwd);
169
119
  return r && r.status === 0 ? r.stdout.replace(/\r?\n$/, '') : cwd;
170
120
  };
171
- export const resolveLedgerSummary = ({ cwd, env }) => {
172
- const root = gitTop(cwd);
173
- const plans = plansInFlight(root);
174
- if (plans.length !== 1) {
175
- throw fail(1, `--ledger-summary needs exactly one in-flight plan (in flight: ${plans.length ? plans.join(', ') : 'none'}) — resolve to one active plan`);
176
- }
177
- const loop = plans[0].replace(/\.md$/, '');
178
- const base = resolveBase(cwd);
179
- const ledgerPath = resolveLedgerPath(cwd, env);
180
- const { records, readError, malformed, malformedReasons } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0 };
181
- // Fail CLOSED like every sibling ledger reader (No-silent-failures): an unreadable OR malformed
182
- // ledger must never silently render an empty/partial digest the reviewer then grounds against — an
183
- // empty section would be indistinguishable from a legitimately-empty segment.
184
- if (readError) throw fail(1, `--ledger-summary cannot read the ledger (${readError}) — failing closed; inspect ${ledgerPath}`);
185
- if (malformed > 0) throw fail(1, `--ledger-summary: the ledger has ${malformed} malformed line(s) — failing closed (a dropped line could hide a round/finding): ${malformedReasons.join('; ')}`);
186
- return renderLedgerSummary(records, { loop, base });
187
- };
188
121
 
189
122
  // ── (f) --autonomy: the effective autonomy policy as a computed facts block (AD-044 Plan 3) ─────
190
123
  // The ONE shared resolver (resolveAutonomy) renders the FULL effective policy — every red-line and
@@ -231,17 +164,17 @@ const gitLine = (args, cwd) => {
231
164
  // EXISTING in-repo file, even gitignored, is a project file — the .env clobber class). Refused:
232
165
  // tracked paths, in-repo not-ignored paths (a new untracked file would move the fingerprint),
233
166
  // every other outside-repo destination, symlink and existing non-regular leaves, and any
234
- // 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:
235
168
  // the parent directory is realpath-resolved first, so a symlink can never route the write onto a
236
169
  // tracked/in-repo file. Returns { path, kind: 'temp' | 'repo-fresh' } — a repo-fresh caller MUST
237
- // 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.
238
171
  export const assertScratchDestination = (outPath, cwd) => {
239
172
  const lexical = isAbsolute(outPath) ? outPath : resolve(cwd, outPath);
240
173
  let leaf = null;
241
174
  try {
242
175
  leaf = lstatSync(lexical);
243
176
  } catch (err) {
244
- // 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
245
178
  // writer is bridge-tier auto-allowable, so an unverifiable leaf must never be written).
246
179
  if (err?.code !== 'ENOENT') {
247
180
  throw fail(1, `--out cannot inspect the destination (${outPath}: ${err?.code ?? err?.message ?? err}) — refusing to write an unverifiable leaf`);
@@ -252,7 +185,7 @@ export const assertScratchDestination = (outPath, cwd) => {
252
185
  throw fail(1, `--out refuses a symlink destination (${outPath}) — the write would follow it onto another file; name the real scratch path`);
253
186
  }
254
187
  if (leaf != null && !leaf.isFile()) {
255
- 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`);
256
189
  }
257
190
  let realParent;
258
191
  try {
@@ -261,8 +194,8 @@ export const assertScratchDestination = (outPath, cwd) => {
261
194
  throw fail(1, `--out parent directory does not exist (${dirname(lexical)}) — create the scratch dir first`);
262
195
  }
263
196
  const full = join(realParent, basename(lexical));
264
- // An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root (codex R8
265
- // 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
266
199
  // the repo is scratch" would let an unattended run overwrite e.g. ~/.bashrc promptless.
267
200
  // $TMPDIR / os.tmpdir() / /tmp are the scratch surface; everything else refuses loudly.
268
201
  const assertTempScratch = () => {
@@ -282,7 +215,7 @@ export const assertScratchDestination = (outPath, cwd) => {
282
215
  if (top == null || top.status !== 0) return { path: assertTempScratch().path, kind: 'temp' }; // no git tree → temp-only scratch
283
216
  const root = top.stdout.replace(/\r?\n$/, '');
284
217
  const rel = relative(root, full);
285
- // 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
286
219
  // `..facts` has rel `..facts` — `startsWith('..')` would misread it as outside and BYPASS the
287
220
  // tracked/ignored refusals below. Only `..` exactly or a `..${sep}`-prefixed rel is outside.
288
221
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { path: assertTempScratch().path, kind: 'temp' };
@@ -294,11 +227,11 @@ export const assertScratchDestination = (outPath, cwd) => {
294
227
  if (ignored == null || ignored.status !== 0) {
295
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`);
296
229
  }
297
- // 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
298
231
  // project file (.env is the canonical victim) — an auto-allowable writer must never clobber one.
299
232
  // The rewritable scratch surface is the system temp; in-repo gitignored writes are create-only —
300
233
  // the CALLER must open with the exclusive flag ('wx'): the kind seals the pre-check against the
301
- // guard→write race (codex R11).
234
+ // guard→write race.
302
235
  if (leaf != null) {
303
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`);
304
237
  }
@@ -310,7 +243,7 @@ export const assertScratchDestination = (outPath, cwd) => {
310
243
  const HELP = `grounding — grounded-review facts assembler for the agent-workflow family (AD-038).
311
244
 
312
245
  Usage:
313
- node grounding.mjs [--constraints] [--autonomy] [--plan <path>] [--ledger-summary] [--reserve-bytes <n>] [--out <path>]
246
+ node grounding.mjs [--constraints] [--autonomy] [--plan <path>] [--reserve-bytes <n>] [--out <path>]
314
247
 
315
248
  --constraints slice the root AGENTS.md "Hard Constraints" section verbatim
316
249
  (exactly one matching heading, else a loud STOP)
@@ -322,10 +255,6 @@ Usage:
322
255
  --plan <path> extract the plan's decision-bearing sections verbatim + whole:
323
256
  "## Approach" + "## Verification" (REQUIRED — STOP if missing),
324
257
  "## Decisions (locked)" when present; a duplicate heading is a STOP
325
- --ledger-summary append a COMPUTED review-ledger digest for the SINGLE in-flight
326
- plan-execution segment (rounds · origins · classifications · verdicts ·
327
- overrides + a per-round render) — "computed, not remembered" facts; a
328
- loud STOP unless exactly one plan is in flight
329
258
  --reserve-bytes <n> the artifact share agy-review will add around these facts — the output
330
259
  budget becomes AGY_MAX_PROMPT_BYTES − n (loud tail-trim on overflow)
331
260
  --out <path> write instead of stdout — system-temp scratch (rewritable), or a FRESH
@@ -345,12 +274,10 @@ const parseArgs = (argv) => {
345
274
  let plan = null;
346
275
  let out = null;
347
276
  let reserve = 0;
348
- let ledgerSummary = false;
349
277
  for (let i = 0; i < argv.length; i += 1) {
350
278
  const a = argv[i];
351
279
  if (a === '--constraints') constraints = true;
352
280
  else if (a === '--autonomy') autonomy = true;
353
- else if (a === '--ledger-summary') ledgerSummary = true;
354
281
  else if (a === '--plan') {
355
282
  plan = argv[i + 1];
356
283
  if (!plan || plan.startsWith('--')) throw fail(2, '--plan requires a <path>');
@@ -366,10 +293,10 @@ const parseArgs = (argv) => {
366
293
  i += 1;
367
294
  } else throw fail(2, `unknown argument: ${a}`);
368
295
  }
369
- if (!constraints && !autonomy && plan == null && !ledgerSummary) {
370
- throw fail(2, 'nothing to assemble — pass --constraints, --autonomy, --plan <path>, and/or --ledger-summary');
296
+ if (!constraints && !autonomy && plan == null) {
297
+ throw fail(2, 'nothing to assemble — pass --constraints, --autonomy, and/or --plan <path>');
371
298
  }
372
- return { constraints, autonomy, plan, out, reserve, ledgerSummary };
299
+ return { constraints, autonomy, plan, out, reserve };
373
300
  };
374
301
 
375
302
  const resolveBudget = (env, reserve) => {
@@ -389,7 +316,7 @@ export const main = (argv, ctx = {}) => {
389
316
  const env = ctx.env ?? process.env;
390
317
  try {
391
318
  if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
392
- const { constraints, autonomy, plan, out, reserve, ledgerSummary } = parseArgs(argv);
319
+ const { constraints, autonomy, plan, out, reserve } = parseArgs(argv);
393
320
  const budget = resolveBudget(env, reserve);
394
321
 
395
322
  const readOrStop = (path, label) => {
@@ -401,7 +328,7 @@ export const main = (argv, ctx = {}) => {
401
328
  };
402
329
  const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
403
330
  const autonomyText = autonomy ? resolveAutonomyFacts({ cwd }) : null;
404
- // --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
405
332
  // with an args wildcard, so an unattended invocation could otherwise point --plan at ANY
406
333
  // readable file and ship it into the facts payload. Plans are in-repo by contract
407
334
  // (docs/plans); an outside-tree path is a loud refusal, never a silent read.
@@ -424,10 +351,6 @@ export const main = (argv, ctx = {}) => {
424
351
  const parts = [];
425
352
  const assembled = assembleGrounding({ constraintsText, autonomyText, planText, planLabel: plan ?? 'plan' });
426
353
  if (assembled) parts.push(assembled);
427
- if (ledgerSummary) {
428
- const summary = resolveLedgerSummary({ cwd, env });
429
- if (summary) parts.push(summary);
430
- }
431
354
  const payload = parts.join('\n');
432
355
  const { text, trimmedBytes } = trimToBudget(payload, budget);
433
356
  const stderr = trimmedBytes > 0
@@ -437,7 +360,7 @@ export const main = (argv, ctx = {}) => {
437
360
  if (out != null) {
438
361
  const dest = assertScratchDestination(out, cwd);
439
362
  // repo-fresh writes are EXCLUSIVE ('wx'): the create-only pre-check would otherwise race a
440
- // 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.
441
364
  // ctx.writeFile is a TEST seam (the EEXIST race arm is not constructible without it).
442
365
  const writeFile = ctx.writeFile ?? writeFileSync;
443
366
  try {
@@ -457,7 +380,7 @@ export const main = (argv, ctx = {}) => {
457
380
  const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
458
381
  if (isDirectRun) {
459
382
  const r = main(process.argv.slice(2));
460
- // 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
461
384
  // byte-exact facts payload, and process.exit() can truncate large piped stdout before Node
462
385
  // flushes. The payload already ends with '\n' (sliceSection normalizes); only a non-payload
463
386
  // message (help / the --out report) gains the terminating newline it lacks.
@@ -17,7 +17,7 @@
17
17
  // path). Add-local-first, then DETECT + REPORT the residual legacy global block — removal is the
18
18
  // explicit, ASK-gated `--remove-global` (prints a restorable backup), never a default (an arbitrary
19
19
  // host's OTHER hidden repo may rely on the same root-anchored global lines). Windows is supported (text
20
- // edit; forward-slash patterns; CRLF preserved via EOL detection). Dependency-free, Node >= 18.
20
+ // edit; forward-slash patterns; CRLF preserved via EOL detection). Dependency-free, Node >= 22.
21
21
 
22
22
  import {
23
23
  readFileSync, writeFileSync, statSync, readdirSync,