peaks-loop 4.0.49 → 4.0.51

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 (179) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/audit-commands.js +1 -0
  5. package/dist/cli/commands/baseline-commands.js +163 -25
  6. package/dist/cli/commands/core/skill-command.js +53 -4
  7. package/dist/cli/commands/core/standards-command.d.ts +24 -0
  8. package/dist/cli/commands/core/standards-command.js +74 -0
  9. package/dist/cli/commands/hooks-commands.js +55 -38
  10. package/dist/cli/commands/share-commands.js +113 -20
  11. package/dist/cli/commands/web-commands.js +8 -1
  12. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
  13. package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
  14. package/dist/services/adapter/adapter.d.ts +30 -0
  15. package/dist/services/adapter/auto-adapter.d.ts +13 -0
  16. package/dist/services/adapter/claude-adapter.js +12 -0
  17. package/dist/services/adapter/codex-adapter.d.ts +12 -0
  18. package/dist/services/adapter/codex-adapter.js +12 -0
  19. package/dist/services/adapter/copilot-adapter.d.ts +12 -0
  20. package/dist/services/adapter/copilot-adapter.js +12 -0
  21. package/dist/services/audit/backing-detector.d.ts +25 -7
  22. package/dist/services/audit/backing-detector.js +33 -17
  23. package/dist/services/audit/enforcer-liveness.d.ts +12 -0
  24. package/dist/services/audit/enforcer-liveness.js +100 -0
  25. package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
  26. package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
  27. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
  28. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
  29. package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
  30. package/dist/services/audit/enforcers/lint-style.js +38 -2
  31. package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
  32. package/dist/services/audit/prose-ratio-calculator.js +25 -18
  33. package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
  34. package/dist/services/audit/red-lines-service.js +51 -7
  35. package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
  36. package/dist/services/capability-audit-service/independent-checker.js +140 -0
  37. package/dist/services/capability-audit-service/index.d.ts +3 -1
  38. package/dist/services/capability-audit-service/index.js +1 -0
  39. package/dist/services/capability-audit-service/runner.d.ts +17 -13
  40. package/dist/services/capability-audit-service/runner.js +76 -15
  41. package/dist/services/capability-audit-service/types.d.ts +48 -0
  42. package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
  43. package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
  44. package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
  45. package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
  46. package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
  47. package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
  48. package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
  49. package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
  50. package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
  51. package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
  52. package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
  53. package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
  54. package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
  55. package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
  56. package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
  57. package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
  58. package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
  59. package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
  60. package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
  61. package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
  62. package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
  63. package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
  64. package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
  65. package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
  66. package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
  67. package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
  68. package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
  69. package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
  70. package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
  71. package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
  72. package/dist/services/capability-guard-runner/registry.d.ts +5 -0
  73. package/dist/services/capability-guard-runner/registry.js +140 -0
  74. package/dist/services/capability-guard-runner/runner.d.ts +26 -0
  75. package/dist/services/capability-guard-runner/runner.js +63 -6
  76. package/dist/services/code/auto-compact-modes.d.ts +13 -2
  77. package/dist/services/code/auto-compact-modes.js +20 -4
  78. package/dist/services/code/post-compact-detector.js +20 -11
  79. package/dist/services/code/step-08-gate.js +21 -6
  80. package/dist/services/config/config-safety.js +11 -9
  81. package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
  82. package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
  83. package/dist/services/final-review/pre-post-diff.js +10 -2
  84. package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
  85. package/dist/services/ide/adapters/codex-adapter.js +1 -2
  86. package/dist/services/ide/adapters/cursor-adapter.js +1 -2
  87. package/dist/services/ide/adapters/hermes-adapter.js +1 -2
  88. package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
  89. package/dist/services/ide/adapters/qoder-adapter.js +1 -2
  90. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
  91. package/dist/services/ide/adapters/trae-adapter.js +1 -2
  92. package/dist/services/ide/adapters/zcode-adapter.js +0 -1
  93. package/dist/services/ide/ide-types.d.ts +0 -2
  94. package/dist/services/observability/observability-service.d.ts +1 -1
  95. package/dist/services/scan/api-diff-types.js +20 -2
  96. package/dist/services/security/safe-settings-path.js +19 -1
  97. package/dist/services/skill/skill-search-service.d.ts +3 -3
  98. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  99. package/dist/services/standards/loop-engineering-lint.js +6 -0
  100. package/dist/services/web/daemon-registry.js +27 -2
  101. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  102. package/dist/services/workspace/claude-settings-template.js +105 -83
  103. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  104. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  105. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  106. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  107. package/dist/services/workspace/workspace-service.js +11 -1
  108. package/dist/shared/fs-utils.d.ts +26 -0
  109. package/dist/shared/fs-utils.js +35 -0
  110. package/package.json +9 -7
  111. package/scripts/copy-templates.mjs +0 -12
  112. package/scripts/install-skills.mjs +154 -53
  113. package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
  114. package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
  115. package/skills/bee/peaks-prd/SKILL.md +3 -3
  116. package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
  117. package/skills/bee/peaks-prd/references/workflow.md +1 -1
  118. package/skills/bee/peaks-qa/SKILL.md +6 -7
  119. package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
  120. package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
  121. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
  122. package/skills/bee/peaks-rd/SKILL.md +2 -3
  123. package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
  124. package/skills/bee/peaks-rd/references/external-references.md +1 -1
  125. package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
  126. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
  127. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
  128. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
  129. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  130. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
  131. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  132. package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
  133. package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
  134. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
  135. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  136. package/skills/bee/peaks-security-audit/SKILL.md +3 -3
  137. package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
  138. package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
  139. package/skills/bee/peaks-ui/SKILL.md +1 -1
  140. package/skills/peaks-audit/SKILL.md +1 -1
  141. package/skills/peaks-code/SKILL.md +20 -18
  142. package/skills/peaks-code/references/context-governance.md +1 -1
  143. package/skills/peaks-code/references/dag-orchestrator.md +3 -4
  144. package/skills/peaks-code/references/external-references.md +1 -1
  145. package/skills/peaks-code/references/external-skill-invocation.md +2 -2
  146. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  147. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  148. package/skills/peaks-code/references/gstack-integration.md +1 -1
  149. package/skills/peaks-code/references/micro-cycle.md +1 -1
  150. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  151. package/skills/peaks-code/references/project-scan-checklist.md +1 -1
  152. package/skills/peaks-code/references/resume-detection.md +1 -1
  153. package/skills/peaks-code/references/runbook.md +6 -3
  154. package/skills/peaks-code/references/session-overload-signal-index.md +6 -4
  155. package/skills/peaks-code/references/startup-sequence.md +17 -17
  156. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  157. package/skills/peaks-code/references/step-11-memory-sediment.md +2 -2
  158. package/skills/peaks-code/references/sub-agent-dispatch.md +26 -25
  159. package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
  160. package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
  161. package/skills/peaks-code/references/worktree-governance.md +1 -1
  162. package/skills/peaks-final-review/SKILL.md +3 -3
  163. package/skills/peaks-ide/references/audit-log-helper.md +5 -4
  164. package/skills/peaks-resume/SKILL.md +1 -1
  165. package/skills/peaks-slice-decompose/SKILL.md +4 -4
  166. package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
  167. package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
  168. package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
  169. package/skills/peaks-solo/SKILL.md +1 -2
  170. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  171. package/dist/cli/commands/context-builder-commands.js +0 -85
  172. package/dist/services/hooks/write-gate.js +0 -111
  173. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  174. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  175. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  176. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  177. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  178. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  179. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -5,9 +5,10 @@ import { fail, ok } from 'peaks-loop-shared/result';
