peaks-loop 4.0.48 → 4.0.50

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 (156) hide show
  1. package/CHANGELOG.md +44 -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/compact-command.js +1 -3
  7. package/dist/cli/commands/core/skill-command.js +53 -4
  8. package/dist/cli/commands/core/standards-command.d.ts +24 -0
  9. package/dist/cli/commands/core/standards-command.js +74 -0
  10. package/dist/cli/commands/feedback-commands.d.ts +11 -7
  11. package/dist/cli/commands/feedback-commands.js +49 -17
  12. package/dist/cli/commands/final-review-commands.js +12 -0
  13. package/dist/cli/commands/hooks-commands.js +55 -38
  14. package/dist/cli/commands/loop-eval-commands.js +22 -6
  15. package/dist/cli/commands/share-commands.js +37 -11
  16. package/dist/cli/commands/slice-integrate-commands.js +17 -0
  17. package/dist/cli/commands/web-commands.js +8 -1
  18. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
  19. package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
  20. package/dist/services/adapter/adapter.d.ts +30 -0
  21. package/dist/services/adapter/auto-adapter.d.ts +13 -0
  22. package/dist/services/adapter/claude-adapter.js +12 -0
  23. package/dist/services/adapter/codex-adapter.d.ts +12 -0
  24. package/dist/services/adapter/codex-adapter.js +12 -0
  25. package/dist/services/adapter/copilot-adapter.d.ts +12 -0
  26. package/dist/services/adapter/copilot-adapter.js +12 -0
  27. package/dist/services/artifacts/artifact-prerequisites.js +10 -0
  28. package/dist/services/artifacts/request-artifact-service.js +59 -38
  29. package/dist/services/audit/backing-detector.d.ts +25 -7
  30. package/dist/services/audit/backing-detector.js +33 -17
  31. package/dist/services/audit/enforcer-liveness.d.ts +12 -0
  32. package/dist/services/audit/enforcer-liveness.js +100 -0
  33. package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
  34. package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
  35. package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
  36. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
  37. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
  38. package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
  39. package/dist/services/audit/enforcers/lint-style.js +38 -2
  40. package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
  41. package/dist/services/audit/prose-ratio-calculator.js +25 -18
  42. package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
  43. package/dist/services/audit/red-lines-service.js +51 -7
  44. package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
  45. package/dist/services/capability-audit-service/independent-checker.js +140 -0
  46. package/dist/services/capability-audit-service/index.d.ts +3 -1
  47. package/dist/services/capability-audit-service/index.js +1 -0
  48. package/dist/services/capability-audit-service/runner.d.ts +17 -13
  49. package/dist/services/capability-audit-service/runner.js +76 -15
  50. package/dist/services/capability-audit-service/types.d.ts +48 -0
  51. package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
  52. package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
  53. package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
  54. package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
  55. package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
  56. package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
  57. package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
  58. package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
  59. package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
  60. package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
  61. package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
  62. package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
  63. package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
  64. package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
  65. package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
  66. package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
  67. package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
  68. package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
  69. package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
  70. package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
  71. package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
  72. package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
  73. package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
  74. package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
  75. package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
  76. package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
  77. package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
  78. package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
  79. package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
  80. package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
  81. package/dist/services/capability-guard-runner/registry.d.ts +5 -0
  82. package/dist/services/capability-guard-runner/registry.js +140 -0
  83. package/dist/services/capability-guard-runner/runner.d.ts +26 -0
  84. package/dist/services/capability-guard-runner/runner.js +63 -6
  85. package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
  86. package/dist/services/code/auto-compact-lifecycle.js +65 -16
  87. package/dist/services/code/auto-compact-modes.d.ts +13 -2
  88. package/dist/services/code/auto-compact-modes.js +20 -4
  89. package/dist/services/code/auto-compact-orchestrator.js +119 -19
  90. package/dist/services/code/compact-event-settle.d.ts +20 -8
  91. package/dist/services/code/compact-event-settle.js +21 -0
  92. package/dist/services/code/post-compact-detector.js +20 -11
  93. package/dist/services/code/step-08-gate.js +21 -6
  94. package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
  95. package/dist/services/config/config-safety.js +11 -9
  96. package/dist/services/context/auto-compact-types.d.ts +20 -2
  97. package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
  98. package/dist/services/feedback/feedback-promotion-service.js +341 -20
  99. package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
  100. package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
  101. package/dist/services/final-review/pre-post-diff.js +10 -2
  102. package/dist/services/job/job-progress-store.js +18 -3
  103. package/dist/services/observability/jsonl-store.d.ts +19 -0
  104. package/dist/services/observability/jsonl-store.js +27 -2
  105. package/dist/services/observability/observability-service.d.ts +11 -4
  106. package/dist/services/observability/observability-service.js +16 -3
  107. package/dist/services/prd/handoff-service.js +43 -0
  108. package/dist/services/qa/qa-business-review-state.js +19 -5
  109. package/dist/services/sc/sc-service.d.ts +8 -0
  110. package/dist/services/sc/sc-service.js +8 -1
  111. package/dist/services/scan/api-diff-types.js +20 -2
  112. package/dist/services/security/safe-settings-path.js +19 -1
  113. package/dist/services/session/getSessionDir.d.ts +33 -0
  114. package/dist/services/session/getSessionDir.js +60 -0
  115. package/dist/services/skill/skill-search-service.d.ts +3 -3
  116. package/dist/services/slice/slice-review-state.js +19 -4
  117. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  118. package/dist/services/standards/loop-engineering-lint.js +6 -0
  119. package/dist/services/web/daemon-registry.js +27 -2
  120. package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
  121. package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
  122. package/dist/services/workflow/pipeline-verify-service.js +23 -10
  123. package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
  124. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  125. package/dist/services/workspace/claude-settings-template.js +105 -83
  126. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  127. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  128. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  129. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  130. package/dist/services/workspace/workspace-service.js +11 -1
  131. package/dist/shared/fs-utils.d.ts +26 -0
  132. package/dist/shared/fs-utils.js +35 -0
  133. package/dist/shared/runtime-root.d.ts +73 -0
  134. package/dist/shared/runtime-root.js +77 -0
  135. package/package.json +9 -7
  136. package/scripts/copy-templates.mjs +0 -12
  137. package/scripts/install-skills.mjs +154 -53
  138. package/skills/bee/peaks-qa/SKILL.md +0 -1
  139. package/skills/bee/peaks-rd/SKILL.md +0 -1
  140. package/skills/peaks-code/SKILL.md +12 -10
  141. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  142. package/skills/peaks-code/references/runbook.md +3 -0
  143. package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
  144. package/skills/peaks-code/references/startup-sequence.md +2 -2
  145. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  146. package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
  147. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  148. package/dist/cli/commands/context-builder-commands.js +0 -85
  149. package/dist/services/hooks/write-gate.js +0 -111
  150. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  151. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  152. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  153. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  154. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  155. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  156. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -5,18 +5,22 @@
