@sabaiway/agent-workflow-kit 3.14.0 → 4.0.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 (64) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +3 -3
  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 +15 -0
  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 +4 -1
  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 +9 -5
  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 +157 -59
  62. package/tools/renderers.mjs +10 -1
  63. package/tools/review-state.mjs +4 -0
  64. package/tools/view-model.mjs +3 -1
@@ -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);
@@ -104,10 +105,10 @@ export const SEVERITIES = Object.freeze({
104
105
  'read-lane': SEVERITY_OPTIONAL,
105
106
  'read-lane.stale': SEVERITY_ATTENTION,
106
107
  'read-lane.missing': SEVERITY_ATTENTION,
108
+ 'state-block': SEVERITY_OPTIONAL,
109
+ agents: SEVERITY_OPTIONAL,
107
110
  'family-freshness': SEVERITY_ATTENTION,
108
111
  'sandbox-masks': SEVERITY_OPTIONAL,
109
- 'agy-adddir': SEVERITY_OPTIONAL,
110
- 'agy-adddir.invalid-env': SEVERITY_ATTENTION,
111
112
  'sandbox-lane': SEVERITY_OPTIONAL,
112
113
  'worktrees-dir': SEVERITY_OPTIONAL,
113
114
  });
@@ -161,11 +162,11 @@ export const WHATS = Object.freeze({
161
162
  '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',
162
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',
163
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
+ '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',
164
167
  'family-freshness': '{parts}',
165
168
  'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
166
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)',
167
- 'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
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',
169
170
  'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
170
171
  'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
171
172
  });
@@ -209,18 +210,73 @@ export const BENEFITS = Object.freeze({
209
210
  'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
210
211
  'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
211
212
  'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
212
- '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',
213
214
  'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
214
215
  'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
215
216
  'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
216
217
  'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
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',
217
220
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
218
221
  'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
219
- 'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
220
222
  'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
221
223
  'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
222
224
  });
223
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
+
224
280
  // A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
225
281
  const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
226
282
 
@@ -439,6 +495,62 @@ const probeReadLane = ({ root, deps, add, skip }) => {
439
495
  }
440
496
  };
441
497
 
498
+ // The state-block-guard offer (AD-075 · OPT-IN-SHIPS-INVISIBLE). This item exists because its absence
499
+ // fired: 3.14.0 shipped the detector with a mode doc, a catalog row and a README row — and no advisor
500
+ // entry — so `upgrade` reported «nothing is broken» to a user who did not have it. There is no writer
501
+ // for this hook, so the apply is a HAND-APPLY pointer at the mode doc, which carries the exact block
502
+ // and the three merge cases.
503
+ const probeStateBlockHook = ({ root, deps, add, skip }) => {
504
+ try {
505
+ const project = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root });
506
+ const local = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root });
507
+ if (isStateBlockGuardWired(project.data) || isStateBlockGuardWired(local.data)) return; // converged
508
+ add(
509
+ 'state-block',
510
+ fillTemplate(WHATS['state-block'], {}),
511
+ `HAND-APPLY: add a Stop hook running ${STATE_BLOCK_HOOK_COMMAND} to ${SETTINGS_FILE} — the exact block and the three merge cases are in references/modes/state-block-guard.md`,
512
+ );
513
+ } catch (err) {
514
+ skip('state-block', err);
515
+ }
516
+ };
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
+
442
554
  const probeFamilyFreshness = ({ deps, add, skip }) => {
443
555
  try {
444
556
  const survey = deps.surveyFamily ?? surveyFamily;
@@ -479,53 +591,13 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
479
591
  }
480
592
  };
481
593
 