5
5
  import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
6
6
  import { findProjectRoot } from '../../services/config/config-safety.js';
7
7
  import { applyHookInstall, planHookInstall, readHookStatus, readInstalledEntriesFromSettings, removeHookInstall, listSuperpowersDenyEntries } from '../../services/skills/hooks-settings-service.js';
8
+ import { resolveHookEntries, resolveHookSpec } from '../../services/skills/hooks-codegate-superpowers.js';
8
9
  import { readJsonObjectFile } from '../../services/ide/shared/atomic-json.js';
9
10
  import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
10
- import { getAdapter, resolveIdeOptionHelp } from '../../services/ide/ide-registry.js';
11
+ import { resolveIdeOptionHelp } from '../../services/ide/ide-registry.js';
11
12
  /**
12
13
  * This module's own directory — `<root>/src/cli/commands` in the source tree,
13
14
  * `<root>/dist/cli/commands` in a build. Same reason as
@@ -50,20 +51,23 @@ function resolveIdeForCommand(options, projectRoot) {
50
51
  * service did not write.
51
52
  */
52
53
  function listExpectedEntriesForIde(ide, _skipProgress = false) {
53
- const adapter = getAdapter(ide);
54
- if (ide === 'trae') {
55
- return [{ matcher: adapter.toolMatcher, sentinel: 'peaks hook handle' }];
56
- }
57
- // Slice 2026-08-06-codegate-vendor-neutral: Claude Code install
58
- // also emits the Edit|Write|MultiEdit code-gate entry. The summary
59
- // mirrors the install shape, NOT a hardcoded expected list. The
60
- // hook source of truth is `src/services/hooks/pre-tool-code-gate.sh`
61
- // (vendor-neutral); the runtime adapter is `peaks code-gate --json`
62
- // registered as a PreToolUse entry.
63
- return [
64
- { matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' },
65
- { matcher: 'Edit|Write|MultiEdit', sentinel: 'peaks code-gate' }
66
- ];
54
+ // Derived from `resolveHookEntries(ide)` — the same function `planHookInstall`
55
+ // / `applyHookInstall` write from — so the summary can only ever report a
56
+ // shape the install actually produces. The previous version was a
57
+ // hand-written literal under a comment claiming it "mirrors the install
58
+ // shape, NOT a hardcoded expected list" (diagnosis 2026-09-15, C4); the
59
+ // literal had drifted from that claim in both directions: it re-typed the
60
+ // code-gate matcher instead of reading `HOOK_CODE_GATE_MATCHER`, and it
61
+ // reported 2 of the 6 entries a claude-code install writes.
62
+ //
63
+ // The filter is the tool-call event. `resolveHookEntries` also returns the
64
+ // once-per-session SessionStart / PostCompact entries; those are not what a
65
+ // per-call hook-entry summary is read for, and leaving them out is the only
66
+ // place this summary is narrower than the install.
67
+ const event = resolveHookSpec(ide).hookEnforceEvent;
68
+ return resolveHookEntries(ide, _skipProgress)
69
+ .filter((entry) => entry.event === event)
70
+ .map((entry) => ({ matcher: entry.matcher, sentinel: entry.sentinel }));
67
71
  }
68
72
  /**
69
73
  * Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): copy the
@@ -108,6 +112,31 @@ function readOnDiskDenyEntries(settings) {
108
112
  return [];
109
113
  return deny.filter((d) => typeof d === 'string');
110
114
  }
115
+ /**
116
+ * One `nextActions` line for a hook-script copy, or `null` when there is
117
+ * nothing to say.
118
+ *
119
+ * Diagnosis 2026-09-15 (C7): the copy failure used to be reported as a single
120
+ * sentence — `'<x> hook not copied (source missing or non-global scope)'` —
121
+ * which merged a real build failure into the expected project-scope no-op.
122
+ * Only `scope === 'global'` copies these scripts at all (see the call sites),
123
+ * so in project scope the sentence described normal behaviour in the wording
124
+ * of a fault, and a healthy `peaks hooks install` read as broken.
125
+ *
126
+ * Scope is therefore what decides the sentence, and it is knowable here — the
127
+ * copy helper cannot tell the two cases apart because both return
128
+ * `copied: false`, but the caller can. Project scope emits no line: there is
129
+ * no action to take, and the envelope's `bridgeHookCopy` / `codeGateHookCopy`
130
+ * field still reports `copied: false` for anyone reading the JSON.
131
+ */
132
+ function describeHookCopy(label, copy, scope, dryRun) {
133
+ if (scope !== 'global')
134
+ return null;
135
+ if (copy.copied) {
136
+ return dryRun ? `would copy ${label} from ${copy.source} to ${copy.target}` : `Copied ${label}: ${copy.target}`;
137
+ }
138
+ return `${label} NOT copied — source missing at ${copy.source}. Run the build (\`pnpm build\`) so the script ships with the package.`;
139
+ }
111
140
  function copyBridgeHookIfPresent(userHome, dryRun = false) {
112
141
  const source = resolve(MODULE_DIR, '..', '..', 'services', 'hooks', 'pre-tool-superpowers-bridge.sh');
113
142
  const target = resolve(userHome, '.claude', 'skills', 'peaks-code', 'hooks', 'pre-tool-superpowers-bridge.sh');
@@ -202,22 +231,16 @@ export function registerHooksCommands(program, io) {
202
231
  // committed, shared file.
203
232
  ...plan.entryTargets.map((entry) => `would write ${entry.matcher || '(no matcher)'} → ${entry.sentinel} to ${entry.settingsPath}`),
204
233
  `would write ${listSuperpowersDenyEntries().length} permissions.deny entries (Layer 3 worktree governance)`,
205
- bridgeCopy.copied
206
- ? `would copy bridge hook from ${bridgeCopy.source} to ${bridgeCopy.target}`
207
- : 'would not copy bridge hook (source missing or non-global scope)',
208
- codeGateCopy.copied
209
- ? `would copy code-gate hook from ${codeGateCopy.source} to ${codeGateCopy.target}`
210
- : 'would not copy code-gate hook (source missing or non-global scope)'
211
- ]), options.json);
234
+ describeHookCopy('bridge hook', bridgeCopy, scope, true),
235
+ describeHookCopy('code-gate hook', codeGateCopy, scope, true)
236
+ ].filter((line) => line !== null)), options.json);
212
237
  return;
