@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
@@ -28,9 +28,9 @@
28
28
  // Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
29
29
  // exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
30
30
  // registry/sandbox-masks); the sandbox-masks and settings probes may run read-only git queries.
31
- // Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
31
+ // Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
32
32
 
33
- import { readFileSync, readdirSync, lstatSync } from 'node:fs';
33
+ import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
34
34
  import { createHash } from 'node:crypto';
35
35
  import { homedir } from 'node:os';
36
36
  import { dirname, join, resolve } from 'node:path';
@@ -54,9 +54,12 @@ import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
54
54
  import { surveyFamily, surveyGateHook } from './family-registry.mjs';
55
55
  import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
56
56
  import { shellQuoteArg } from './review-state.mjs';
57
+ import { isFinalCapableDeclaration } from './run-gates.mjs';
58
+ import { resolveGitHooksPath } from './commit-guard.mjs';
57
59
  import { loadConfig } from './orchestration-config.mjs';
58
60
  import { DEFAULT_BUNDLE_ROOT, SETTINGS_FILENAME, settingsPath, parseSettings, duplicateKeys } from './bridge-settings-read.mjs';
59
61
  import { assertContainedRealPath } from './fs-safe.mjs';
62
+ import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
60
63
 
61
64
  const HERE = dirname(fileURLToPath(import.meta.url));
62
65
  const toolPath = (rel) => join(HERE, rel);