482
- const probeAgyAdddir = ({ deps, add, skip }) => {
483
- try {
484
- const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
485
- if (!probePlaced('agy-review')) return;
486
- // Configured means a VALID boolean value (the wrapper validates and falls back to the default
487
- // on garbage presence alone proves nothing). An explicit valid 0 is a user CHOICE
488
- // (refuse mode) respected, never nagged. env > file, the wrappers' own precedence.
489
- const isValidBool = (v) => v === '0' || v === '1';
490
- const env = deps.getenv ?? process.env;
491
- if (env.AGY_REVIEW_ALLOW_ADDDIR != null) {
492
- if (isValidBool(env.AGY_REVIEW_ALLOW_ADDDIR)) return; // an explicit valid env choice — respected
493
- // A SET-BUT-EMPTY env var is the wrapper's opt-out shape (${!key+x}: it shadows the file
494
- // and falls back to the built-in refuse default) — a user CHOICE, never nagged (codex).
495
- if (env.AGY_REVIEW_ALLOW_ADDDIR === '') return;
496
- // env > file: while ANY env value is set the wrapper ignores the settings file, so the file
497
- // writer cannot fix an invalid env — the honest apply is to fix/unset the env var (codex).
498
- const value = truncatedTo(oneLineOf(JSON.stringify(env.AGY_REVIEW_ALLOW_ADDDIR)), templateBudget(WHATS['agy-adddir.invalid-env']));
499
- 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');
500
- return;
501
- }
502
- const confPath = settingsPath({ getenv: env, home: deps.home });
503
- const readFile = deps.readFile ?? readFileSync;
504
- const text = (() => {
505
- try {
506
- return readFile(confPath, 'utf8');
507
- } catch (err) {
508
- if (err?.code === 'ENOENT') return '';
509
- throw err;
510
- }
511
- })();
512
- const parsed = parseSettings(text);
513
- const fileEntries = parsed.byKey.get('AGY_REVIEW_ALLOW_ADDDIR');
514
- const fileValue = fileEntries?.length ? fileEntries[fileEntries.length - 1].value : null;
515
- if (fileValue != null && isValidBool(fileValue)) return; // env is absent here — a valid file value governs
516
- // The settings writer REFUSES a duplicate-carrying file — rendering its command would hand
517
- // the user a guaranteed failure; the honest apply is fix-duplicates-first (codex terminal).
518
- const dups = duplicateKeys(parsed);
519
- const what = fillTemplate(WHATS['agy-adddir'], { file: SETTINGS_FILENAME });
520
- if (dups.length > 0) {
521
- 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`);
522
- return;
523
- }
524
- add('agy-adddir', what, `node ${q(toolPath('bridge-settings.mjs'))} --set AGY_REVIEW_ALLOW_ADDDIR=1 --apply`);
525
- } catch (err) {
526
- skip('agy-adddir', err);
527
- }
528
- };
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.
529
601
 
530
602
  // The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
531
603
  // wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
@@ -617,6 +689,29 @@ export const READ_LANE_KEY = 'readLane';
617
689
  const GATE_HOOK_REL = '.claude/hooks/agent-workflow-gates.mjs';
618
690
  const BUNDLED_HOOK_ABS = join(HERE, '..', 'references', 'hooks', 'gate-approve.mjs');
619
691
 
692
+ // The state-block-guard wiring the advisor looks for. Matched on the RUNTIME FILE NAME inside the
693
+ // command, not on an exact string: the hook has no writer, so every user pastes their own path —
694
+ // a copy under `.claude/hooks/`, a kit-source path, `--require-block` or not. Any Stop entry that
695
+ // runs this runtime counts as wired, which is the honest question ("is it watching?").
696
+ export const STATE_BLOCK_HOOK_RUNTIME = 'state-block-guard.mjs';
697
+ // NO `--require-block` in the offered command. That flag turns on the absent-block report, and this
698
+ // kit does not mandate the three-part closing block — recommending it to every project would hand
699
+ // them a hook that warns after nearly every turn. The mode doc explains when to add it. (Offering the
700
+ // strict flag by default would also contradict the very reason the report was made opt-in.)
701
+ export const STATE_BLOCK_HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR/.claude/hooks/${STATE_BLOCK_HOOK_RUNTIME}"`;
702
+ const runsStateBlockGuard = (hook) => typeof hook?.command === 'string' && hook.command.includes(STATE_BLOCK_HOOK_RUNTIME);
703
+ // Both entry shapes count. The matcher-group form (an object carrying its own `hooks` array) is what
704
+ // this family ships and what was observed firing; a flat entry is accepted too, because the question
705
+ // this probe answers is "is anything watching?" — and guessing NO for a wiring that works would nag
706
+ // someone who already did the work.
707
+ const isStateBlockGuardWired = (data) => {
708
+ const entries = data?.hooks?.Stop;
709
+ if (!Array.isArray(entries)) return false;
710
+ return entries.some((entry) => (Array.isArray(entry?.hooks)
711
+ ? entry.hooks.some(runsStateBlockGuard)
712
+ : runsStateBlockGuard(entry)));
713
+ };
714
+
620
715
  // Byte-compare the placed gate hook against the bundled runtime. A read error (an unreadable placed
621
716
  // hook, a broken kit bundle) propagates → the probe states a skip, never a wrong currency verdict.
622
717
  const isPlacedHookCurrent = (root, deps) => {
@@ -667,7 +762,7 @@ const readAckValue = (root, deps, ackKey) => {
667
762
  // false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
668
763
  // an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
669
764
  // turns it into a stated skip (a BROKEN toggle the writer would refuse to overwrite is not "off").
670
- // 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.
671
766
  const readReadLaneToggle = (root, deps) => {
672
767
  const readFile = deps.readFile ?? readFileSync;
673
768
  const lstat = deps.lstat ?? lstatSync;
@@ -693,7 +788,7 @@ const readReadLaneToggle = (root, deps) => {
693
788
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
694
789
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
695
790
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
696
- 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']);
697
792
 
698
793
  const probeSandboxLane = ({ root, deps, add, skip }) => {
699
794
  try {
@@ -879,9 +974,10 @@ const PROBES = Object.freeze([
879
974
  probeGates,
880
975
  probeCommitGuard,
881
976
  probeReadLane,
977
+ probeStateBlockHook,
978
+ probeCheapAgents,
882
979
  probeFamilyFreshness,
883
980
  probeMasksItem,
884
- probeAgyAdddir,
885
981
  probeSandboxLane,
886
982
  probeWorktreesDir,
887
983
  ]);
@@ -899,7 +995,8 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
899
995
  // its class differs from the base (the invalid-env attention arm).
900
996
  // `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
901
997
  // the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
902
- // 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.
903
1000
  const add = (key, what, apply, severityKey = key, detail = null) => {
904
1001
  const problems = [];
905
1002
  if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
@@ -958,7 +1055,8 @@ Usage:
958
1055
  Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
959
1056
  one composed verdict line opens every non-optimal render, then per item {severity · what is
960
1057
  sub-optimal · the benefit in one plain line · an optional \`recipe:\` line (the sandbox-lane live
961
- 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
962
1060
  REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
963
1061
  section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
964
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
 
@@ -37,7 +37,9 @@ const bridgeVm = (b) => ({
37
37
  settings: b.settings?.error
38
38
  ? { error: b.settings.error }
39
39
  : b.settings?.active?.length
40
- ? { active: b.settings.active.map((a) => ({ key: a.key, value: a.value, source: a.source })) }
40
+ // `retired` rides along: a retired knob is CONFIGURED but arms nothing, and dropping the flag
41
+ // here rendered a dead key as an active setting on the one surface a user reads by default.
42
+ ? { active: b.settings.active.map((a) => ({ key: a.key, value: a.value, source: a.source, retired: a.retired ?? null })) }
41
43
  : null,
42
44
  });
43
45