5
5
  * - `peaks feedback check-unpromoted --project <path> [--strict]`
6
6
  *
7
7
  * Companion to `sops/feedback-promotion-sop.md`. The promote command
8
- * generates a stub for the chosen enforcement layer (A: peaks-sop
9
- * gate, B: peaks-hooks PreToolUse, C: mode-gate hardFloorCategory)
10
- * and writes the promotion marker + sidecar + RD envelope. The
11
- * check-unpromoted command scans `.peaks/memory/*.md` for feedback
12
- * memories without a promotion marker and emits a structured list.
13
- * `--strict` flips exit code to non-zero when any unpromoted feedback
14
- * is found — used by `peaks workflow verify-pipeline` Gate H.
8
+ * writes the promotion marker + sidecar + RD envelope, and — for the
9
+ * layers whose artifact it can produce (A: a registered SOP manifest)
10
+ * — the enforcement artifact itself. Layers B and C live in shared
11
+ * files it does not own, so there it records the requirement and
12
+ * reports `effective: false` instead of claiming success.
13
+ *
14
+ * The check-unpromoted command scans `.peaks/memory/*.md` for feedback
15
+ * memories whose promotion is missing OR not backed by its layer's
16
+ * artifact, and emits a structured list. `--strict` flips exit code to
17
+ * non-zero when any is found — used by `peaks workflow verify-pipeline`
18
+ * Gate H.
15
19
  */