213
238
  }
214
239
  const result = applyHookInstall(scope, projectRoot, { ide, skipProgress });
215
- // Slice #3: build the per-IDE entries summary from the actual installed
216
- // entries, not the slice #1 PEAKS_HOOK_ENTRIES constant (which is the
217
- // claude-code default). The user's JSON envelope must reflect the IDE
218
- // they targeted. Slice #014: the install only emits the gate-enforce
219
- // entry; the summary mirrors the install shape, NOT a hardcoded
220
- // expected list.
240
+ // Slice #3: build the per-IDE entries summary for the IDE the user
241
+ // targeted, not the slice #1 PEAKS_HOOK_ENTRIES constant (which is the
242
+ // claude-code default). The summary is derived from the install's own
243
+ // entry table — see `listExpectedEntriesForIde`.
221
244
  const installedEntries = listExpectedEntriesForIde(ide, skipProgress);
222
245
  // Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): when
223
246
  // the install targets global scope, also copy the superpowers-bridge
@@ -242,16 +265,10 @@ export function registerHooksCommands(program, io) {
242
265
  // Slice 2026-07-29-worktree-layer3-deny: surface L3 deny
243
266
  // write alongside the hook install — single atomic write.
244
267
  `Layer 3 deny: wrote ${listSuperpowersDenyEntries().length} permissions.deny entries (worktree governance)`,
245
- bridgeCopy.copied
246
- ? `Copied bridge hook: ${bridgeCopy.target}`
247
- : 'Bridge hook not copied (source missing or non-global scope)',
248
- codeGateCopy.copied
249
- ? `Copied code-gate hook: ${codeGateCopy.target}`
250
- : 'Code-gate hook not copied (source missing or non-global scope)'
251
- ]
252
- : (bridgeCopy.copied
253
- ? [`Bridge hook copied: ${bridgeCopy.target}`]
254
- : []);
268
+ describeHookCopy('bridge hook', bridgeCopy, scope, false),
269
+ describeHookCopy('code-gate hook', codeGateCopy, scope, false)
270
+ ].filter((line) => line !== null)
271
+ : [describeHookCopy('bridge hook', bridgeCopy, scope, false)].filter((line) => line !== null);
255
272
  // Slice 2026-07-29-worktree-layer3-deny: emit L3 deny bookkeeping
256
273
  // in the JSON envelope so downstream automation (audit / sc) can
257
274
  // confirm Layer 3 was applied without re-reading the file. When
@@ -1,4 +1,5 @@
1
- import { resolve } from 'node:path';
1
+ import { existsSync, readdirSync, realpathSync as realpathSyncNative } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
2
3
  import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
3
4
  import { addJsonOption, printResult } from '../cli-helpers.js';
4
5
  import { readSharedChannel, writeSharedEntry, SHARED_CHANNEL_SOFT_VALUE_WARN } from 'peaks-loop-shared-channel';
