@sabaiway/agent-workflow-kit 3.15.0 → 4.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 (65) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +4 -4
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +12 -8
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +735 -55
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +899 -51
  7. package/bridges/antigravity-cli-bridge/bin/agy.sh +4 -3
  8. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +23 -0
  9. package/bridges/antigravity-cli-bridge/capability.json +14 -4
  10. package/bridges/antigravity-cli-bridge/references/driving-agy.md +12 -4
  11. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
  12. package/bridges/antigravity-cli-bridge/references/review-prompt.md +65 -2
  13. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  14. package/bridges/codex-cli-bridge/bin/codex-exec.sh +2 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +63 -13
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +38 -0
  17. package/bridges/codex-cli-bridge/capability.json +1 -1
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/agents/review-lens.md +39 -0
  21. package/references/hooks/gate-approve.mjs +54 -7
  22. package/references/modes/agents.md +11 -2
  23. package/references/modes/autonomy-doctor.md +2 -0
  24. package/references/modes/backends.md +2 -0
  25. package/references/modes/bootstrap.md +2 -0
  26. package/references/modes/bridge-settings.md +4 -1
  27. package/references/modes/commit-guard.md +2 -0
  28. package/references/modes/core-evidence.md +2 -0
  29. package/references/modes/coverage-check.md +2 -0
  30. package/references/modes/doc-parity.md +2 -0
  31. package/references/modes/gates.md +2 -0
  32. package/references/modes/grounding.md +2 -0
  33. package/references/modes/help.md +2 -0
  34. package/references/modes/hook.md +13 -2
  35. package/references/modes/migrate-adr-store.md +2 -0
  36. package/references/modes/procedures.md +2 -0
  37. package/references/modes/recipes.md +2 -0
  38. package/references/modes/recommendations.md +4 -3
  39. package/references/modes/review-state.md +2 -0
  40. package/references/modes/sandbox-masks.md +2 -0
  41. package/references/modes/set-autonomy.md +2 -0
  42. package/references/modes/set-recipe.md +3 -0
  43. package/references/modes/setup.md +2 -0
  44. package/references/modes/state-block-guard.md +2 -0
  45. package/references/modes/status.md +4 -1
  46. package/references/modes/uninstall.md +2 -0
  47. package/references/modes/upgrade.md +2 -0
  48. package/references/modes/velocity.md +7 -0
  49. package/references/modes/worktrees.md +2 -0
  50. package/tools/bridge-settings-read.mjs +40 -10
  51. package/tools/bridge-settings.mjs +22 -7
  52. package/tools/cheap-agents.mjs +15 -5
  53. package/tools/commands.mjs +2 -2
  54. package/tools/core-evidence.mjs +29 -2
  55. package/tools/detect-backends.mjs +1 -1
  56. package/tools/manifest/schema.md +7 -0
  57. package/tools/manifest/validate.mjs +8 -0
  58. package/tools/presentation.mjs +1 -1
  59. package/tools/procedures.mjs +9 -2
  60. package/tools/recipes.mjs +4 -1
  61. package/tools/recommendations.mjs +110 -59
  62. package/tools/renderers.mjs +10 -1
  63. package/tools/review-state.mjs +4 -0
  64. package/tools/velocity-profile.mjs +6 -3
  65. package/tools/view-model.mjs +3 -1