16
20
  import { resolve as resolvePath } from 'node:path';
17
21
  import { findProjectRoot } from '../../services/config/config-safety.js';
18
22
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
19
- import { generatePromotionStub, isPromotionLayer, listUnpromotedFeedback, parseFeedbackMemory, PROMOTION_LAYER_DETAILS, PROMOTION_LAYERS, promoteFeedback } from '../../services/feedback/feedback-promotion-service.js';
23
+ import { generatePromotionStub, isPromotionLayer, listPromotionExempt, listUnpromotedFeedback, missingArtifacts, parseFeedbackMemory, PROMOTION_LAYER_DETAILS, PROMOTION_LAYERS, promoteFeedback, promotionArtifactChecks } from '../../services/feedback/feedback-promotion-service.js';
20
24
  import { fail, ok } from 'peaks-loop-shared/result';
21
25
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
22
26
  export function registerFeedbackCommands(program, io) {
@@ -26,15 +30,16 @@ export function registerFeedbackCommands(program, io) {
26
30
  addJsonOption(feedback
27
31
  .command('promote <memory-file>')
28
32
  .description('Promote a feedback memory to one of the 3 enforcement layers (A=peaks-sop gate, B=peaks-hooks PreToolUse, C=mode-gate hardFloorCategory). ' +
29
- 'Reads `.peaks/memory/<file>.md`, generates a code stub for the chosen layer, and writes ' +
30
- 'the promotion marker (HTML comment + sidecar .promotion.json) + an RD envelope at ' +
31
- '`.peaks/_runtime/<sid>/rd/feedback-promote-<name>.json`. ' +
33
+ 'Reads `.peaks/memory/<file>.md` and writes the promotion marker (HTML comment + sidecar .promotion.json) + an RD envelope at ' +
34
+ '`.peaks/_runtime/<sid>/rd/feedback-promote-<name>.json`. Layer A additionally generates and registers the SOP manifest the ' +
35
+ 'SOP engine reads. Layers B and C live in shared files the command does not own, so it records the requirement and exits ' +
36
+ 'non-zero (PROMOTION_NOT_EFFECTIVE) until the registration is present. ' +
32
37
  'Without --layer, the CLI lists the 3 layer options as nextActions and exits with code 0 ' +
33
38
  '(use --layer <A|B|C> to actually promote, or pass --dry-run to preview the stub).')
34
39
  .option('--layer <A|B|C>', `enforcement layer (${PROMOTION_LAYERS.join(' | ')})`)
35
40
  .option('--project <path>', 'project root (default: cwd)')
36
41
  .option('--promoted-by <id>', 'identity string for the audit envelope (default: peaks-rd fork agent)')
37
- .option('--dry-run', 'preview the stub without writing the marker / sidecar / envelope')).action((memoryFile, opts) => {
42
+ .option('--dry-run', 'preview the stub without writing the marker / sidecar / envelope')).action(async (memoryFile, opts) => {
38
43
  try {
39
44
  const projectRoot = opts.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
40
45
  const memoryPath = memoryFile.endsWith('.md')
@@ -91,7 +96,7 @@ export function registerFeedbackCommands(program, io) {
91
96
  const layer = opts.layer;
92
97
  const sessionId = getCurrentSessionId(projectRoot) ?? 'unknown-sid';
93
98
  const promotedBy = opts.promotedBy ?? 'peaks-rd fork agent';
94
- const envelope = promoteFeedback({
99
+ const envelope = await promoteFeedback({
95
100
  feedbackPath: memoryPath,
96
101
  layer,
97
102
  promotedBy,
@@ -99,11 +104,26 @@ export function registerFeedbackCommands(program, io) {
99
104
  projectRoot,
100
105
  dryRun: false
101
106
  });
102
- printResult(io, ok('feedback.promote', envelope, [], [
107
+ // rid 2026-09-14-gate-h-promotion: report what was written, not what
108
+ // the stub wished for. A promotion whose layer artifact is absent is
109
+ // recorded but NOT effective, and says so with a non-zero exit.
110
+ const notes = [
103
111
  `Promoted feedback "${envelope.name}" to layer ${envelope.layer} (${envelope.layerDetail}).`,
104
112
  `Generated files: ${envelope.generatedFiles.join(', ')}`,
105
113
  `Envelope written to .peaks/_runtime/${sessionId}/rd/feedback-promote-${envelope.name}.json`
114
+ ];
115
+ if (envelope.effective) {
116
+ printResult(io, ok('feedback.promote', envelope, [], notes), opts.json);
117
+ return;
118
+ }
119
+ const missing = missingArtifacts(promotionArtifactChecks(envelope.name, envelope.layer), projectRoot);
120
+ printResult(io, fail('feedback.promote', 'PROMOTION_NOT_EFFECTIVE', `Promotion recorded but not effective — layer ${envelope.layer} is not backed by its artifact: ${missing.join('; ')}`, envelope, [
121
+ ...notes,
122
+ ...(envelope.layer === 'C'
123
+ ? ['Layer C lives in source code: register the hard-floor category in src/services/code/mode-gate.ts, then re-run promote to record it.']
124
+ : ['Apply the snippet to the file(s) named above, then re-run promote to record it.'])
106
125
  ]), opts.json);
126
+ process.exitCode = 1;
107
127
  }
108
128
  catch (err) {
109
129
  printResult(io, fail('feedback.promote', 'PROMOTE_FAILED', getErrorMessage(err), { memoryFile }, ['Verify the file is a feedback memory and re-run']), opts.json);
@@ -119,23 +139,35 @@ export function registerFeedbackCommands(program, io) {
119
139
  .option('--strict', 'exit non-zero when any unpromoted feedback is found (used by verify-pipeline Gate H)')).action((opts) => {
120
140
  try {
121
141
  const unpromoted = listUnpromotedFeedback({ projectRoot: opts.project });
142
+ const exempt = listPromotionExempt({ projectRoot: opts.project });
122
143
  const count = unpromoted.length;
123
144
  if (count === 0) {
124
- printResult(io, ok('feedback.check-unpromoted', { count: 0, unpromoted: [] }, [], [`No unpromoted feedback found in .peaks/memory/.`]), opts.json);
145
+ printResult(io, ok('feedback.check-unpromoted', { count: 0, unpromoted: [], exempt }, [], [
146
+ `No unpromoted feedback found in .peaks/memory/.`,
147
+ ...(exempt.length === 0
148
+ ? []
149
+ : [`${exempt.length} memor${exempt.length === 1 ? 'y' : 'ies'} declare themselves not-to-be-promoted: ${exempt.map((e) => `${e.name} (${e.code})`).join('; ')}.`])
150
+ ]), opts.json);
125
151
  return;
126
152
  }
127
153
  const message = `${count} feedback memor${count === 1 ? 'y is' : 'ies are'} not yet promoted to an enforcement layer.`;
128
154
  const nextActions = [
129
155
  `Run \`peaks feedback promote <memory-file> --layer <A|B|C>\` for each entry above.`,
130
156
  'A = peaks-sop gate, B = peaks-hooks PreToolUse, C = mode-gate hardFloorCategory.',
157
+ 'A marker alone does not count: the entry above names the artifact its layer still owes.',
131
158
  'See sops/feedback-promotion-sop.md for the SOP and the layer-choice rubric.'
132
159
  ];
133
160
  if (opts.strict === true) {
134
- printResult(io, fail('feedback.check-unpromoted', 'UNPROMOTED_FEEDBACK_FOUND', message, { count, unpromoted }, nextActions), opts.json);
161
+ printResult(io, fail('feedback.check-unpromoted', 'UNPROMOTED_FEEDBACK_FOUND', message, { count, unpromoted, exempt }, nextActions), opts.json);
135
162
  process.exitCode = 1;
136
163
  return;
137
164
  }
138
- printResult(io, ok('feedback.check-unpromoted', { count, unpromoted }, [message], nextActions), opts.json);
165
+ printResult(io, ok('feedback.check-unpromoted', { count, unpromoted, exempt }, [
166
+ message,
167
+ ...(exempt.length === 0
168
+ ? []
169
+ : [`${exempt.length} memor${exempt.length === 1 ? 'y' : 'ies'} declare themselves not-to-be-promoted: ${exempt.map((e) => `${e.name} (${e.code})`).join('; ')}.`])
170
+ ], nextActions), opts.json);
139
171
  }
140
172
  catch (err) {
141
173
  printResult(io, fail('feedback.check-unpromoted', 'CHECK_FAILED', getErrorMessage(err), { project: opts.project }, ['Verify --project path and re-run']), opts.json);
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import { existsSync, statSync } from 'node:fs';
28
28
  import { join, resolve } from 'node:path';
29
+ import { REQUEST_ID_PATTERN } from '../../services/artifacts/request-artifact-service.js';
29
30
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
30
31
  import { fail, ok } from 'peaks-loop-shared/result';
31
32
  import { createAnthropicRunner, LlmBindingError, LlmRequestError, resolveAnthropicConfig, } from '../../services/llm/anthropic-runner.js';
@@ -135,6 +136,17 @@ export function registerFinalReviewCommands(program, io) {
135
136
  // in two places: the pre-flight existence check (here) and the
136
137
  // stub-path envelope (below). The service computes it identically
137
138
  // at `src/services/final-review/final-review-service.ts:45-52`.
139
+ //
140
+ // Rid axis. The rid is the CLI positional and it is the FILENAME
141
+ // segment below, so it is checked here, at the join. Added 2026-09-14
142
+ // (repair R1): this join was a second-slot hole — the `--session-id`
143
+ // validation above passed and the rid slot escaped. Found by the
144
+ // widened rule D, not by a human reading the file.
145
+ if (!REQUEST_ID_PATTERN.test(rid)) {
146
+ printResult(io, fail('final-review.prepare', 'RID_INVALID', `Invalid request id: ${rid} (expected letters, digits, dots, underscores, or dashes)`, emptyFinalReviewData(rid, options.sessionId, ''), ['Pass the rid of the slice, e.g. 2026-09-14-some-slug']), options.json);
147
+ process.exitCode = 1;
148
+ return;
149
+ }
138
150
  const auditGoalPath = join(projectValidation.projectRoot, '.peaks', '_runtime', sessionValidation.sessionId, 'audit-goal', `${rid}.json`);
139
151
  // 4. Pre-flight: if the audit-goal file is missing, surface a 404
140
152
  // BEFORE the stub/provider check so the caller learns the real
@@ -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
@@ -166,14 +166,25 @@ export function registerWorkflowEvalCommands(program, io) {
166
166
  process.exitCode = 1;
167
167
  return;
168
168
  }
169
- // Sid axis. `--session` reaches two joins below (`dir` and, through
170
- // `nextEvalCaptureIndex`, the same literal), so one guard here covers
171
- // the whole `--capture-score` write path.
169
+ // Sid axis AND rid axis — the join below takes TWO ids, not one.
170
+ // Corrected 2026-09-14 (repair R1): this comment used to say one guard
171
+ // covers "the whole `--capture-score` write path". The security audit of
172
+ // `2026-09-14-cli-id-escape-instrumentation` (F1b) measured that false —
173
+ // the `--session` guard passed and the `rid` slot escaped:
174
+ // `peaks loop eval '../../../../…/EVILCYC' --capture-score --session
175
+ // <legal>` created `<projectRoot>/../EVILCYC/cycles/cycle-1.json` under
176
+ // an `ok: true` envelope. The rid is the CLI positional and has no pinned
177
+ // format, so the segment check is the control for that axis.
172
178
  if (isUnsafePathInput(options.session)) {
173
179
  printResult(io, fail('loop.eval', 'INVALID_SESSION_ID', `Invalid session id: ${options.session} (must be a single path segment)`, { provided: options.session }, ['Pass a session id that is a single path segment']), options.json);
174
180
  process.exitCode = 1;
175
181
  return;
176
182
  }
183
+ if (isUnsafePathInput(rid)) {
184
+ printResult(io, fail('loop.eval', 'INVALID_REQUEST_ID', `Invalid request id: ${rid} (must be a single path segment)`, { provided: rid }, ['Pass a request id that is a single path segment']), options.json);
185
+ process.exitCode = 1;
186
+ return;
187
+ }
177
188
  const score = envelope.degraded ? 0.25 : (envelope.gateAction === 'pass' ? 1.0 : envelope.gateAction === 'warn' ? 0.5 : 0.0);
178
189
  const dir = join(projectRoot, '.peaks', '_runtime', options.session, 'loop', rid, 'cycles');
179
190
  const n = nextEvalCaptureIndex(projectRoot, options.session, rid);
@@ -484,12 +495,17 @@ export function registerWorkflowEvalCommands(program, io) {
484
495
  * writes. The run-driver also writes to the same dir; the next
485
496
  * index = max(prior)+1, or 1. */
486
497
  function nextEvalCaptureIndex(projectRoot, sid, rid) {
487
- // Sid axis: this function is the join for the capture-score cycle dir. The
488
- // action guards `options.session` before calling in, but the seam is callable
489
- // on its own, so the join states its own contract.
498
+ // BOTH id axes: this function is the join for the capture-score cycle dir.
499
+ // The action guards `options.session` and `rid` before calling in, but the
500
+ // seam is callable on its own, so the join states its own contract. The rid
501
+ // half was missing until 2026-09-14 (repair R1) — the same second-slot hole
502
+ // the action had, in the same file, one function below it.
490
503
  if (isUnsafePathInput(sid)) {
491
504
  throw new Error(`Invalid session id: ${sid} (must be a single path segment)`);
492
505
  }
506
+ if (isUnsafePathInput(rid)) {
507
+ throw new Error(`Invalid request id: ${rid} (must be a single path segment)`);
508
+ }
493
509
  const dir = join(projectRoot, '.peaks', '_runtime', sid, 'loop', rid, 'cycles');
494
510
  if (!existsSync(dir))
495
511
  return 1;
@@ -1,3 +1,4 @@
1
+ import { realpathSync as realpathSyncNative } from 'node:fs';
1
2
  import { resolve } from 'node:path';
2
3
  import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
3
4
  import { addJsonOption, printResult } from '../cli-helpers.js';
@@ -275,18 +276,18 @@ 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).
285
+ // No record-path index is kept for DAG-dispatched batches yet; the caller
286
+ // is expected to have a single shared record directory. We pass the empty
287
+ // list — the runner tracks outcomes through its own contract-store writes,
288
+ // so the dispatcher reaches no slot and reports no results (see the
289
+ // `recordPaths: []` case in tests/unit/services/dispatch/
290
+ // sub-agent-dispatchers.test.ts, which pins that empty shape).
290
291
  const input = {
291
292
  batchId: options.batch,
292
293
  dispatchCount: 1,
@@ -304,7 +305,14 @@ export function registerAwaitCommand(parent, io) {
304
305
  results,
305
306
  summary
306
307
  }, [], [
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.'
308
+ // Slice 2026-09-15-s9: corrected. This used to tell users that the
309
+ // four non-Claude IDEs would report `awaitByLlm: <ide> 1.2 fallback`,
310
+ // the slice-1.2 marker that slice 1.3 replaced with a real
311
+ // file-polling await. The text survived because nothing tested it —
312
+ // no adapter produces that note any more (asserted in
313
+ // sub-agent-dispatchers.test.ts), and the only code that still emits
314
+ // it, `awaitByLlmFallback`, has no callers.
315
+ `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
316
  ]), asJson);
309
317
  }
310
318
  catch (error) {
@@ -325,6 +333,24 @@ function awaitErrorNextActions(code) {
325
333
  }
326
334
  return 'See error message; check that --batch matches the dispatch envelope and --timeout is a positive integer ms.';
327
335
  }
336
+ /**
337
+ * Resolve a record's on-disk path through `realpathSync` so the value returned
338
+ * here matches the value `writeInitialDispatchRecord` produces — which itself
339
+ * passes through `assertSafeDispatchRecordPath` and ends up canonicalized. On
340
+ * macOS, `mkdtempSync(join(tmpdir(), prefix))` returns `/var/folders/...`
341
+ * while `process.cwd()` after `chdir` returns `/private/var/folders/...`;
342
+ * without this helper the test fixture's `queuedPath` (canonical) and the
343
+ * envelope's `finalized[].recordPath` (raw) never compare equal. Falls back
344
+ * to the lexical path when the file is missing (race between scan and write).
345
+ */
346
+ function safeRecordPath(p) {
347
+ try {
348
+ return realpathSyncNative(p);
349
+ }
350
+ catch {
351
+ return p;
352
+ }
353
+ }
328
354
  export function registerFinalizeCommand(parent, io) {
329
355
  addJsonOption(parent
330
356
  .command('finalize')
@@ -418,7 +444,7 @@ export function registerFinalizeCommand(parent, io) {
418
444
  // `--batch` branch below has always used this same filter.
419
445
  if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
420
446
  continue;
421
- const p = path2.join(dir, f);
447
+ const p = safeRecordPath(path2.join(dir, f));
422
448
  const r = tryReadRecord(p);
423
449
  if (r === null || r.requestId !== options.requestId)
424
450
  continue;
@@ -470,7 +496,7 @@ export function registerFinalizeCommand(parent, io) {
470
496
  for (const f of fs2.readdirSync(dir)) {
471
497
  if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
472
498
  continue;
473
- const p = path2.join(dir, f);
499
+ const p = safeRecordPath(path2.join(dir, f));
474
500
  const r = tryReadRecord(p);
475
501
  if (r === null)
476
502
  continue;
@@ -10,6 +10,7 @@ import { existsSync, readFileSync } from 'node:fs';
10
10
  import { join, resolve } from 'node:path';
11
11
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
12
12
  import { isUnsafePathInput } from '../../shared/path-safety.js';
13
+ import { SLICE_ID_PATTERN } from '../../services/sc/sc-service.js';
13
14
  import { integrateSlices } from '../../services/slice/slice-integration.js';
14
15
  import { fail, ok } from 'peaks-loop-shared/result';
15
16
  import { addJsonOption, printResult } from '../cli-helpers.js';
@@ -23,6 +24,22 @@ function loadContractsForSlices(projectRoot, sessionId, sliceIds) {
23
24
  return [];
24
25
  const result = [];
25
26
  for (const sliceId of sliceIds) {
27
+ // Slice-id axis: the id becomes the FILENAME below, so `--slices a,../..`
28
+ // reads a JSON file outside the contracts dir. Guarded 2026-09-14 (repair
29
+ // R1, security audit F3 of `2026-09-14-cli-id-escape-instrumentation`) —
30
+ // the session guard above covers the other axis and this one was open.
31
+ // Skipped with a warning rather than thrown, matching this function's
32
+ // existing contract for a contract it cannot read.
33
+ //
34
+ // NOTE for the instrument: rule D in
35
+ // `tests/unit/runtime/no-runtime-input-guard.test.ts` does NOT see this
36
+ // site — its root is the local `const dir` above, not a same-file call, so
37
+ // it is that rule's limit (m). The guard is real; the coverage is not
38
+ // claimed.
39
+ if (!SLICE_ID_PATTERN.test(sliceId)) {
40
+ console.warn(`loadContractsForSlices: skipping invalid slice id ${JSON.stringify(sliceId)} (must be a single filename segment)`);
41
+ continue;
42
+ }
26
43
  const file = join(dir, `${sliceId}.json`);
27
44
  if (!existsSync(file))
28
45
  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;