@@ -275,25 +276,45 @@ export function registerAwaitCommand(parent, io) {
275
276
  const adapter = getAdapter(ide);
276
277
  const dispatcher = adapter.subAgentDispatcher;
277
278
  if (typeof dispatcher.awaitBatch !== 'function') {
278
- printResult(io, fail('sub-agent.await', 'IDE_NOT_SUPPORTED', `IDE ${ide} does not support awaitBatch (1.2 MVP only ships claude-code)`, { ok: false }, [
279
- 'Switch to claude-code, or rely on LLM-side await for non-claude-code IDEs in slice 1.3.'
279
+ printResult(io, fail('sub-agent.await', 'IDE_NOT_SUPPORTED', `IDE ${ide} does not support awaitBatch`, { ok: false }, [
280
+ 'Every built-in adapter has an awaitBatch since slice 1.3; a dispatcher without one is a custom adapter registered outside the built-in set.'
280
281
  ]), asJson);
281
282
  process.exitCode = 1;
282
283
  return;
283
284
  }
284
- // 1.2 MVP: we don't keep a separate record path index for DAG-dispatched
285
- // batches yet; the caller is expected to have a single shared record
286
- // directory. We pass the empty list — the MVP runner tracks outcomes
287
- // through its own contract-store writes; the dispatcher just signals
288
- // "ready to await" through the awaitBatch LRU queue (slice 1.3
289
- // upgrades to cross-process heartbeat polling).
290
- const input = {
291
- batchId: options.batch,
292
- dispatchCount: 1,
293
- recordPaths: [],
294
- ...(timeoutMs !== undefined ? { timeoutMs } : {})
295
- };
296
285
  try {
286
+ // Slice 2026-09-16-n1-await-reports: resolve the batch's records from the
287
+ // session's dispatch directory. `recordPaths` used to be hardcoded to
288
+ // `[]`, and `awaitBatch` short-circuits on an empty list
289
+ // (await-batch.ts:134) — so `await` reported zero results and exited 0
290
+ // for every batch, forever. A batch we cannot locate is now a failure,
291
+ // not a success with nothing in it.
292
+ const { readRecord } = await import('../../services/dispatch/dispatch-record-writer.js');
293
+ const scan = resolveBatchRecords({
294
+ projectRoot,
295
+ sessionId: sid,
296
+ batchId: options.batch,
297
+ readOne: readRecord
298
+ });
299
+ if (scan.recordPaths.length === 0) {
300
+ const unreadableNote = scan.unreadable.length > 0
301
+ ? ` ${scan.unreadable.length} dispatch record(s) there could not be read, so they could not be matched to this batch: ${scan.unreadable.join(', ')}`
302
+ : '';
303
+ printResult(io, fail('sub-agent.await', 'NO_DISPATCH_RECORDS', `No dispatch record with batchId=${options.batch} under ${scan.sessionDir}.${unreadableNote}`, {
304
+ ok: false,
305
+ batchId: options.batch,
306
+ sessionDir: scan.sessionDir,
307
+ unreadableRecords: scan.unreadable
308
+ }, [awaitErrorNextActions('NO_DISPATCH_RECORDS')]), asJson);
309
+ process.exitCode = 1;
310
+ return;
311
+ }
312
+ const input = {
313
+ batchId: options.batch,
314
+ dispatchCount: scan.recordPaths.length,
315
+ recordPaths: scan.recordPaths,
316
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
317
+ };
297
318
  const results = await dispatcher.awaitBatch(input);
298
319
  const summary = summarizeBatchResults(results);
299
320
  printResult(io, ok('sub-agent.await', {
@@ -302,9 +323,19 @@ export function registerAwaitCommand(parent, io) {
302
323
  batchId: options.batch,
303
324
  ide: dispatcher.label,
304
325
  results,
305
- summary
306
- }, [], [
307
- 'For trae / trae-cn / codex / cursor, results will report status=timeout with note=`awaitByLlm: <ide> 1.2 fallback`. The calling LLM holds the real await.'
326
+ summary,
327
+ unreadableRecords: scan.unreadable
328
+ }, scan.unreadable.length > 0
329
+ ? [`${scan.unreadable.length} unreadable dispatch record(s) in this session were skipped and are NOT part of the results: ${scan.unreadable.join(', ')}`]
330
+ : [], [
331
+ // Slice 2026-09-15-s9: corrected. This used to tell users that the
332
+ // four non-Claude IDEs would report `awaitByLlm: <ide> 1.2 fallback`,
333
+ // the slice-1.2 marker that slice 1.3 replaced with a real
334
+ // file-polling await. The text survived because nothing tested it —
335
+ // no adapter produces that note any more (asserted in
336
+ // sub-agent-dispatchers.test.ts), and the emitter that produced it,
337
+ // `awaitByLlmFallback`, has since been removed.
338
+ `Each non-claude-code IDE labels its own results (see the \`note\` field), so a timed-out slot is attributable to the adapter it came from.`
308
339
  ]), asJson);
309
340
  }
310
341
  catch (error) {
@@ -323,8 +354,70 @@ function awaitErrorNextActions(code) {
323
354
  if (code === 'IDE_NOT_SUPPORTED') {
324
355
  return 'Switch to claude-code, or rely on LLM-side await for non-claude-code IDEs in slice 1.3.';
325
356
  }
357
+ if (code === 'NO_DISPATCH_RECORDS') {
358
+ return 'Check --session-id / --project: records live under .peaks/_sub_agents/<sessionId>/. Use the batchId exactly as printed by the dispatch envelope.';
359
+ }
326
360
  return 'See error message; check that --batch matches the dispatch envelope and --timeout is a positive integer ms.';
327
361
  }
362
+ /**
363
+ * Resolve a record's on-disk path through `realpathSync` so the value returned
364
+ * here matches the value `writeInitialDispatchRecord` produces — which itself
365
+ * passes through `assertSafeDispatchRecordPath` and ends up canonicalized. On
366
+ * macOS, `mkdtempSync(join(tmpdir(), prefix))` returns `/var/folders/...`
367
+ * while `process.cwd()` after `chdir` returns `/private/var/folders/...`;
368
+ * without this helper the test fixture's `queuedPath` (canonical) and the
369
+ * envelope's `finalized[].recordPath` (raw) never compare equal. Falls back
370
+ * to the lexical path when the file is missing (race between scan and write).
371
+ */
372
+ function safeRecordPath(p) {
373
+ try {
374
+ return realpathSyncNative(p);
375
+ }
376
+ catch {
377
+ return p;
378
+ }
379
+ }
380
+ /**
381
+ * Slice 2026-09-16-n1-await-reports: which on-disk records belong to a batch.
382
+ *
383
+ * Reuses the conventions already in the repo instead of inventing a new one:
384
+ * - records live at `.peaks/_sub_agents/<sid>/dispatch-<rid>-<ts>.json`
385
+ * (`dispatchRecordPath`, src/services/security/safe-settings-path.ts);
386
+ * - a record's batch is its own `batchId` field, written by
387
+ * `writeInitialDispatchRecord`. The `--batch` branch of `finalize` (below)
388
+ * and `findBatchRecords` in heartbeat-watch-command.ts resolve a batch the
389
+ * same way: scan the session dir, filter `dispatch-*.json`, compare field.
390
+ *
391
+ * `unreadable` lists the `dispatch-*.json` candidates whose batch could NOT be
392
+ * determined. The caller must not fold them into "no such batch": a record it
393
+ * cannot read is not evidence that the batch is empty.
394
+ */
395
+ function resolveBatchRecords(input) {
396
+ const sessionDir = resolve(input.projectRoot, '.peaks', '_sub_agents', input.sessionId);
397
+ const recordPaths = [];
398
+ const unreadable = [];
399
+ if (!existsSync(sessionDir))
400
+ return { sessionDir, recordPaths, unreadable };
401
+ for (const name of readdirSync(sessionDir)) {
402
+ // `active-dispatches.json` (the index) and `batch-<uuid>.counter.json` are
403
+ // not records; neither carries a `version`, so `readRecord` would reject
404
+ // them as "version mismatch". Same filter as the `--batch` branch below.
405
+ if (!name.startsWith('dispatch-') || !name.endsWith('.json'))
406
+ continue;
407
+ const recordPath = safeRecordPath(join(sessionDir, name));
408
+ let batchId;
409
+ try {
410
+ batchId = input.readOne(recordPath).batchId;
411
+ }
412
+ catch {
413
+ unreadable.push(recordPath);
414
+ continue;
415
+ }
416
+ if (batchId === input.batchId)
417
+ recordPaths.push(recordPath);
418
+ }
419
+ return { sessionDir, recordPaths, unreadable };
420
+ }
328
421
  export function registerFinalizeCommand(parent, io) {
329
422
  addJsonOption(parent
330
423
  .command('finalize')
@@ -418,7 +511,7 @@ export function registerFinalizeCommand(parent, io) {
418
511
  // `--batch` branch below has always used this same filter.
419
512
  if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
420
513
  continue;
421
- const p = path2.join(dir, f);
514
+ const p = safeRecordPath(path2.join(dir, f));
422
515
  const r = tryReadRecord(p);
423
516
  if (r === null || r.requestId !== options.requestId)
424
517
  continue;
@@ -470,7 +563,7 @@ export function registerFinalizeCommand(parent, io) {
470
563
  for (const f of fs2.readdirSync(dir)) {
471
564
  if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
472
565
  continue;
473
- const p = path2.join(dir, f);
566
+ const p = safeRecordPath(path2.join(dir, f));
474
567
  const r = tryReadRecord(p);
475
568
  if (r === null)
476
569
  continue;
@@ -161,7 +161,14 @@ export async function runWebOp(io, op, args, asJson) {
161
161
  return;
162
162
  }
163
163
  const info = await ensureDaemon(projectRoot, sessionId);
164
- const response = await new WebDaemonClient(info).call(op, { ...opArgs, dispatchId: dispatchId(), projectRoot, sessionId }, OP_TIMEOUT_MS);
164
+ // The daemon reads `projectRoot` back from `daemon.json` — the record it
165
+ // itself wrote — and that round-trip is the only path that survives the
166
+ // macOS `/var` <-> `/private/var` symlink: the writer's prefix may not
167
+ // match `projectRoot` resolved from `process.cwd()` (the kernel resolves
168
+ // symlinks on `chdir`), and the integration test asserts on that exact
169
+ // round-tripped value.
170
+ const daemonProjectRoot = info.projectRoot;
171
+ const response = await new WebDaemonClient(info).call(op, { ...opArgs, dispatchId: dispatchId(), projectRoot: daemonProjectRoot, sessionId }, OP_TIMEOUT_MS);
165
172
  if (!response.ok || response.data === null) {
166
173
  const code = safeDaemonCode(response.code);
167
174
  // The daemon no longer downloads (R3), so "the browser is not installed"
@@ -60,4 +60,10 @@ export interface WorkflowTerminalizeOptions {
60
60
  readonly requireConsumed?: boolean;
61
61
  readonly json?: boolean;
62
62
  }
63
+ /**
64
+ * The literal that means "no session could be resolved". Kept as a named
65
+ * constant because three call sites now have to ASK whether resolution
66
+ * failed rather than consume the value as if it were an id.
67
+ */
68
+ export declare const UNKNOWN_SESSION_ID = "unknown-sid";
63
69
  export declare function registerWorkflowLifecycleCommand(parent: Command, io: ProgramIO): void;
@@ -9,6 +9,8 @@
9
9
  import { fail, ok, getErrorMessage } from 'peaks-loop-shared/result';
10
10
  import { addJsonOption, printResult } from '../cli-helpers.js';
11
11
  import { resolveCallerId } from '../../services/session/resolve-caller-id.js';
12
+ import { findProjectRoot } from '../../services/config/config-safety.js';
13
+ import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
12
14
  import { initWorkflow, terminalizeWorkflow, } from '../../services/workflow/workflow-presence-lifecycle.js';
13
15
  import { readGraph, emptyGraph } from '../../services/workflow/workflow-graph-store.js';
14
16
  import { WORKFLOW_ID_REGEX, TERMINAL_REASONS } from '../../services/workflow/workflow-graph-types.js';
@@ -26,8 +28,62 @@ function deriveCallerId() {
26
28
  function deriveProjectRoot(options) {
27
29
  return options.project ?? process.cwd();
28
30
  }
31
+ /**
32
+ * The literal that means "no session could be resolved". Kept as a named
33
+ * constant because three call sites now have to ASK whether resolution
34
+ * failed rather than consume the value as if it were an id.
35
+ */
36
+ export const UNKNOWN_SESSION_ID = 'unknown-sid';
37
+ /**
38
+ * Resolve the session id for every `workflow *` command.
39
+ *
40
+ * Tier order (matches the idiom every other command family in this CLI
41
+ * already uses — see `job-commands.ts:85`, `container-commands.ts:138`):
42
+ *
43
+ * 1. `--session-id <sid>` — explicit wins
44
+ * 2. `PEAKS_SESSION_ID` env var — scripted / sub-process callers
45
+ * 3. `getCurrentSessionId(projectRoot)` — this caller's OWN binding
46
+ * (`callers/<callerId>.json`) falling back to the project-global
47
+ * `.peaks/_runtime/session.json`, the same resolver `peaks session
48
+ * info --active` and `peaks session checkpoint` use
49
+ * 4. `unknown-sid` — nothing is bound
50
+ *
51
+ * Tier 3 was MISSING until 2026-09-15 (S6), and its absence is the whole
52
+ * defect: `workflow init` resolved on tiers 1–2 only, so with no flag and no
53
+ * env var it wrote the graph into `.peaks/_runtime/unknown-sid/graphs/`
54
+ * regardless of what the binding file said. `peaks workflow node prepare`
55
+ * then ran under the CORRECT session dir and reported `PEAKS_GRAPH_NOT_FOUND`
56
+ * — the graph it was asking for had been written one bucket over. The bucket
57
+ * accumulated artifacts from at least 4 distinct caller ids between 2026-09-01
58
+ * and 2026-09-15, i.e. it had never worked for anyone.
59
+ *
60
+ * The same prior-art sweep (`.peaks/memory/archived/2026-06-26-unknown-sid-root-cause.md`)
61
+ * converted six inline `?? 'unknown-sid'` sites to this 4-tier chain;
62
+ * `workflow-lifecycle-commands.ts` was missed. This is that site.
63
+ *
64
+ * The project root passed to tier 3 goes through the same `findProjectRoot`
65
+ * walk `session info` uses, so a command run from a SUBDIRECTORY of the
66
+ * project resolves the project's binding instead of `cwd`'s (which has none).
67
+ */
29
68
  function deriveSessionId(options) {
30
- return options.sessionId ?? process.env.PEAKS_SESSION_ID ?? 'unknown-sid';
69
+ if (typeof options.sessionId === 'string' && options.sessionId.length > 0)
70
+ return options.sessionId;
71
+ const fromEnv = process.env.PEAKS_SESSION_ID;
72
+ if (typeof fromEnv === 'string' && fromEnv.length > 0)
73
+ return fromEnv;
74
+ return getCurrentSessionId(findProjectRoot(options.project ?? process.cwd()) ?? (options.project ?? process.cwd())) ?? UNKNOWN_SESSION_ID;
75
+ }
76
+ /**
77
+ * Tier 3 resolving to `unknown-sid` means NO session is bound at all. A
78
+ * `workflow` subcommand that WRITES must fail loudly on that rather than
79
+ * create a bucket named after a failure — `graph list` already does (see its
80
+ * `PEAKS_SESSION_NOT_BOUND` guard below); `init` did not.
81
+ */
82
+ function buildSessionNotBoundEnvelope(sessionId, projectRoot) {
83
+ return fail('workflow.init', 'PEAKS_SESSION_NOT_BOUND', `No peaks session is bound for '${projectRoot}' (derived session id '${sessionId}'): --session-id, PEAKS_SESSION_ID and the project binding are all absent.`, { workflowId: null }, [
84
+ 'Run `peaks workspace init --project <p>` to bind a session, then re-run.',
85
+ 'Or pass `--session-id <sid>` explicitly.',
86
+ ]);
31
87
  }
32
88
  export function registerWorkflowLifecycleCommand(parent, io) {
33
89
  // Reuse the existing `workflow` parent (created by `registerWorkflowCommands`)
@@ -46,8 +102,13 @@ export function registerWorkflowLifecycleCommand(parent, io) {
46
102
  const asJson = options.json === true;
47
103
  try {
48
104
  const callerId = deriveCallerId();
49
- const sessionId = deriveSessionId(options);
50
105
  const projectRoot = deriveProjectRoot(options);
106
+ const sessionId = deriveSessionId(options);
107
+ if (sessionId === UNKNOWN_SESSION_ID) {
108
+ printResult(io, buildSessionNotBoundEnvelope(sessionId, projectRoot), asJson);
109
+ process.exitCode = 1;
110
+ return;
111
+ }
51
112
  const workflowId = options.workflowId ?? `wf-${Date.now().toString(36)}`;
52
113
  if (!WORKFLOW_ID_REGEX.test(workflowId)) {
53
114
  throw new Error(`workflowId shape invalid: ${workflowId}`);
@@ -120,7 +181,7 @@ export function registerWorkflowLifecycleCommand(parent, io) {
120
181
  const asJson = options.json === true;
121
182
  try {
122
183
  const sessionId = deriveSessionId(options);
123
- if (!sessionId || sessionId === 'unknown-sid') {
184
+ if (sessionId === UNKNOWN_SESSION_ID) {
124
185
  throw new Error('PEAKS_SESSION_NOT_BOUND: no session id');
125
186
  }
126
187
  const result = { envelopeVersion: '4.0.8', sessionId, graphs: [] };
@@ -1,3 +1,33 @@
1
+ /**
2
+ * ⚠ DEAD CODE — NOT THE LIVE VENDOR LAYER, AND NOT THE LIVE IDE LAYER.
3
+ *
4
+ * Three things in this repo are named `<Vendor>Adapter`, and only one of
5
+ * them is live. Read this table before importing anything from here:
6
+ *
7
+ * 1. `src/services/runtime/vendors/<vendor>.ts`
8
+ * → `class CodexAdapter implements VendorAdapter` (runtime detect +
9
+ * compact). LIVE — wired by `RuntimeService`'s built-in list. This is
10
+ * the one a reader looking for "the codex adapter" wants.
11
+ * 2. `packages/peaks-loop-internal-runtime/src/vendor/<vendor>-adapter.ts`
12
+ * → a second `CodexAdapter`, different surface (binary / headlessArgs /
13
+ * parseStatusLine / detectInstalled) for the dispatched-sub-agent
14
+ * runtime. LIVE — exported by that package's public index.
15
+ * 3. THIS FILE'S SIBLINGS — `claude-adapter.ts` / `codex-adapter.ts` /
16
+ * `copilot-adapter.ts` + `auto-adapter.ts`.
17
+ * → **UNREFERENCED.** Zero importers in `src/`, `tests/`, `packages/`
18
+ * or `scripts/`. Every method throws `ADAPTER_NOT_IMPLEMENTED`; the
19
+ * one exception, `detect()` on the codex/copilot pair, is a hardcoded
20
+ * `return false`. `peaks skill adapter list` (the only CLI surface
21
+ * that names these) does not read them — it returns a literal array.
22
+ *
23
+ * The three layers diverged from a single planned skill-adapter surface and
24
+ * were never reconciled. These placeholder files are kept because they mark
25
+ * intent for unimplemented work; they are NOT kept because anything uses
26
+ * them. If you are here to implement them, the decision you actually need is
27
+ * which of layers 1–3 survives — see
28
+ * `tests/unit/runtime/vendor-adapter-layer.test.ts`, which pins which layer
29
+ * is live and fails the moment that stops being true.
30
+ */
1
31
  export interface AdapterSegment {
2
32
  name: string;
3
33
  skillMd: string;
@@ -1,3 +1,16 @@
1
+ /**
2
+ * ⚠ DEAD CODE — zero importers in `src/`, `tests/`, `packages/`, `scripts/`.
3
+ * See the block comment on `src/services/adapter/adapter.ts`.
4
+ *
5
+ * `detectAndPick()` picks the first adapter whose `detect()` returns true.
6
+ * It cannot pick anything today: its only candidate implementations are its
7
+ * three dead siblings, and the codex/copilot pair hardcode `detect()` to
8
+ * `false` while the claude one hardcodes `true` — so the "pick" is a
9
+ * constant that never varies with the environment. It is not wired to
10
+ * `peaks skill adapter set-active` either; that CLI verb
11
+ * (`src/cli/commands/adapter-commands.ts`) echoes back the name it was given
12
+ * and reads no adapter file.
13
+ */
1
14
  import { Adapter } from "./adapter.js";
2
15
  type Detectable = Pick<Adapter, "name" | "detect">;
3
16
  export declare class AutoAdapter {
@@ -1,3 +1,15 @@
1
+ /**
2
+ * ⚠ DEAD CODE — not a live adapter. See the block comment on
3
+ * `src/services/adapter/adapter.ts` for the three same-named layers.
4
+ *
5
+ * This is the only file in the cluster with a real implementation, which is
6
+ * what makes it the most misleading one: it writes a real
7
+ * `~/.claude/skills/peaks-bee-<name>.peaks-generated/SKILL.md` scratch dir if
8
+ * you call it. Nothing calls it — zero importers in `src/`, `tests/`,
9
+ * `packages/` or `scripts/`. The live Claude Code knowledge lives in
10
+ * `src/services/ide/adapters/claude-code-adapter.ts` (IDE surface) and
11
+ * `src/services/runtime/vendors/claude-code.ts` (runtime compact).
12
+ */
1
13
  import { writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
2
14
  import { join } from "node:path";
3
15
  export class ClaudeAdapter {
@@ -1,3 +1,15 @@
1
+ /**
2
+ * ⚠ DEAD CODE — not the live `CodexAdapter`. See the block comment on
3
+ * `src/services/adapter/adapter.ts` for the three same-named layers, and
4
+ * `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
5
+ *
6
+ * The class below has zero importers anywhere in this repo. The live codex
7
+ * adapter is `src/services/runtime/vendors/codex.ts` (runtime detect +
8
+ * compact, wired into `RuntimeService`) and
9
+ * `packages/peaks-loop-internal-runtime/src/vendor/codex-adapter.ts` (the
10
+ * dispatched-sub-agent runtime). `detect()` here returns a hardcoded
11
+ * `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
12
+ */
1
13
  import { Adapter } from "./adapter.js";
2
14
  export declare class CodexAdapter implements Pick<Adapter, "name"> {
3
15
  private readonly _o;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * ⚠ DEAD CODE — not the live `CodexAdapter`. See the block comment on
3
+ * `src/services/adapter/adapter.ts` for the three same-named layers, and
4
+ * `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
5
+ *
6
+ * The class below has zero importers anywhere in this repo. The live codex
7
+ * adapter is `src/services/runtime/vendors/codex.ts` (runtime detect +
8
+ * compact, wired into `RuntimeService`) and
9
+ * `packages/peaks-loop-internal-runtime/src/vendor/codex-adapter.ts` (the
10
+ * dispatched-sub-agent runtime). `detect()` here returns a hardcoded
11
+ * `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
12
+ */
1
13
  import { ADAPTER_NOT_IMPLEMENTED } from "./adapter.js";
2
14
  export class CodexAdapter {
3
15
  _o;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * ⚠ DEAD CODE — not the live `CopilotAdapter`. See the block comment on
3
+ * `src/services/adapter/adapter.ts` for the three same-named layers, and
4
+ * `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
5
+ *
6
+ * The class below has zero importers anywhere in this repo. The live copilot
7
+ * adapter is `src/services/runtime/vendors/copilot.ts` (runtime detect +
8
+ * compact, wired into `RuntimeService`) and
9
+ * `packages/peaks-loop-internal-runtime/src/vendor/copilot-adapter.ts` (the
10
+ * dispatched-sub-agent runtime). `detect()` here returns a hardcoded
11
+ * `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
12
+ */
1
13
  import { Adapter } from "./adapter.js";
2
14
  export declare class CopilotAdapter implements Pick<Adapter, "name"> {
3
15
  private readonly _o;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * ⚠ DEAD CODE — not the live `CopilotAdapter`. See the block comment on
3
+ * `src/services/adapter/adapter.ts` for the three same-named layers, and
4
+ * `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
5
+ *
6
+ * The class below has zero importers anywhere in this repo. The live copilot
7
+ * adapter is `src/services/runtime/vendors/copilot.ts` (runtime detect +
8
+ * compact, wired into `RuntimeService`) and
9
+ * `packages/peaks-loop-internal-runtime/src/vendor/copilot-adapter.ts` (the
10
+ * dispatched-sub-agent runtime). `detect()` here returns a hardcoded
11
+ * `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
12
+ */
1
13
  import { ADAPTER_NOT_IMPLEMENTED } from "./adapter.js";
2
14
  export class CopilotAdapter {
3
15
  _o;
@@ -2,23 +2,41 @@
2
2
  * Backing detector — classifies each red line as `cli-backed`, `partial`,
3
3
  * or `prose-only`. The classifier already sets the backing for catalog hits
4
4
  * (cli-backed when an enforcer file path is present). This module exists to
5
- * handle the post-classification nuance: heuristics for the "partial" tier
6
- * (a gate exists but the LLM can bypass it) and verification that the
7
- * enforcer file actually exists on disk.
5
+ * handle the post-classification nuances: heuristics for the "partial" tier
6
+ * (a gate exists but the LLM can bypass it), verification that the enforcer
7
+ * file exists on disk, and — since A9 of the 2026-09-15 diagnosis —
8
+ * verification that the enforcer is actually reachable from a call site.
9
+ *
10
+ * `cli-backed` means "a CLI surface runs this rule". A file that exists but
11
+ * that nothing imports runs nothing, so it is `prose-only` with the same
12
+ * weight as an unwritten rule. Callers pass the live set from
13
+ * `enforcer-liveness.ts`; `null` means "liveness undecidable for this
14
+ * project" (no `src/` tree), in which case the existing file is trusted.
8
15
  */
9
16
  import type { RedLineEntry } from './types.js';
17
+ /**
18
+ * `liveEnforcers` is the set of enforcerRefs with a call site (see
19
+ * `enforcer-liveness.ts`), or `null` when the project has no `src/` tree to
20
+ * scan and liveness therefore cannot be decided.
21
+ */
22
+ export type LiveEnforcerSet = ReadonlySet<string> | null;
10
23
  export interface BackingResult {
11
24
  readonly entry: RedLineEntry;
12
25
  readonly enforcerExists: boolean;
26
+ /** True when the enforcer file exists but no call site imports it. */
27
+ readonly enforcerDead: boolean;
13
28
  }
14
29
  /**
15
30
  * Re-classify a single RedLineEntry. Returns a new entry with the
16
- * `backing` field updated and `enforcerRef` possibly nulled if the
17
- * referenced file does not exist on disk.
31
+ * `backing` field updated; `enforcerRef` is preserved even when the
32
+ * backing is downgraded, because the dead reference is the triage
33
+ * information the report needs.
18
34
  */
19
- export declare function classifyBacking(entry: RedLineEntry, projectRoot: string): BackingResult;
35
+ export declare function classifyBacking(entry: RedLineEntry, projectRoot: string, liveEnforcers: LiveEnforcerSet): BackingResult;
20
36
  export interface BackingBatchResult {
21
37
  readonly entries: readonly RedLineEntry[];
22
38
  readonly warnings: readonly string[];
39
+ /** Sorted, deduplicated enforcerRefs downgraded for having no call site. */
40
+ readonly deadEnforcers: readonly string[];
23
41
  }
24
- export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string): BackingBatchResult;
42
+ export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string, liveEnforcers: LiveEnforcerSet): BackingBatchResult;