@@ -97,6 +100,7 @@ export const SEVERITIES = Object.freeze({
97
100
  'review-recipe': SEVERITY_ATTENTION,
98
101
  'gates-declaration': SEVERITY_OPTIONAL,
99
102
  'gate-hook': SEVERITY_OPTIONAL,
103
+ 'commit-guard': SEVERITY_OPTIONAL,
100
104
  'read-lane': SEVERITY_OPTIONAL,
101
105
  'read-lane.stale': SEVERITY_ATTENTION,
102
106
  'read-lane.missing': SEVERITY_ATTENTION,
@@ -105,6 +109,7 @@ export const SEVERITIES = Object.freeze({
105
109
  'agy-adddir': SEVERITY_OPTIONAL,
106
110
  'agy-adddir.invalid-env': SEVERITY_ATTENTION,
107
111
  'sandbox-lane': SEVERITY_OPTIONAL,
112
+ 'worktrees-dir': SEVERITY_OPTIONAL,
108
113
  });
109
114
  // The per-item render tags (frozen presentation data, same language contract as the templates).
110
115
  export const SEVERITY_LABELS = Object.freeze({
@@ -152,6 +157,7 @@ export const WHATS = Object.freeze({
152
157
  'review-recipe': '{degraded}',
153
158
  'gates-declaration': 'no declared gate matrix (docs/ai/gates.json absent or empty) — gates prompt one by one; the apply PREVIEWS its --apply line, writes nothing',
154
159
  'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
160
+ 'commit-guard': 'the gate matrix is final-run-capable but no commit-guard arms the pre-commit hook — a commit needs no green receipt yet',
155
161
  'read-lane': 'the gate hook is wired but the read-only compound lane is off — pipes/chains of seeded reads still prompt one by one',
156
162
  'read-lane.stale': 'the read-lane is ON but the placed gate hook is stale — an old hook never reads lanes.json, so the lane is silently dark; reseed it',
157
163
  'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
@@ -161,6 +167,7 @@ export const WHATS = Object.freeze({
161
167
  'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
162
168
  'agy-adddir.invalid-env': 'AGY_REVIEW_ALLOW_ADDDIR is set to an INVALID value ({value}) — refuse-mode applies and the settings file is shadowed while it is set',
163
169
  'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
170
+ 'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
164
171
  });
165
172
 
166
173
  // ── the shape contract (D2): registry strings AND composed items stay one line under the cap ────
@@ -205,11 +212,13 @@ export const BENEFITS = Object.freeze({
205
212
  'review-recipe': 'review coverage — the review recipe you configured actually runs instead of silently degrading',
206
213
  'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
207
214
  'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
215
+ 'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
208
216
  'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
209
217
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
210
218
  'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
211
219
  'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
212
220
  'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
221
+ 'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
213
222
  });
214
223
 
215
224
  // A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
@@ -292,7 +301,7 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
292
301
  const variant = plan.tuple ? 'sandbox-provision.installable' : 'sandbox-provision';
293
302
  const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (plan.tuple ? String(plan.tuple).length : 0));
294
303
  // The doctor reads process.cwd() (deployment-gated) and takes no --cwd flag — the one-liner
295
- // pins the target project via a cd prefix (codex R2, Segment B).
304
+ // pins the target project via a cd prefix (Segment B).
296
305
  add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }), `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`);
297
306
  } catch (err) {
298
307
  skip('sandbox-provision', err);
@@ -301,7 +310,7 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
301
310
 
302
311
  const probeReviewRecipe = ({ root, deps, add, skip }) => {
303
312
  try {
304
- // The VALIDATED reader (codex R2, Segment B): a schema-invalid config (unknown activity/slot,
313
+ // The VALIDATED reader (Segment B): a schema-invalid config (unknown activity/slot,
305
314
  // bad recipe) throws here and becomes a stated skip — raw JSON.parse would silently ignore it.
306
315
  const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
307
316
  const detection = detectBackends(deps);
@@ -325,14 +334,14 @@ const probeGates = ({ root, deps, add, skip }) => {
325
334
  const sg = surveyGateHook(root, deps);
326
335
  if (sg.error) throw new Error(sg.error);
327
336
  if (sg.declarationPresent && sg.declaredGates === null) throw new Error(sg.declarationError ?? 'gates.json present but unreadable');
328
- // An ABSENT file and the seeded-EMPTY list are equally undeclared (codex R2, Segment B); the
329
- // apply is the consent-gated seeder PREVIEW (it proposes entries from the project's own
337
+ // An ABSENT file and the seeded-EMPTY list are equally undeclared (Segment B); the
338
+ // apply is the consent-gated gates-init PREVIEW (it proposes entries from the project's own
330
339
  // scripts and writes only on an explicit yes) — never the runner.
331
340
  if (!sg.declarationPresent || sg.declaredGates === 0) {
332
- // The seeder writes ONLY with --apply and consent is per-entry (--only) the apply field
333
- // stays a PURE executable command (run-exactly-as-rendered feeds it to the shell); the
334
- // two-step preview semantics live in WHAT, never as prose appended to the command.
335
- add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('seed-gates.mjs'))} --cwd ${q(root)}`);
341
+ // gates-init writes ONLY with --apply the apply field stays a PURE executable command
342
+ // (run-exactly-as-rendered feeds it to the shell); the two-step preview semantics live in
343
+ // WHAT, never as prose appended to the command.
344
+ add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}`);
336
345
  return;
337
346
  }
338
347
  if (sg.declaredGates > 0 && !sg.wired) {
@@ -343,6 +352,56 @@ const probeGates = ({ root, deps, add, skip }) => {
343
352
  }
344
353
  };
345
354
 
355
+ // The D10 consumer surface: once the declaration is FINAL-capable (the canonical core checks
356
+ // present, the checker LAST — the run-gates helper is the one home of that rule), offer the
357
+ // consented commit-guard install — for a MANAGED guardless pre-commit hook AND for an absent one
358
+ // alike; an UNMANAGED hook is a loud skip (manual merge, never an overwrite offer); a
359
+ // non-final-capable declaration gets NO offer (an armed guard over a matrix that cannot mint a
360
+ // receipt would block every commit).
361
+ const INSTALLER_MARKER_RE = /:install-git-hooks\.mjs$/m;
362
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
363
+ // Convergence needs the EXACT canonical armed line the installer writes — full-line anchored on
364
+ // the kit's OWN resolved guard path. A commented-out line or a lookalike path (fake-commit-guard)
365
+ // must never read as armed (that would suppress the offer and leave commits unguarded).
366
+ const canonicalGuardLineRe = () => new RegExp(`^node "${escapeRe(toolPath('commit-guard.mjs'))}" --check$`, 'm');
367
+ const probeCommitGuard = ({ root, deps, add, skip }) => {
368
+ try {
369
+ const read = deps.readFile ?? readFileSync;
370
+ const gatesRaw = (() => {
371
+ try {
372
+ return String(read(join(root, 'docs', 'ai', 'gates.json'), 'utf8'));
373
+ } catch {
374
+ return null;
375
+ }
376
+ })();
377
+ if (gatesRaw === null) return; // undeclared — the gates-declaration item owns that story
378
+ const gates = JSON.parse(gatesRaw).gates;
379
+ if (!isFinalCapableDeclaration(gates, root)) return; // no receipt to guard yet — no offer
380
+ const installer = join(root, 'scripts', 'install-git-hooks.mjs');
381
+ if (!(deps.exists ?? existsSync)(installer)) {
382
+ skip('commit-guard', new Error('the hook installer (scripts/install-git-hooks.mjs) is not deployed — run upgrade first'));
383
+ return;
384
+ }
385
+ const hooksPath = (deps.gitHooksPath ?? resolveGitHooksPath)(root);
386
+ if (hooksPath === null) return; // not a git tree — nothing to hook
387
+ const hook = (() => {
388
+ try {
389
+ return String(read(join(hooksPath, 'pre-commit'), 'utf8'));
390
+ } catch {
391
+ return null;
392
+ }
393
+ })();
394
+ if (hook !== null && canonicalGuardLineRe().test(hook)) return; // armed — converged (managed or not)
395
+ if (hook !== null && !INSTALLER_MARKER_RE.test(hook)) {
396
+ skip('commit-guard', new Error('an UNMANAGED pre-commit hook exists — merge the guard line by hand (the installer refuses to overwrite it)'));
397
+ return;
398
+ }
399
+ add('commit-guard', fillTemplate(WHATS['commit-guard'], {}), `node ${q(installer)} --commit-guard ${q(toolPath('commit-guard.mjs'))}`);
400
+ } catch (err) {
401
+ skip('commit-guard', err);
402
+ }
403
+ };
404
+
346
405
  // The read-lane offer (AD-055 Part II, Help-through-Recommendations): once the gate hook is PLACED
347
406
  // and WIRED, offer to enable the opt-in read-only compound lane. Mutually exclusive with the
348
407
  // gate-hook item BY CONSTRUCTION — that item keys on `!wired`, this one on `wired` — so an unplaced
@@ -412,7 +471,7 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
412
471
  if (!needsMasksApply(p)) return;
413
472
  const variant = p.staleReal.length > 0 ? 'sandbox-masks.stale-real' : 'sandbox-masks';
414
473
  // A stale-real-only fence (EMPTY derivation over a non-empty block) makes the plain --apply
415
- // REFUSE — the exact one-liner must carry --clear there (codex R1, Segment B).
474
+ // REFUSE — the exact one-liner must carry --clear there (Segment B).
416
475
  const apply = p.masks.length === 0 && p.staleReal.length > 0 ? `${p.applyCmd} --clear` : p.applyCmd;
417
476
  add('sandbox-masks', fillTemplate(WHATS[variant], { n: p.masks.length, m: p.staleReal.length }), apply);
418
477
  } catch (err) {
@@ -425,7 +484,7 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
425
484
  const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
426
485
  if (!probePlaced('agy-review')) return;
427
486
  // Configured means a VALID boolean value (the wrapper validates and falls back to the default
428
- // on garbage — presence alone proves nothing; codex R3). An explicit valid 0 is a user CHOICE
487
+ // on garbage — presence alone proves nothing). An explicit valid 0 is a user CHOICE
429
488
  // (refuse mode) — respected, never nagged. env > file, the wrappers' own precedence.
430
489
  const isValidBool = (v) => v === '0' || v === '1';
431
490
  const env = deps.getenv ?? process.env;
@@ -626,7 +685,7 @@ const readReadLaneToggle = (root, deps) => {
626
685
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
627
686
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
628
687
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
629
- export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane', 'read-lane']);
688
+ export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane', 'read-lane', 'worktrees-dir']);
630
689
 
631
690
  const probeSandboxLane = ({ root, deps, add, skip }) => {
632
691
  try {
@@ -683,6 +742,28 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
683
742
  }
684
743
  };
685
744
 
745
+ // The worktrees-dir arming item (parallel feature worktrees): fires when the resolved worktrees
746
+ // parent dir (docs/ai/worktrees.json parentDir, else the repo's own parent) is not confirmed
747
+ // writable by a trusted host signal. The provision preflight remains the real create+delete test.
748
+ const probeWorktreesDir = ({ root, deps, add, skip }) => {
749
+ try {
750
+ const config = loadWorktreesConfig(root, deps);
751
+ const parent = config.parentDir == null ? dirname(root) : resolve(root, config.parentDir);
752
+ // the SAME canonical derivation the provision preflight probes (worktrees.mjs) — an absent
753
+ // configured dir resolves to its nearest existing ancestor, never a false denial
754
+ const probeDir = resolveProbeDir(parent, deps);
755
+ if (deps.canWriteDir?.(probeDir) === true) return;
756
+ const dir = truncatedTo(oneLineOf(probeDir), templateBudget(WHATS['worktrees-dir']));
757
+ add(
758
+ 'worktrees-dir',
759
+ fillTemplate(WHATS['worktrees-dir'], { dir }),
760
+ `HAND-APPLY: add ${JSON.stringify(probeDir)} to sandbox.filesystem.allowWrite in .claude/settings.json on settings-native hosts; on harness-managed hosts grant this dir for the session or use the provision terminal fallback`,
761
+ );
762
+ } catch (err) {
763
+ skip('worktrees-dir', err);
764
+ }
765
+ };
766
+
686
767
  // ── assembly (frozen presentation order) ─────────────────────────────────────────────────────────
687
768
  const PROBES = Object.freeze([
688
769
  probeVelocityItems,
@@ -690,11 +771,13 @@ const PROBES = Object.freeze([
690
771
  probeSandboxProvision,
691
772
  probeReviewRecipe,
692
773
  probeGates,
774
+ probeCommitGuard,
693
775
  probeReadLane,
694
776
  probeFamilyFreshness,
695
777
  probeMasksItem,
696
778
  probeAgyAdddir,
697
779
  probeSandboxLane,
780
+ probeWorktreesDir,
698
781
  ]);
699
782
 
700
783
  export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
@@ -737,7 +820,7 @@ export const formatRecommendations = ({ items, skips }) => {
737
820
  const verdict = composeVerdict({ attention, optional: items.length - attention, skipped: skips.length });
738
821
  if (verdict == null) {
739
822
  // The flow-optimal claim renders ONLY when every probe ran and none fired — an empty item
740
- // list beside skipped checks would falsely attest optimality (codex R1, Segment B).
823
+ // list beside skipped checks would falsely attest optimality (Segment B).
741
824
  lines.push(RECOMMENDATIONS_EMPTY_LINE);
742
825
  return lines.join('\n');
743
826
  }
@@ -8,7 +8,7 @@
8
8
  // tripping the (narrow) attribution rules. Only the TEST is self-excluded (it holds attribution
9
9
  // fixtures); the scanner source contains no attribution construction, so it is scanned normally.
10
10
  //
11
- // Pure `scanText` for unit fixtures; `scanPaths`/CLI for the release tree. Dependency-free, Node >= 18.
11
+ // Pure `scanText` for unit fixtures; `scanPaths`/CLI for the release tree. Dependency-free, Node >= 22.
12
12
 
13
13
  import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
14
14
  import { join, extname, basename, resolve } from 'node:path';
@@ -35,6 +35,9 @@ const ATTRIBUTION = [
35
35
  { re: /reviewed by (claude|codex|chatgpt|gpt|gemini|copilot|cursor|the (ai|model|agent))/i, label: 'AI review attribution' },
36
36
  { re: /authored[- ]by[: ][^\n]{0,40}\b(claude|chatgpt|gemini|copilot)\b/i, label: 'AI authorship attribution' },
37
37
  ];
38
+ const REVIEWER_IDENTITY = [
39
+ { re: /\b(?:agy|codex)(?:\s+|-)r\d+(?:(?:\+|\/)r?\d+)*(?:-[a-z0-9]+)*\b/i, label: 'reviewer-round identity' },
40
+ ];
38
41
 
39
42
  const allowlistCovers = (matched, allowlist) =>
40
43
  allowlist.some((term) => matched.toLowerCase().includes(term.toLowerCase()));
@@ -49,6 +52,10 @@ export const scanText = (text, { allowlist = ALLOWLIST } = {}) => {
49
52
  findings.push({ line: idx + 1, kind: 'attribution', detail: `${label}: "${m[0]}"` });
50
53
  }
51
54
  }
55
+ for (const { re, label } of REVIEWER_IDENTITY) {
56
+ const m = line.match(re);
57
+ if (m) findings.push({ line: idx + 1, kind: 'reviewer-identity', detail: `${label}: "${m[0]}"` });
58
+ }
52
59
  });
53
60
  return findings;
54
61
  };
@@ -83,7 +90,7 @@ export const scanPaths = (paths, opts = {}) => {
83
90
  return report;
84
91
  };
85
92
 
86
- const main = (argv) => {
93
+ export const main = (argv) => {
87
94
  const reportOnly = argv.includes('--report');
88
95
  const paths = argv.filter((a) => a !== '--report');
89
96
  if (paths.length === 0) {
@@ -93,7 +100,7 @@ const main = (argv) => {
93
100
  const report = scanPaths(paths);
94
101
  for (const r of report) console.log(`[${r.kind}] ${r.file}:${r.line} — ${r.detail}`);
95
102
  if (report.length === 0) {
96
- console.log('[release-scan] clean — no AI attribution found.');
103
+ console.log('[release-scan] clean — no AI attribution or reviewer-round identity found.');
97
104
  return;
98
105
  }
99
106
  console.log(`\n[release-scan] ${report.length} finding(s).`);
@@ -5,7 +5,7 @@
5
5
  // the JSON mode is handled by the caller (it prints the envelope, not a render). The renderers do
6
6
  // LAYOUT + GLYPHS only — phrasing + headline counts already live in the ViewModel.
7
7
  //
8
- // Pure, no side effects, Node >= 18.
8
+ // Pure, no side effects, Node >= 22.
9
9
 
10
10
  import { BLOCK_TITLES, SETTINGS_LABELS, glyphsFor, NO_DEPLOYMENT } from './presentation.mjs';
11
11