@@ -42,8 +42,10 @@ const renderReader = (registry, parsed, fileState, ctx, path) => {
42
42
  for (const entry of registry.values()) {
43
43
  const eff = effectiveOf(entry, parsed, getenv);
44
44
  const note = eff.note ? ` — ${eff.note}` : '';
45
- lines.push(` ${entry.key} = ${displayValue(eff.value)} [${eff.source}]${note}`);
45
+ const retired = entry.retired ? ' ⚠ RETIRED' : '';
46
+ lines.push(` ${entry.key} = ${displayValue(eff.value)} [${eff.source}]${retired}${note}`);
46
47
  lines.push(` ${entry.bridge} · ${allowedLabel(entry)} · ${entry.effect}`);
48
+ if (entry.retired) lines.push(` RETIRED — ${entry.retired}`);
47
49
  }
48
50
  const dups = duplicateKeys(parsed);
49
51
  const unknown = [...parsed.byKey.keys()].filter((k) => !registry.has(k));
@@ -61,7 +63,7 @@ const buildReaderJson = (registry, parsed, fileState, ctx, path) => {
61
63
  fileState: fileState.state,
62
64
  knobs: [...registry.values()].map((entry) => {
63
65
  const eff = effectiveOf(entry, parsed, getenv);
64
- return { key: entry.key, bridge: entry.bridge, kind: entry.kind, effective: eff.value, source: eff.source, note: eff.note ?? null, default: entry.default };
66
+ return { key: entry.key, bridge: entry.bridge, kind: entry.kind, effective: eff.value, source: eff.source, note: eff.note ?? null, default: entry.default, retired: entry.retired ?? null };
65
67
  }),
66
68
  duplicateKeys: duplicateKeys(parsed),
67
69
  unknownKeys: [...parsed.byKey.keys()].filter((k) => !registry.has(k)),
@@ -85,14 +87,21 @@ export const reconcileSettings = (ctx = {}) => {
85
87
  if (fileState.state === 'absent') return { outcome: 'absent', lines: [` bridge-settings: no settings file — skipped (${path})`] };
86
88
  if (fileState.state === 'unusable') return { outcome: 'unusable', lines: [` bridge-settings: settings file is a symlink / not a regular file — skipped, never edited (${path})`] };
87
89
  const parsed = parseSettings(fileState.text);
88
- const known = [...parsed.byKey.keys()].filter((k) => registry.has(k));
89
- const unknown = [...parsed.byKey.keys()].filter((k) => !registry.has(k));
90
+ // RECOGNIZED and CURRENT are not the same thing: a manifest-RETIRED key is still in the registry
91
+ // (so an existing line never warns as unknown) but it arms nothing, and filing it under "all
92
+ // current" would be the survival check contradicting the retirement contract on the one surface
93
+ // init/upgrade actually shows.
94
+ const fileKeys = [...parsed.byKey.keys()];
95
+ const retired = fileKeys.filter((k) => registry.get(k)?.retired);
96
+ const known = fileKeys.filter((k) => registry.has(k) && !registry.get(k).retired);
97
+ const unknown = fileKeys.filter((k) => !registry.has(k));
90
98
  const dups = duplicateKeys(parsed);
91
99
  const lines = [];
92
- if (unknown.length) lines.push(` bridge-settings: ${unknown.length} unknown/retired key(s) preserved verbatim (never edited) — compare with the current knobs when convenient: ${unknown.join(', ')}`);
100
+ if (unknown.length) lines.push(` bridge-settings: ${unknown.length} unknown key(s) preserved verbatim (never edited) — compare with the current knobs when convenient: ${unknown.join(', ')}`);
101
+ for (const key of retired) lines.push(` bridge-settings: ${key} is RETIRED and arms nothing — preserved verbatim (never edited); clear it with --unset ${key} --apply. ${registry.get(key).retired}`);
93
102
  if (dups.length) lines.push(` bridge-settings: duplicate key(s) — the reader takes the last; fix by hand before the writer will edit: ${dups.join(', ')}`);
94
- if (!unknown.length && !dups.length) lines.push(` bridge-settings: ${known.length} key(s) recognized, all current`);
95
- return { outcome: unknown.length ? 'flagged' : dups.length ? 'duplicates' : 'ok', lines };
103
+ if (!unknown.length && !retired.length && !dups.length) lines.push(` bridge-settings: ${known.length} key(s) recognized, all current`);
104
+ return { outcome: unknown.length || retired.length ? 'flagged' : dups.length ? 'duplicates' : 'ok', lines };
96
105
  };
97
106
 
98
107
  // ── writer ─────────────────────────────────────────────────────────────────────────
@@ -143,6 +152,12 @@ const validateOps = (ops, registry) => {
143
152
  for (const op of ops) {
144
153
  const entry = registry.get(op.key);
145
154
  if (!entry) throw fail(2, `unknown key "${op.key}" — not a settings knob of any bundled bridge. Known keys: ${[...registry.keys()].join(', ')}`);
155
+ // A RETIRED key (D3) is recognized so an existing line never warns as unknown — but writing a NEW
156
+ // one would arm nothing while reading as configuration. `--unset` stays allowed on purpose: it is
157
+ // the recovery the retirement notice tells the user to run.
158
+ if (op.kind === 'set' && entry.retired) {
159
+ throw fail(2, `${op.key} is RETIRED — ${entry.retired} Setting it would arm nothing; use --unset ${op.key} to clear an existing line.`);
160
+ }
146
161
  if (op.kind === 'set' && !settingValueValid(entry, op.value)) {
147
162
  throw fail(2, `invalid value "${op.value}" for ${op.key} — allowed: ${allowedLabel(entry)}`);
148
163
  }
@@ -28,6 +28,7 @@
28
28
  import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
29
29
  import { join, resolve, dirname } from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
+ import { shellQuoteArg } from './review-state.mjs';
31
32
 
32
33
  const HERE = dirname(fileURLToPath(import.meta.url));
33
34
 
@@ -49,8 +50,10 @@ export const CHEAP_AGENTS_BUNDLE = 'CHEAP_AGENTS_BUNDLE';
49
50
 
50
51
  const USAGE = `usage: cheap-agents [--dry-run | --apply] [--cwd <dir>] [--help]
51
52
 
52
- Places the bundled cheap-lane subagent definitions (haiku/low, read-only tools) into the
53
- project's ${AGENTS_DIR}/. Default is --dry-run (a preview; writes nothing). --apply writes.
53
+ Places the bundled READ-ONLY subagent definitions into the project's ${AGENTS_DIR}/. No vehicle
54
+ grants a shell: three ride a cheap model (haiku/low) for mechanical work — extraction sweeps,
55
+ changelog fact-skeletons, gate triage — and review-lens is a read-only REVIEW vehicle on a
56
+ review-capable model. Default is --dry-run (a preview; writes nothing). --apply writes.
54
57
  An existing file with DIFFERENT content is preserved and reported, never overwritten.`;
55
58
 
56
59
  export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
@@ -176,8 +179,8 @@ const ACTION_LABEL = {
176
179
  export const formatResult = (result) => {
177
180
  const lines = [
178
181
  result.dryRun
179
- ? 'agent-workflow cheap-lane agents — DRY RUN (no changes; re-run with --apply)'
180
- : 'agent-workflow cheap-lane agents — APPLY',
182
+ ? 'agent-workflow read-only subagents — DRY RUN (no changes)'
183
+ : 'agent-workflow read-only subagents — APPLY',
181
184
  ];
182
185
  for (const item of result.plan) {
183
186
  const verb = result.dryRun && item.action === 'place' ? 'would place' : ACTION_LABEL[item.action];
@@ -187,8 +190,15 @@ export const formatResult = (result) => {
187
190
  lines.push(`note: no current deployment stamp found (${result.stamp ?? 'none'}) — --apply will refuse until init/upgrade runs.`);
188
191
  }
189
192
  lines.push(
190
- 'the vehicles are Claude Code subagents (model: haiku, effort: low, read-only tools) for mechanical work only judgment, review, and real code stay on your main lane.',
193
+ 'the vehicles are Claude Code subagents with READ-ONLY tools and NO shellso a fan-out can never turn into a wave of approval prompts.',
194
+ 'three ride the cheap lane (model: haiku, effort: low) for mechanical work; review-lens is an ADDITIONAL read-only review opinion, not a replacement for your configured review recipe. Writing code and running gates stay on your main lane.',
191
195
  );
196
+ // A preview must print the EXACT command that applies it. The advisor renders this dry-run as an
197
+ // item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
198
+ // bare "re-run with --apply" would leave the caller to reconstruct --cwd and its quoting.
199
+ if (result.dryRun && result.plan.some((item) => item.action === 'place')) {
200
+ lines.push(`to apply, run exactly: node ${shellQuoteArg(fileURLToPath(import.meta.url))} --apply --cwd ${shellQuoteArg(result.projectDir)}`);
201
+ }
192
202
  if (!result.dryRun && result.wrote) {
193
203
  lines.push('hidden-mode note: if this deployment is hidden, run the hide-footprint reconcile so the placed files stay out of `git status`.');
194
204
  }
@@ -131,7 +131,7 @@ const CATALOG = [
131
131
  invocation: invocationOf('agents'),
132
132
  group: 'Configure',
133
133
  kind: WRITER,
134
- oneLine: 'Place bundled cheap-model subagent definitions for mechanical work sweeps, changelog skeletons, gate triage (Claude Code; opt-in; preview first).',
134
+ oneLine: 'Place bundled READ-ONLY subagent definitions, none of them granted a shell — three cheap-model ones for mechanical work (sweeps, changelog skeletons, gate triage) and a review lens (Claude Code; opt-in; preview first).',
135
135
  },
136
136
  {
137
137
  key: 'hook',
@@ -305,7 +305,7 @@ const TUNE_TAIL = Object.freeze([
305
305
  '',
306
306
  'Tune — opt-in accelerators (consent-first: every writer previews before writing; nothing runs without your yes)',
307
307
  ` ${BARE_INVOCATION} velocity routine read-only commands stop prompting (incl. the --kit-tools tier for the kit's own read-only tools)`,
308
- ` ${BARE_INVOCATION} agents cheap-model subagents take the mechanical work (sweeps, changelog skeletons, gate triage)`,
308
+ ` ${BARE_INVOCATION} agents shell-free subagents: cheap ones take the mechanical work, a review lens gives an extra read-only opinion`,
309
309
  ` ${BARE_INVOCATION} gates run your declared gates (docs/ai/gates.json) as one batch; its guide also offers the consent-gated seeding preview — writes only on your yes`,
310
310
  ` ${BARE_INVOCATION} hook auto-approve exactly your declared gate commands (byte-exact matches only)`,
311
311
  ` ${BARE_INVOCATION} set-recipe put a ready review backend to work on plans and diffs`,
@@ -452,8 +452,22 @@ export const REVIEW_RECEIPT_CLASS = Object.freeze({
452
452
  MALFORMED_MARKER: 'malformed-marker',
453
453
  POSTURE_UNMARKED: 'posture-unmarked',
454
454
  MALFORMED_POSTURE: 'malformed-posture',
455
+ DELIVERY_UNMARKED: 'delivery-unmarked',
456
+ MALFORMED_DELIVERY: 'malformed-delivery',
455
457
  });
456
458
 
459
+ // Backends whose `code` receipts must SELF-DECLARE how the change set reached the model (D8b).
460
+ // Scoped, not universal: agy's oversized lane was the one observed returning a confident
461
+ // fabrication, and codex's receipt semantics are deliberately untouched.
462
+ const DELIVERY_DECLARING_BACKENDS = new Set(['agy']);
463
+
464
+ // A delivery declaration is a lowercase token naming HOW delivery was established (`inline` when
465
+ // the whole change set rode one prompt, `fed` when a chunked feed proved it by echo). The gate
466
+ // requires PRESENT and WELL-FORMED, never a PARTICULAR value — which lane was used is the
467
+ // wrapper's business; that the receipt declares one at all is the gate's.
468
+ const isValidReceiptDelivery = (delivery) =>
469
+ typeof delivery === 'string' && delivery.length > 0 && delivery.length <= 32 && /^[a-z][a-z0-9-]*$/.test(delivery);
470
+
457
471
  // The D5 posture declaration (strip Phase 4): an object whose `model` is a NON-EMPTY string;
458
472
  // `effort` (when present) a non-empty string; `tier` (when present) a non-empty string or null.
459
473
  // Backend-agnostic — the wrapper is the writer authority on WHICH keys it declares.
@@ -483,6 +497,15 @@ export const classifyReviewReceiptForTree = (receipt, fingerprint) => {
483
497
  if (!isValidReceiptPosture(receipt.posture)) return REVIEW_RECEIPT_CLASS.MALFORMED_POSTURE;
484
498
  if (!isRecognizedVerdict(receipt.verdict)) return REVIEW_RECEIPT_CLASS.UNRECOGNIZED_VERDICT;
485
499
  if (receipt.grounded !== true) return REVIEW_RECEIPT_CLASS.UNGROUNDED;
500
+ // LAST on purpose. The delivery arm may only intercept a receipt that would otherwise ATTEST:
501
+ // placing it earlier pulled delivery-less `unrecognized-verdict` / `ungrounded` agy receipts out
502
+ // of summarize's latest-NORMAL selection, so an EARLIER ship survived a LATER bad receipt — the
503
+ // selection-first doctrine, broken silently. Those two classes stay byte-identical to before;
504
+ // the ONE class this arm changes is the previously-attesting one, which is the point.
505
+ if (DELIVERY_DECLARING_BACKENDS.has(receipt.backend)) {
506
+ if (!Object.hasOwn(receipt, 'delivery')) return REVIEW_RECEIPT_CLASS.DELIVERY_UNMARKED;
507
+ if (!isValidReceiptDelivery(receipt.delivery)) return REVIEW_RECEIPT_CLASS.MALFORMED_DELIVERY;
508
+ }
486
509
  return REVIEW_RECEIPT_CLASS.ATTESTING;
487
510
  };
488
511
 
@@ -504,6 +527,8 @@ export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
504
527
  const malformedMarker = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_MARKER);
505
528
  const postureUnmarked = rowsFor(REVIEW_RECEIPT_CLASS.POSTURE_UNMARKED);
506
529
  const malformedPosture = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_POSTURE);
530
+ const deliveryUnmarked = rowsFor(REVIEW_RECEIPT_CLASS.DELIVERY_UNMARKED);
531
+ const malformedDelivery = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_DELIVERY);
507
532
  const counts = {
508
533
  currentCount: classified.length,
509
534
  ungroundedCount: rowsFor(REVIEW_RECEIPT_CLASS.UNGROUNDED).length,
@@ -512,6 +537,7 @@ export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
512
537
  markerRejected: malformedMarker.length,
513
538
  unmarkedRejected: unmarked.length,
514
539
  postureRejected: postureUnmarked.length + malformedPosture.length,
540
+ deliveryRejected: deliveryUnmarked.length + malformedDelivery.length,
515
541
  };
516
542
  if (normal.length > 0) {
517
543
  const latest = normal[normal.length - 1];
@@ -521,7 +547,7 @@ export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
521
547
  }
522
548
  if (classified.length > 0) {
523
549
  return {
524
- state: malformedMarker.length > 0 || unmarked.length > 0 || counts.postureRejected > 0 ? 'rejected' : 'probe',
550
+ state: malformedMarker.length > 0 || unmarked.length > 0 || counts.postureRejected > 0 || counts.deliveryRejected > 0 ? 'rejected' : 'probe',
525
551
  receipt: null,
526
552
  ...counts,
527
553
  };
@@ -537,12 +563,13 @@ export const describeMissingReviewAttestation = (summary) => {
537
563
  summary.markerRejected > 0 ? `${summary.markerRejected} receipt(s) with a malformed probe marker` : null,
538
564
  summary.unmarkedRejected > 0 ? `${summary.unmarkedRejected} receipt(s) with no probe marker` : null,
539
565
  (summary.postureRejected ?? 0) > 0 ? `${summary.postureRejected} receipt(s) with an absent/invalid run posture (a pre-D5 wrapper — re-run the review on the current bridge)` : null,
566
+ (summary.deliveryRejected ?? 0) > 0 ? `${summary.deliveryRejected} agy receipt(s) with an absent/invalid delivery declaration (minted before the change set was PROVEN delivered — re-run the review on the current bridge)` : null,
540
567
  ].filter(Boolean);
541
568
  const exclusionSuffix = exclusions.length > 0 ? `; excluded ${exclusions.join(', ')}` : '';
542
569
  if (summary.state === 'ungrounded') return `the latest normal receipt for the current tree is ungrounded${exclusionSuffix}`;
543
570
  if (summary.state === 'unrecognized-verdict') return `the latest normal receipt carries an unrecognized verdict (${JSON.stringify(summary.receipt?.verdict ?? null)}) — an unknown verdict never attests${exclusionSuffix}`;
544
571
  if (summary.state === 'probe') return 'only probe receipts exist for the current tree — a probe review never attests';
545
- if (summary.state === 'rejected') return `current-tree receipts have an untrustworthy probe marker or run posture${exclusionSuffix}`;
572
+ if (summary.state === 'rejected') return `current-tree receipts have an untrustworthy probe marker, run posture or delivery declaration${exclusionSuffix}`;
546
573
  return 'no fresh code receipt exists for the current tree';
547
574
  };
548
575
 
@@ -133,7 +133,7 @@ const RAW_BACKENDS = [
133
133
  'agy-review --continue [--decided @f] [--focus "…"]',
134
134
  'agy-review --conversation <id> [--decided @f] [--focus "…"]',
135
135
  ],
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",
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; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; 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",
137
137
  notes: [
138
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
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',
@@ -83,6 +83,13 @@ this block). Each entry:
83
83
  union of both bridges' `settings` keys) and skips other wrappers' keys silently.
84
84
  - `effect` (string, required) — what the knob does, incl. built-in defaults and any spend/risk
85
85
  caveat (the credit-rate caveat rides here for the tier knob).
86
+ - `retired` (string, optional; **>= 20 chars when present**) — the STATED reason a key is kept only
87
+ for RECOGNITION. Recognition alone is not enough: a retired key stays in the registry so an
88
+ existing settings line never starts warning as unknown, but it **arms nothing**. Runtime semantics
89
+ the field switches on: the writer REFUSES a new `--set` (and still permits `--unset`, which is the
90
+ stated recovery), the reader/`--json`/status/`procedures` render it as retired rather than active,
91
+ and the init/upgrade survival check reports it as flagged rather than "all current". A bare flag is
92
+ rejected by validation — a dead knob must explain itself wherever it surfaces.
86
93
 
87
94
  Wrappers never parse JSON at run time: each carries its own shell registry/validation constants,
88
95
  drift-guarded set-equal to this block by the bridge `bin/*.test.mjs` suites (help section keys,
@@ -596,6 +596,14 @@ export const validateManifest = (skillDir) => {
596
596
  seenKeys.add(entry.key);
597
597
  }
598
598
  if (typeof entry.effect !== 'string' || !entry.effect) errors.push(`${at}.effect must be a non-empty string`);
599
+ // RETIRED metadata (D3): recognition alone is not enough. A key kept only so an existing
600
+ // settings line never warns as unknown carries a STATED reason; the writer refuses a new
601
+ // `--set` of it and every reader surface renders it as retired. Optional, but never a bare
602
+ // flag — an unvalidated field must not be able to ship.
603
+ if (Object.hasOwn(entry, 'retired')
604
+ && (typeof entry.retired !== 'string' || entry.retired.trim().length < 20)) {
605
+ errors.push(`${at}.retired must be a string stating WHY the key is retired (>= 20 chars)`);
606
+ }
599
607
  if (!Array.isArray(entry.appliesTo) || entry.appliesTo.length === 0
600
608
  || !entry.appliesTo.every((c) => typeof c === 'string' && c)) {
601
609
  errors.push(`${at}.appliesTo must be a non-empty array of wrapper cmd names`);
@@ -41,7 +41,7 @@ export const SETTINGS_LABELS = Object.freeze({
41
41
  recipes: 'recipes',
42
42
  attribution: 'attribution',
43
43
  velocity: 'velocity',
44
- agents: 'cheap agents',
44
+ agents: 'subagents',
45
45
  hook: 'gate hook',
46
46
  });
47
47
 
@@ -160,7 +160,10 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
160
160
  const contracts = dispatch
161
161
  .map((d) => ({ backend: d.backend, role: d.role, cmd: wrapperCmdFor(d.backend, d.role), contract: wrapperContractFor(d.backend, d.role) }))
162
162
  .filter((c) => c.cmd && c.contract)
163
- .map((c) => ({ ...c, settings: knobsFor(c.cmd).map((k) => ({ key: k.key, allowed: allowedLabel(k) })) }));
163
+ // `retired` rides along: without it this surface advertised a RETIRED key as an ordinary
164
+ // settable knob, while the writer refuses to set it — a driving contract that contradicts the
165
+ // tool it points at.
166
+ .map((c) => ({ ...c, settings: knobsFor(c.cmd).map((k) => ({ key: k.key, allowed: allowedLabel(k), retired: k.retired ?? null })) }));
164
167
  return { slot, ...resolved, backends, contracts };
165
168
  });
166
169
  };
@@ -329,7 +332,11 @@ const contractLines = ({ cmd, contract, settings }) => {
329
332
  // branch — contractLines drops any contract key it does not name, so this must be enumerated here).
330
333
  if ((settings ?? []).length) {
331
334
  lines.push(' host settings (survive kit upgrades — set via /agent-workflow-kit bridge-settings):');
332
- for (const s of settings) lines.push(` ${s.key} — ${s.allowed}`);
335
+ for (const s of settings) {
336
+ lines.push(s.retired
337
+ ? ` ${s.key} — RETIRED: recognized but arms nothing; the writer refuses --set, --unset clears an existing line`
338
+ : ` ${s.key} — ${s.allowed}`);
339
+ }
333
340
  }
334
341
  return lines;
335
342
  };
package/tools/recipes.mjs CHANGED
@@ -352,7 +352,10 @@ export const composeStatusLine = (detection, recommendation, settings = null, au
352
352
  // chars — collapse them to a single space so the "exactly one line" backend-status contract holds.
353
353
  const oneLine = (s) => String(s).replace(/[\s]+/g, ' ').trim();
354
354
  const active = settings?.active ?? [];
355
- const suffix = active.length ? ` · settings: ${active.map((s) => `${oneLine(s.key)}=${oneLine(s.value)}`).join(' · ')}` : '';
355
+ // A RETIRED knob is rendered as retired, never as an armed capability (D3 Invariant E): the line
356
+ // exists in the user's file, so hiding it would be a silent deletion — but reading it as active
357
+ // would claim a capability the wrapper no longer has.
358
+ const suffix = active.length ? ` · settings: ${active.map((s) => `${oneLine(s.key)}=${oneLine(s.value)}${s.retired ? ' (RETIRED — arms nothing)' : ''}`).join(' · ')}` : '';
356
359
  // The autonomy segment (AD-044 Plan 4): rendered ONLY when the caller supplies the computed
357
360
  // facts (composeAutonomyFacts) — an omitted param keeps the line byte-identical (the settings-
358
361
  // suffix precedent). Fact-only: effective per-activity levels + the render-sync state; an absent
@@ -57,9 +57,10 @@ import { shellQuoteArg } from './review-state.mjs';
57
57
  import { isFinalCapableDeclaration } from './run-gates.mjs';
58
58
  import { resolveGitHooksPath } from './commit-guard.mjs';
59
59
  import { loadConfig } from './orchestration-config.mjs';
60
- import { DEFAULT_BUNDLE_ROOT, SETTINGS_FILENAME, settingsPath, parseSettings, duplicateKeys } from './bridge-settings-read.mjs';
60
+ import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
61
61
  import { assertContainedRealPath } from './fs-safe.mjs';
62
62
  import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
63
+ import { preflightCheapAgents } from './cheap-agents.mjs';
63
64
 
64
65
  const HERE = dirname(fileURLToPath(import.meta.url));
65
66
  const toolPath = (rel) => join(HERE, rel);
@@ -105,10 +106,9 @@ export const SEVERITIES = Object.freeze({
105
106
  'read-lane.stale': SEVERITY_ATTENTION,
106
107
  'read-lane.missing': SEVERITY_ATTENTION,
107
108
  'state-block': SEVERITY_OPTIONAL,
109
+ agents: SEVERITY_OPTIONAL,
108
110
  'family-freshness': SEVERITY_ATTENTION,
109
111
  'sandbox-masks': SEVERITY_OPTIONAL,
110
- 'agy-adddir': SEVERITY_OPTIONAL,
111
- 'agy-adddir.invalid-env': SEVERITY_ATTENTION,
112
112
  'sandbox-lane': SEVERITY_OPTIONAL,
113
113
  'worktrees-dir': SEVERITY_OPTIONAL,
114
114
  });
@@ -163,11 +163,10 @@ export const WHATS = Object.freeze({
163
163
  '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',
164
164
  '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',
165
165
  'state-block': 'nothing checks the closing state block — a turn that ends on «nothing needed from you», or on a promise it never started, passes unseen',
166
+ agents: '{n} read-only subagent(s) not placed (Claude Code) — no shell-free vehicle for that work; the apply PREVIEWS first',
166
167
  'family-freshness': '{parts}',
167
168
  'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
168
169
  'sandbox-masks.stale-real': '{n} sandbox device mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
169
- 'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
170
- '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',
171
170
  'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
172
171
  'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
173
172
  });
@@ -211,19 +210,73 @@ export const BENEFITS = Object.freeze({
211
210
  'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
212
211
  'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
213
212
  'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
214
- 'review-recipe': 'review coverage — the review recipe you configured actually runs instead of silently degrading',
213
+ 'review-recipe': 'recipe coverage — the review AND execution recipes you configured actually run instead of silently degrading',
215
214
  'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
216
215
  'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
217
216
  'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
218
217
  'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
219
218
  'state-block': 'no silent stalls — a turn ending on «you are not needed», or on work it never started, warns at once instead of waiting to be spotted',
219
+ agents: 'cost and quiet — mechanical work runs on a cheap model, and no vehicle has a shell, so a read-only fan-out cannot flood you with prompts',
220
220
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
221
221
  'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
222
- 'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
223
222
  'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
224
223
  'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
225
224
  });
226
225
 
226
+ // ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
227
+ // 3.14.0 shipped a capability with every surface an AGENT reads and NO advisor entry, so `upgrade`
228
+ // told a user their setup was optimal while the thing that had just shipped sat unwired. Every other
229
+ // surface of a mode is drift-guarded; the advisor was the one that was not, and it is the only
230
+ // surface a user receives PASSIVELY.
231
+ //
232
+ // Keyed by CAPABILITY, not by mode: a per-mode registry cannot detect a new opt-in added INSIDE an
233
+ // already-registered mode (the read-lane belongs to mode `hook`), because the mode set does not move.
234
+ // Each id is DECLARED at its point of use in references/modes/<mode>.md and this registry is asserted
235
+ // set-equal to those declarations (test/advisor-coverage.test.mjs), mirroring the catalog ⟷ SKILL.md
236
+ // ⟷ mode-docs triangle commands.test.mjs already enforces.
237
+ //
238
+ // Every row carries EXACTLY ONE of `advisorKey` (the offer that observes its unconfigured state) or
239
+ // `exempt` (a stated reason it legitimately has none). Adding a capability ADDS a checked row; it
240
+ // never widens a blocklist.
241
+ export const OPT_IN_CAPABILITIES = Object.freeze([
242
+ { id: 'velocity-core', mode: 'velocity', advisorKey: 'velocity-core' },
243
+ { id: 'kit-tools-tier', mode: 'velocity', advisorKey: 'kit-tools-tier' },
244
+ { id: 'bridge-tier', mode: 'velocity', advisorKey: 'bridge-tier' },
245
+ { id: 'autonomy-render', mode: 'velocity', advisorKey: 'autonomy-render' },
246
+ { id: 'sandbox-lane', mode: 'velocity', advisorKey: 'sandbox-lane' },
247
+ { id: 'autonomy-policy', mode: 'set-autonomy', advisorKey: 'autonomy-policy' },
248
+ { id: 'sandbox-provision', mode: 'autonomy-doctor', advisorKey: 'sandbox-provision' },
249
+ { id: 'gates-declaration', mode: 'gates', advisorKey: 'gates-declaration' },
250
+ { id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
251
+ { id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
252
+ { id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
253
+ { id: 'state-block', mode: 'state-block-guard', advisorKey: 'state-block' },
254
+ { id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
255
+ { id: 'worktrees-dir', mode: 'worktrees', advisorKey: 'worktrees-dir' },
256
+ { id: 'family-freshness', mode: 'upgrade', advisorKey: 'family-freshness' },
257
+ { id: 'review-recipe', mode: 'set-recipe', advisorKey: 'review-recipe' },
258
+ // The execute slot is a DISTINCT opt-in from the review slot, and the same probe reports both —
259
+ // which is why the review-recipe benefit is worded for either slot rather than for review alone.
260
+ { id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
261
+ { id: 'agents', mode: 'agents', advisorKey: 'agents' },
262
+ // Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
263
+ // a TRUST-POSTURE change. The kit never nudges a user toward weakening their approval posture (the
264
+ // same doctrine that keeps sandbox network/filesystem allowances HAND-APPLY); velocity presents the
265
+ // full honest posture at its own consent moment, where the user is already deciding.
266
+ {
267
+ id: 'accept-edits',
268
+ mode: 'velocity',
269
+ exempt: 'a trust-posture change (auto-applied edits, auto-run mkdir/touch/mv/cp) — the kit never nudges a user toward weakening their own approval posture; velocity states the full posture at its own consent moment',
270
+ },
271
+ // Exempt for the mirror-image reason: the Fast tier bills at a higher credit rate, so an unprompted
272
+ // offer would be the kit nudging the user to spend money.
273
+ {
274
+ id: 'codex-fast',
275
+ mode: 'bridge-settings',
276
+ exempt: 'a paid SPEND knob (the priority tier bills at a higher credit rate) — the kit never nudges a user toward spending money; bridge-settings surfaces it with its cost caveat when asked',
277
+ },
278
+ ].map((c) => Object.freeze(c)));
279
+
227
280
  // A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
228
281
  const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
229
282
 
@@ -462,6 +515,42 @@ const probeStateBlockHook = ({ root, deps, add, skip }) => {
462
515
  }
463
516
  };
464
517
 
518
+ // The cheap-agents offer (OPT-IN-SHIPS-INVISIBLE). This item exists because the coverage registry
519
+ // surfaced its absence: `agents` is the family's SECOND `.claude/` writer, the `help` Tune tail
520
+ // advertises it, and the advisor had no entry for it — so a user who never runs `help` never learned
521
+ // it existed while the advisor reported the deployment optimal. Found by the guard, not by an incident.
522
+ // Only a PLACE action counts as a gap: `already-current` is converged and `customized-preserved` is
523
+ // the user's own edit, which the writer never clobbers and the advisor must never nag about.
524
+ const probeCheapAgents = ({ root, deps, add, skip }) => {
525
+ try {
526
+ const preflight = preflightCheapAgents({ cwd: root }, deps);
527
+ // The writer refuses below the expected lineage, so offering its command would hand the user a
528
+ // guaranteed failure — the honest surface is a stated skip naming the recovery.
529
+ if (!preflight.stampOk) {
530
+ skip('agents', new Error(`not a deployed agent-workflow project at the current lineage (found ${preflight.stamp ?? 'none'}) — run upgrade first`));
531
+ return;
532
+ }
533
+ const toPlace = preflight.plan.filter((item) => item.action === 'place');
534
+ if (toPlace.length === 0) return; // converged
535
+ // The writer's contract is «--dry-run first, ALWAYS» (references/modes/agents.md, and its stated
536
+ // invariant «preview by default»), so the rendered line is the PREVIEW — this item joins the
537
+ // dry-run-preview class the mode doc already documents for the gates-declaration seeder, where the
538
+ // same confirmation then runs the follow-up the preview prints. Rendering --apply here would have
539
+ // skipped the per-vehicle plan the user is supposed to see before consenting.
540
+ // The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
541
+ // VISIBLE deployment, and the apply slot must stay one pure executable command.
542
+ add(
543
+ 'agents',
544
+ fillTemplate(WHATS.agents, { n: toPlace.length }),
545
+ `node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`,
546
+ 'agents',
547
+ `hidden-mode deployments only: after the --apply the preview prints, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
548
+ );
549
+ } catch (err) {
550
+ skip('agents', err);
551
+ }
552
+ };
553
+
465
554
  const probeFamilyFreshness = ({ deps, add, skip }) => {
466
555
  try {
467
556
  const survey = deps.surveyFamily ?? surveyFamily;
@@ -502,53 +591,13 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
502
591
  }
503
592
  };
504
593
 
505
- const probeAgyAdddir = ({ deps, add, skip }) => {
506
- try {
507
- const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
508
- if (!probePlaced('agy-review')) return;
509
- // Configured means a VALID boolean value (the wrapper validates and falls back to the default
510
- // on garbage presence alone proves nothing). An explicit valid 0 is a user CHOICE
511
- // (refuse mode) respected, never nagged. env > file, the wrappers' own precedence.
512
- const isValidBool = (v) => v === '0' || v === '1';
513
- const env = deps.getenv ?? process.env;
514
- if (env.AGY_REVIEW_ALLOW_ADDDIR != null) {
515
- if (isValidBool(env.AGY_REVIEW_ALLOW_ADDDIR)) return; // an explicit valid env choice — respected
516
- // A SET-BUT-EMPTY env var is the wrapper's opt-out shape (${!key+x}: it shadows the file
517
- // and falls back to the built-in refuse default) — a user CHOICE, never nagged (codex).
518
- if (env.AGY_REVIEW_ALLOW_ADDDIR === '') return;
519
- // env > file: while ANY env value is set the wrapper ignores the settings file, so the file
520
- // writer cannot fix an invalid env — the honest apply is to fix/unset the env var (codex).
521
- const value = truncatedTo(oneLineOf(JSON.stringify(env.AGY_REVIEW_ALLOW_ADDDIR)), templateBudget(WHATS['agy-adddir.invalid-env']));
522
- add('agy-adddir', fillTemplate(WHATS['agy-adddir.invalid-env'], { value }), 'HAND-APPLY: unset AGY_REVIEW_ALLOW_ADDDIR in the environment (or export it as 1), THEN configure it durably via the bridge-settings writer', 'agy-adddir.invalid-env');
523
- return;
524
- }
525
- const confPath = settingsPath({ getenv: env, home: deps.home });
526
- const readFile = deps.readFile ?? readFileSync;
527
- const text = (() => {
528
- try {
529
- return readFile(confPath, 'utf8');
530
- } catch (err) {
531
- if (err?.code === 'ENOENT') return '';
532
- throw err;
533
- }
534
- })();
535
- const parsed = parseSettings(text);
536
- const fileEntries = parsed.byKey.get('AGY_REVIEW_ALLOW_ADDDIR');
537
- const fileValue = fileEntries?.length ? fileEntries[fileEntries.length - 1].value : null;
538
- if (fileValue != null && isValidBool(fileValue)) return; // env is absent here — a valid file value governs
539
- // The settings writer REFUSES a duplicate-carrying file — rendering its command would hand
540
- // the user a guaranteed failure; the honest apply is fix-duplicates-first (codex terminal).
541
- const dups = duplicateKeys(parsed);
542
- const what = fillTemplate(WHATS['agy-adddir'], { file: SETTINGS_FILENAME });
543
- if (dups.length > 0) {
544
- add('agy-adddir', what, `HAND-APPLY: ${SETTINGS_FILENAME} carries duplicate key(s) (${dups.join(', ')}) and the settings writer refuses to edit it — remove the duplicate lines by hand, THEN run: node ${q(toolPath('bridge-settings.mjs'))} --set AGY_REVIEW_ALLOW_ADDDIR=1 --apply`);
545
- return;
546
- }
547
- add('agy-adddir', what, `node ${q(toolPath('bridge-settings.mjs'))} --set AGY_REVIEW_ALLOW_ADDDIR=1 --apply`);
548
- } catch (err) {
549
- skip('agy-adddir', err);
550
- }
551
- };
594
+ // probeAgyAdddir is GONE, not silenced. It offered to arm AGY_REVIEW_ALLOW_ADDDIR with the benefit
595
+ // "large reviews — an oversized agy code review offloads to a staging dir instead of refusing".
596
+ // Headless agy AUTO-DENIES its own read_file tool, so that lane could return a confident fabrication
597
+ // (two BLOCKING findings citing lines of a file that has none, observed) or an empty SHIP, with no
598
+ // way to tell the advisor was recommending the one lane whose failure mode is undetectable. The
599
+ // knob is retired in the wrappers (recognized, arms nothing) and an oversized code review is now a
600
+ // chunked feed with a per-part delivery proof, so there is nothing left to offer.
552
601
 
553
602
  // The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
554
603
  // wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
@@ -713,7 +762,7 @@ const readAckValue = (root, deps, ackKey) => {
713
762
  // false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
714
763
  // an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
715
764
  // turns it into a stated skip (a BROKEN toggle the writer would refuse to overwrite is not "off").
716
- // A present-but-non-boolean `readLane` is a valid store the writer merges → false (offer), never a skip.
765
+ // A present-but-non-boolean value is a valid store the writer merges → false (offer), never a skip.
717
766
  const readReadLaneToggle = (root, deps) => {
718
767
  const readFile = deps.readFile ?? readFileSync;
719
768
  const lstat = deps.lstat ?? lstatSync;
@@ -739,7 +788,7 @@ const readReadLaneToggle = (root, deps) => {
739
788
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
740
789
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
741
790
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
742
- export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane', 'read-lane', 'worktrees-dir']);
791
+ export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir']);
743
792
 
744
793
  const probeSandboxLane = ({ root, deps, add, skip }) => {
745
794
  try {
@@ -926,9 +975,9 @@ const PROBES = Object.freeze([
926
975
  probeCommitGuard,
927
976
  probeReadLane,
928
977
  probeStateBlockHook,
978
+ probeCheapAgents,
929
979
  probeFamilyFreshness,
930
980
  probeMasksItem,
931
- probeAgyAdddir,
932
981
  probeSandboxLane,
933
982
  probeWorktreesDir,
934
983
  ]);
@@ -946,7 +995,8 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
946
995
  // its class differs from the base (the invalid-env attention arm).
947
996
  // `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
948
997
  // the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
949
- // egress hosts + resolved writable dirs). Single-line like apply; absent for every other item.
998
+ // egress hosts + resolved writable dirs; the worktrees-dir hand-apply-first grant advice; the
999
+ // agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
950
1000
  const add = (key, what, apply, severityKey = key, detail = null) => {
951
1001
  const problems = [];
952
1002
  if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
@@ -1005,7 +1055,8 @@ Usage:
1005
1055
  Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
1006
1056
  one composed verdict line opens every non-optimal render, then per item {severity · what is
1007
1057
  sub-optimal · the benefit in one plain line · an optional \`recipe:\` line (the sandbox-lane live
1008
- recipe, or the worktrees-dir hand-apply-first grant advice) · the exact consent-gated apply one-liner}. --cwd is
1058
+ recipe, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
1059
+ follow-up) · the exact consent-gated apply one-liner}. --cwd is
1009
1060
  REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
1010
1061
  section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
1011
1062
  stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
@@ -60,8 +60,17 @@ const renderBridges = (vm, { glyph, color }) => {
60
60
  // Absent when no knob is active, so the block stays byte-identical to before when nothing is set.
61
61
  if (b.settings?.error) lines.push(` ${pad('', MEMBER_COL)}${glyph.note} couldn't read bridge settings (${b.settings.error})`);
62
62
  else if (b.settings?.active?.length) {
63
- const active = b.settings.active.map((s) => `${s.key}=${s.value} [${s.source}]`).join(' · ');
63
+ // A RETIRED knob is CONFIGURED but arms nothing. It must still SHOW — the line really is in
64
+ // the user's file — but rendering it like any other active setting would claim a capability
65
+ // the wrapper no longer has, which is the exact confusion the retirement exists to prevent.
66
+ const active = b.settings.active
67
+ .map((s) => `${s.key}=${s.value} [${s.source}]${s.retired ? ' RETIRED — arms nothing' : ''}`)
68
+ .join(' · ');
64
69
  lines.push(` ${pad('', MEMBER_COL)}settings: ${active}`);
70
+ const retired = b.settings.active.filter((s) => s.retired);
71
+ for (const s of retired) {
72
+ lines.push(` ${pad('', MEMBER_COL)}${glyph.note} ${s.key} is retired — clear it with: bridge-settings --unset ${s.key} --apply`);
73
+ }
65
74
  }
66
75
  }
67
76
  return lines;
@@ -244,6 +244,7 @@ export const backendReceiptStatus = (receipts, backend, fingerprint) => {
244
244
  markerRejected: summary.markerRejected,
245
245
  unmarkedRejected: summary.unmarkedRejected,
246
246
  postureRejected: summary.postureRejected,
247
+ deliveryRejected: summary.deliveryRejected,
247
248
  };
248
249
  if (summary.state === 'current') {
249
250
  return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', shipClass: isShipVerdict(summary.receipt.verdict), grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
@@ -373,6 +374,9 @@ const rejectionCause = (b) => {
373
374
  if ((b.postureRejected ?? 0) > 0) {
374
375
  parts.push(`${b.postureRejected} with an absent/invalid run posture (D5) — a pre-posture wrapper minted it; re-run the review on the current bridge`);
375
376
  }
377
+ if ((b.deliveryRejected ?? 0) > 0) {
378
+ parts.push(`${b.deliveryRejected} with an absent/invalid delivery declaration (D8b) — the receipt never declared HOW the change set reached the model, so delivery was not proven; re-run the review on the current bridge`);
379
+ }
376
380
  return parts.join(' + ');
377
381
  };
378
382