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.
- package/CHANGELOG.md +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- 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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
30
|
-
'
|
|
31
|
-
'
|
|
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
|
-
|
|
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: [] }, [], [
|
|
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 }, [
|
|
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 {
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
216
|
-
//
|
|
217
|
-
// claude-code default). The
|
|
218
|
-
//
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
170
|
-
//
|
|
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
|
-
//
|
|
488
|
-
// action guards `options.session` before calling in, but the
|
|
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
|
|
279
|
-
'
|
|
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
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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;
|