peaks-loop 4.0.47 → 4.0.49
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/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.js +48 -8
- package/dist/cli/commands/compact-command.js +110 -0
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -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 +4 -4
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-eval-commands.js +31 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +12 -0
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/request-commands.js +8 -0
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/slice-integrate-commands.js +22 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/index.js +5 -45
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +140 -65
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +77 -46
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +194 -0
- package/dist/services/code/auto-compact-lifecycle.js +229 -11
- package/dist/services/code/auto-compact-orchestrator.js +118 -7
- package/dist/services/code/compact-event-settle.d.ts +134 -0
- package/dist/services/code/compact-event-settle.js +240 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -0
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/evidence/evidence-generator.js +86 -49
- 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/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/ide/ide-registry.d.ts +19 -0
- package/dist/services/ide/ide-registry.js +21 -0
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/job/job-state-store.js +7 -0
- 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 +10 -3
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +124 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +23 -0
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
- package/dist/services/skills/hooks-settings-service.js +14 -4
- package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
- package/dist/services/skills/session-start-hook-constants.js +45 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +29 -11
- package/dist/services/slice/slice-review-state.js +23 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +221 -103
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +47 -33
- package/dist/services/workflow/pipeline-verify-types.d.ts +15 -6
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/workspace-claude-settings-materializer.js +78 -7
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +6 -6
- package/skills/bee/peaks-prd/SKILL.md +7 -5
- package/skills/bee/peaks-qa/SKILL.md +5 -5
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +8 -6
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
|
@@ -127,7 +127,17 @@ export function registerDispatchCommand(parent, io) {
|
|
|
127
127
|
? { maxConcurrent }
|
|
128
128
|
: {}),
|
|
129
129
|
});
|
|
130
|
-
|
|
130
|
+
// The handler's `ok` is the launch outcome (a vendor CLI that is not
|
|
131
|
+
// installed is a failure, not a footnote) — the caller must not
|
|
132
|
+
// re-wrap it as `ok()` unconditionally, which is how `ok: true` with
|
|
133
|
+
// `pid: -1` reached the orchestrator.
|
|
134
|
+
if (result.ok) {
|
|
135
|
+
printResult(io, ok(result.command, result.data, result.warnings ?? [], result.nextActions ?? []), asJson);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
printResult(io, fail(result.command, 'DISPATCH_DETACHED_SPAWN_FAILED', `Could not launch the vendor CLI for a detached dispatch; nothing was started.`, result.data, result.nextActions ?? []), asJson);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
131
141
|
}
|
|
132
142
|
catch (error) {
|
|
133
143
|
printResult(io, fail('sub-agent.dispatch', 'DISPATCH_DETACHED_ERROR', getErrorMessage(error), {
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { isUnsafePathInput } from '../../../shared/path-safety.js';
|
|
10
11
|
export async function doctorInvokeFromCode(opts) {
|
|
12
|
+
// Sid axis: `opts.sid` is the sole path segment between the runtime root and
|
|
13
|
+
// the write below, and nothing upstream validated it.
|
|
14
|
+
if (isUnsafePathInput(opts.sid)) {
|
|
15
|
+
throw new Error(`Invalid session id: ${opts.sid} (must be a single path segment)`);
|
|
16
|
+
}
|
|
11
17
|
const dir = join('.peaks', '_runtime', opts.sid, 'doctor');
|
|
12
18
|
mkdirSync(dir, { recursive: true });
|
|
13
19
|
const proposalPath = join(dir, 'proposal.md');
|
|
@@ -5,13 +5,17 @@
|
|
|
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 type { Command } from 'commander';
|
|
17
21
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
@@ -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
|
|
@@ -7,7 +7,7 @@ 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
8
|
import { readJsonObjectFile } from '../../services/ide/shared/atomic-json.js';
|
|
9
9
|
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
10
|
-
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
10
|
+
import { getAdapter, resolveIdeOptionHelp } from '../../services/ide/ide-registry.js';
|
|
11
11
|
/**
|
|
12
12
|
* This module's own directory — `<root>/src/cli/commands` in the source tree,
|
|
13
13
|
* `<root>/dist/cli/commands` in a build. Same reason as
|
|
@@ -153,7 +153,7 @@ export function registerHooksCommands(program, io) {
|
|
|
153
153
|
.description(`Install the peaks-managed gate-enforce hook entry into the adapter's settings.json. Slice #014: only the gate-enforce entry is installed; the legacy progress-start entry is no longer installed. Idempotent: re-runs are no-ops. Project scope by default.`)
|
|
154
154
|
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
155
155
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
156
|
-
.option('--ide <id>',
|
|
156
|
+
.option('--ide <id>', resolveIdeOptionHelp())
|
|
157
157
|
.option('--dry-run', 'show what would change without writing')
|
|
158
158
|
.option('--no-progress', 'skip the progress-start PreToolUse hook entry; install ONLY the gate-enforce entry')).action((options) => {
|
|
159
159
|
const scope = resolveScope(options);
|
|
@@ -282,7 +282,7 @@ export function registerHooksCommands(program, io) {
|
|
|
282
282
|
.description("Remove the peaks-managed gate-enforce hook entry from the target settings.json. Any legacy progress-start entry that a pre-#014 install left behind is also removed (sentinel-based scan). Third-party hooks are preserved.")
|
|
283
283
|
.option('--global', 'remove from the user-level ~/.claude/settings.json instead of the project')
|
|
284
284
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
285
|
-
.option('--ide <id>',
|
|
285
|
+
.option('--ide <id>', resolveIdeOptionHelp())).action((options) => {
|
|
286
286
|
const scope = resolveScope(options);
|
|
287
287
|
const projectRoot = resolveProjectRoot(scope, options.project);
|
|
288
288
|
const ide = resolveIdeForCommand(options, projectRoot);
|
|
@@ -310,7 +310,7 @@ export function registerHooksCommands(program, io) {
|
|
|
310
310
|
.description('Report which peaks-managed hook entries are installed.')
|
|
311
311
|
.option('--global', 'inspect the user-level ~/.claude/settings.json instead of the project')
|
|
312
312
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
313
|
-
.option('--ide <id>',
|
|
313
|
+
.option('--ide <id>', resolveIdeOptionHelp())).action((options) => {
|
|
314
314
|
const scope = resolveScope(options);
|
|
315
315
|
const projectRoot = resolveProjectRoot(scope, options.project);
|
|
316
316
|
const ide = resolveIdeForCommand(options, projectRoot);
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
6
6
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
7
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
7
8
|
import { JobStateStore } from '../../services/job/job-state-store.js';
|
|
8
9
|
import { JobOrchestrator } from '../../services/job/job-orchestrator.js';
|
|
9
10
|
import { writeJobProgress, readJobProgress, tryReadJobProgress } from '../../services/job/job-progress-store.js';
|
|
@@ -70,6 +71,13 @@ function resolveJobStateRoot(opts, jobId) {
|
|
|
70
71
|
if (!sessionId) {
|
|
71
72
|
throw new Error('NO_ACTIVE_SESSION: peaks job requires --session-id or an active peaks-code session via peaks workspace init');
|
|
72
73
|
}
|
|
74
|
+
// Sid axis — the sibling of the jobId guard in `JobStateStore.jobDir`. A
|
|
75
|
+
// caller-supplied `--session-id` reaches this join unmodified, so a
|
|
76
|
+
// traversal value lands `job/<id>/state.json` outside every project root
|
|
77
|
+
// while the envelope still reads `ok: true`.
|
|
78
|
+
if (isUnsafePathInput(sessionId)) {
|
|
79
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
80
|
+
}
|
|
73
81
|
const rootDir = join(project, '.peaks', '_runtime', sessionId, 'job');
|
|
74
82
|
if (jobId && !existsSync(join(rootDir, jobId, 'state.json'))) {
|
|
75
83
|
const other = findSessionHoldingJob(project, jobId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
3
4
|
import { resolveWorkflow, planWorkflow, planWorkflowRun } from '../../services/workflow/workflow-loader.js';
|
|
4
5
|
import { dispatchEvaluator } from '../../services/loop/evaluator-dispatcher.js';
|
|
5
6
|
import { DEFAULT_MONOTONIC_THRESHOLD } from '../../services/loop/monotonic-guard.js';
|
|
@@ -165,6 +166,25 @@ export function registerWorkflowEvalCommands(program, io) {
|
|
|
165
166
|
process.exitCode = 1;
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
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.
|
|
178
|
+
if (isUnsafePathInput(options.session)) {
|
|
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);
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
return;
|
|
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
|
+
}
|
|
168
188
|
const score = envelope.degraded ? 0.25 : (envelope.gateAction === 'pass' ? 1.0 : envelope.gateAction === 'warn' ? 0.5 : 0.0);
|
|
169
189
|
const dir = join(projectRoot, '.peaks', '_runtime', options.session, 'loop', rid, 'cycles');
|
|
170
190
|
const n = nextEvalCaptureIndex(projectRoot, options.session, rid);
|
|
@@ -475,6 +495,17 @@ export function registerWorkflowEvalCommands(program, io) {
|
|
|
475
495
|
* writes. The run-driver also writes to the same dir; the next
|
|
476
496
|
* index = max(prior)+1, or 1. */
|
|
477
497
|
function nextEvalCaptureIndex(projectRoot, sid, rid) {
|
|
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.
|
|
503
|
+
if (isUnsafePathInput(sid)) {
|
|
504
|
+
throw new Error(`Invalid session id: ${sid} (must be a single path segment)`);
|
|
505
|
+
}
|
|
506
|
+
if (isUnsafePathInput(rid)) {
|
|
507
|
+
throw new Error(`Invalid request id: ${rid} (must be a single path segment)`);
|
|
508
|
+
}
|
|
478
509
|
const dir = join(projectRoot, '.peaks', '_runtime', sid, 'loop', rid, 'cycles');
|
|
479
510
|
if (!existsSync(dir))
|
|
480
511
|
return 1;
|
|
@@ -46,6 +46,7 @@ export function registerPerfAuditCommands(program, io) {
|
|
|
46
46
|
.option('--project <path>', 'project root (default: cwd)')).action((options) => {
|
|
47
47
|
const projectRoot = options.project ?? process.cwd();
|
|
48
48
|
const sid = options.sid;
|
|
49
|
+
const rid = options.rid;
|
|
49
50
|
if (sid === undefined || sid.length === 0) {
|
|
50
51
|
printResult(io, fail('perf-audit.detect', 'SID_REQUIRED', 'session id (--sid) is required for perf-audit.detect', { state: 'sid-missing' }, ['Pass --sid <session-id> (e.g. 2026-06-27-session-...)', 'Run `peaks session info --active --json` to find the active sid.']), options.json);
|
|
51
52
|
process.exitCode = 1;
|
|
@@ -55,6 +56,7 @@ export function registerPerfAuditCommands(program, io) {
|
|
|
55
56
|
const detect = detectPerfAudit({
|
|
56
57
|
projectRoot,
|
|
57
58
|
sessionId: sid,
|
|
59
|
+
...(rid !== undefined ? { requestId: rid } : {}),
|
|
58
60
|
});
|
|
59
61
|
const envelope = detect.state === 'ready'
|
|
60
62
|
? ok('perf-audit.detect', detect, [...detect.warnings], [...detect.nextActions])
|
|
@@ -33,6 +33,7 @@ import { spawn } from 'node:child_process';
|
|
|
33
33
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
|
|
34
34
|
import { join, dirname, resolve } from 'node:path';
|
|
35
35
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
36
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
36
37
|
import { getErrorMessage } from '../cli-helpers.js';
|
|
37
38
|
import { runBrowserAction } from '../../services/qa/browser-wrapper-service.js';
|
|
38
39
|
export const DEFAULT_PORT = 8931;
|
|
@@ -43,6 +44,17 @@ export function playwrightSessionsDir(projectRoot) {
|
|
|
43
44
|
return join(projectRoot, '.peaks', '_runtime', PLAYWRIGHT_SESSIONS_DIR);
|
|
44
45
|
}
|
|
45
46
|
export function sessionFilePath(projectRoot, terminalId) {
|
|
47
|
+
// Terminal-id axis. The derived id is already sanitised (`deriveTerminalId`),
|
|
48
|
+
// but `--terminal` is caller-supplied and reaches this join unsanitised.
|
|
49
|
+
// Measured (repair cycle 1, item 1): `playwright stop --terminal ../../../../X`
|
|
50
|
+
// read a session record planted OUTSIDE the project root, sent SIGTERM to the
|
|
51
|
+
// pid it named, unlinked that file, and returned `ok: true` — arbitrary
|
|
52
|
+
// process termination plus an arbitrary file delete, one flag away. This is
|
|
53
|
+
// the join every reader/writer of a session record goes through, so this is
|
|
54
|
+
// where the invariant lives.
|
|
55
|
+
if (isUnsafePathInput(terminalId)) {
|
|
56
|
+
throw new Error(`Invalid terminal id: ${terminalId} (must be a single path segment)`);
|
|
57
|
+
}
|
|
46
58
|
return join(playwrightSessionsDir(projectRoot), `${terminalId}.json`);
|
|
47
59
|
}
|
|
48
60
|
/**
|
|
@@ -44,7 +44,7 @@ export function registerPrdCommands(program, io) {
|
|
|
44
44
|
// peaks prd handoff init
|
|
45
45
|
addJsonOption(handoff
|
|
46
46
|
.command('init')
|
|
47
|
-
.description('Initialize an immutable handoff: sha256(body) → frontmatter → write to .peaks/_runtime/<sid>/prd/handoff
|
|
47
|
+
.description('Initialize an immutable handoff: sha256(body) → frontmatter → write to .peaks/_runtime/<sid>/prd/handoff-<rid>.md')
|
|
48
48
|
.requiredOption('--rid <request-id>', 'request id (e.g. 001-v2-11-cc-group-b)')
|
|
49
49
|
.requiredOption('--sid <session-id>', 'session id (e.g. 2026-06-26-session-a28d69)')
|
|
50
50
|
.requiredOption('--body <body>', 'handoff body markdown, or @<file> to read from disk')
|
|
@@ -29,6 +29,7 @@ import { join, resolve } from 'node:path';
|
|
|
29
29
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
30
30
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
31
31
|
import { archiveScreenshots } from '../../services/qa/screenshot-archive-service.js';
|
|
32
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
32
33
|
import { getSessionId } from '../../services/session/session-manager.js';
|
|
33
34
|
import { BrowserRestartDetector } from '../../services/qa/browser-restart-detector.js';
|
|
34
35
|
import { BrowserEventLogger } from '../../services/qa/browser-event-logger.js';
|
|
@@ -68,6 +69,12 @@ async function ensureContextForQa(goal, project, sid) {
|
|
|
68
69
|
}
|
|
69
70
|
export const DEFAULT_MAX_BROWSER_RESTARTS = 3;
|
|
70
71
|
export function runQaSlice(input) {
|
|
72
|
+
// Sid axis: this is the join for `qa/browser-events.jsonl`. The `qa gate run`
|
|
73
|
+
// action guards before calling in, but the function is exported for tests and
|
|
74
|
+
// states its own contract at the join.
|
|
75
|
+
if (isUnsafePathInput(input.sessionId)) {
|
|
76
|
+
throw new Error(`Invalid session id: ${input.sessionId} (must be a single path segment)`);
|
|
77
|
+
}
|
|
71
78
|
const detector = new BrowserRestartDetector({
|
|
72
79
|
maxRestarts: input.maxRestarts,
|
|
73
80
|
enabled: input.detectorEnabled
|
|
@@ -221,6 +228,15 @@ export function registerQaCommands(program, io) {
|
|
|
221
228
|
// when the user did not supply one.
|
|
222
229
|
const qaProject = options.project;
|
|
223
230
|
const qaSid = options.sessionId ?? 'ad-hoc';
|
|
231
|
+
// Sid axis. `--session-id` reaches three consumers below — the context
|
|
232
|
+
// pre-build, `loadMutReport`, and `runQaSlice`'s
|
|
233
|
+
// `qa/browser-events.jsonl` join — so one guard at resolution covers all
|
|
234
|
+
// three. (`peaks qa archive-screenshots` has its own, separate join.)
|
|
235
|
+
if (isUnsafePathInput(qaSid)) {
|
|
236
|
+
printResult(io, fail('qa.gate', 'INVALID_SESSION_ID', `Invalid session id: ${qaSid} (must be a single path segment)`, { provided: qaSid }, ['Pass a session id that is a single path segment']), options.json);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
224
240
|
await ensureContextForQa('qa gate run', qaProject, qaSid);
|
|
225
241
|
// Plan 2 / Task 8 — load peaks-mut's report (MUT.sig) if present.
|
|
226
242
|
// Returns null when peaks-mut was not run; the gate treats that
|
|
@@ -289,6 +305,12 @@ export function registerQaCommands(program, io) {
|
|
|
289
305
|
try {
|
|
290
306
|
const projectRoot = resolve(options.project);
|
|
291
307
|
const sid = options.sessionId?.trim() || getSessionId(projectRoot) || 'ad-hoc';
|
|
308
|
+
// Sid axis. Guarded after resolution so the flag value and the session
|
|
309
|
+
// binding are both covered. `archiveScreenshots` cannot do this itself:
|
|
310
|
+
// it only ever sees a fully-resolved `targetDir`.
|
|
311
|
+
if (isUnsafePathInput(sid)) {
|
|
312
|
+
throw new Error(`Invalid session id: ${sid} (must be a single path segment)`);
|
|
313
|
+
}
|
|
292
314
|
const targetDir = join(projectRoot, '.peaks', '_runtime', sid, 'qa', 'screenshots');
|
|
293
315
|
const envelope = archiveScreenshots({ sourceDir: options.source, targetDir });
|
|
294
316
|
printResult(io, ok('qa.archive-screenshots', envelope, [], envelope.moved.length === 0
|
|
@@ -10,6 +10,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
10
10
|
import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
|
|
11
11
|
import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
|
|
12
12
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
13
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
13
14
|
/**
|
|
14
15
|
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
15
16
|
* `request list`. `count` is the true total; `names` carries
|
|
@@ -189,6 +190,13 @@ export function registerRequestCommands(program, io) {
|
|
|
189
190
|
// non-existent .peaks/default/ dir and crashes with ENOENT, so when
|
|
190
191
|
// --session-id is omitted we look the artifact up to find its session.
|
|
191
192
|
let resolvedSessionId = options.sessionId;
|
|
193
|
+
// Sid axis: either arm of `resolvedSessionId` can be a caller-supplied
|
|
194
|
+
// `--session-id`, and it is joined into the bypass-counter root below.
|
|
195
|
+
if (resolvedSessionId !== undefined && isUnsafePathInput(resolvedSessionId)) {
|
|
196
|
+
printResult(io, fail('request.transition', 'INVALID_SESSION_ID', `Invalid session id: ${resolvedSessionId} (must be a single path segment)`, { provided: resolvedSessionId }, ['Pass a session id that is a single path segment']), options.json);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
192
200
|
if (resolvedSessionId === undefined) {
|
|
193
201
|
const { showRequestArtifact: showForSession } = await import('../../services/artifacts/request-artifact-service.js');
|
|
194
202
|
const located = await showForSession({ projectRoot: options.project, role, requestId });
|
|
@@ -408,7 +408,7 @@ export function registerScanCommands(program, io) {
|
|
|
408
408
|
});
|
|
409
409
|
const nextActions = [];
|
|
410
410
|
if (report.gateAction === 'block') {
|
|
411
|
-
nextActions.push('Karpathy review file missing under scope=all. Per karpathy §1 Think Before Coding,
|
|
411
|
+
nextActions.push('Karpathy review file missing under scope=all. Per karpathy §1 Think Before Coding, the rd:qa-handoff gate wants rd/karpathy-review-<rid>.md; this scanner has no rid and reads only the back-compat name rd/karpathy-review.md, so it cannot go green on the rid-scoped name alone. `peaks request transition --state qa-handoff` is the authoritative gate.');
|
|
412
412
|
}
|
|
413
413
|
else if (report.gateAction === 'warn') {
|
|
414
414
|
nextActions.push(`Karpathy review emitted ${report.totalViolations} violation(s). Review the warnings and re-run after cleanup (karpathy §3 Surgical Changes).`);
|
|
@@ -46,6 +46,7 @@ export function registerSecurityAuditCommands(program, io) {
|
|
|
46
46
|
.option('--project <path>', 'project root (default: cwd)')).action((options) => {
|
|
47
47
|
const projectRoot = options.project ?? process.cwd();
|
|
48
48
|
const sid = options.sid;
|
|
49
|
+
const rid = options.rid;
|
|
49
50
|
if (sid === undefined || sid.length === 0) {
|
|
50
51
|
printResult(io, fail('security-audit.detect', 'SID_REQUIRED', 'session id (--sid) is required for security-audit.detect', { state: 'sid-missing' }, ['Pass --sid <session-id> (e.g. 2026-06-27-session-...)', 'Run `peaks session info --active --json` to find the active sid.']), options.json);
|
|
51
52
|
process.exitCode = 1;
|
|
@@ -55,6 +56,7 @@ export function registerSecurityAuditCommands(program, io) {
|
|
|
55
56
|
const detect = detectSecurityAudit({
|
|
56
57
|
projectRoot,
|
|
57
58
|
sessionId: sid,
|
|
59
|
+
...(rid !== undefined ? { requestId: rid } : {}),
|
|
58
60
|
});
|
|
59
61
|
const envelope = detect.state === 'ready'
|
|
60
62
|
? ok('security-audit.detect', detect, [...detect.warnings], [...detect.nextActions])
|
|
@@ -9,15 +9,37 @@
|
|
|
9
9
|
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
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
13
|
+
import { SLICE_ID_PATTERN } from '../../services/sc/sc-service.js';
|
|
12
14
|
import { integrateSlices } from '../../services/slice/slice-integration.js';
|
|
13
15
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
14
16
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
15
17
|
function loadContractsForSlices(projectRoot, sessionId, sliceIds) {
|
|
18
|
+
// Sid axis: `--session-id` reaches this join unmodified.
|
|
19
|
+
if (isUnsafePathInput(sessionId)) {
|
|
20
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
21
|
+
}
|
|
16
22
|
const dir = resolve(projectRoot, '.peaks', '_runtime', sessionId, 'dispatch', 'contracts');
|
|
17
23
|
if (!existsSync(dir))
|
|
18
24
|
return [];
|
|
19
25
|
const result = [];
|
|
20
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
|
+
}
|
|
21
43
|
const file = join(dir, `${sliceId}.json`);
|
|
22
44
|
if (!existsSync(file))
|
|
23
45
|
continue;
|
|
@@ -6,11 +6,39 @@ import { renderStatusLine, resolveStatusLineCapability, } from '../../services/s
|
|
|
6
6
|
import { applyStatusLineInstall, planStatusLineInstall, removeStatusLineInstall } from '../../services/skills/statusline-settings-service.js';
|
|
7
7
|
import { readHookStatus as readSettingsStatus } from '../../services/skills/hooks-settings-service.js';
|
|
8
8
|
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
9
|
+
import { resolveIdeOptionHelp } from '../../services/ide/ide-registry.js';
|
|
9
10
|
import { decideCompactStatusline, renderCompactStatusline, } from '../../services/compact-statusline/compact-statusline-service.js';
|
|
10
11
|
import { getSessionIdCanonical } from '../../services/session/session-manager.js';
|
|
12
|
+
import { writeHarnessWitness } from '../../services/context/harness-context-witness.js';
|
|
11
13
|
const STDIN_READ_TIMEOUT_MS = 250;
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Read piped stdin if present; resolve quickly with '' when attached to a TTY.
|
|
16
|
+
*
|
|
17
|
+
* `PEAKS_STATUSLINE_STDIN` is a test seam (same shape as the `PEAKS_HOOK_STDIN`
|
|
18
|
+
* seam in `gate-commands.ts`): the harness payload is the only channel that
|
|
19
|
+
* carries the harness's own context numbers, and a test that cannot supply one
|
|
20
|
+
* cannot verify the capture at all.
|
|
21
|
+
*
|
|
22
|
+
* WHY THIS SEAM IS ACCEPTABLE ON A DIFFERENT ARGUMENT (repair cycle 3). The
|
|
23
|
+
* precedent does not transfer by itself: `PEAKS_HOOK_STDIN` is defended in
|
|
24
|
+
* `.peaks/memory/peaks-hook-stdin-test-seam-pattern.md` because its payload
|
|
25
|
+
* still routes through the `enforceBashCommand` SOP gate. This payload is
|
|
26
|
+
* routed through no gate — it is PERSISTED verbatim as the record the witness
|
|
27
|
+
* guard then reads. So the compensating control is different, not absent: the
|
|
28
|
+
* record is observation-only and one-way. It reaches only `data` and the
|
|
29
|
+
* `warnings` / `nextActions` arrays of `peaks code context-now`; the compact
|
|
30
|
+
* `action` there is computed from `probe.ratio` alone, before and independently
|
|
31
|
+
* of the witness. A forged payload can therefore make the instrument lie —
|
|
32
|
+
* measured: a hand-written record yields a confident false `agree`, or a
|
|
33
|
+
* self-contradictory false `disagree` — and can do nothing else. What it cannot
|
|
34
|
+
* do is fire, block or delay a compact. Unconditional rather than NODE_ENV-
|
|
35
|
+
* gated, for the reason already recorded for the sibling seam.
|
|
36
|
+
*/
|
|
13
37
|
function readStdin() {
|
|
38
|
+
const override = process.env['PEAKS_STATUSLINE_STDIN'];
|
|
39
|
+
if (override !== undefined) {
|
|
40
|
+
return Promise.resolve(override);
|
|
41
|
+
}
|
|
14
42
|
return new Promise((resolve) => {
|
|
15
43
|
if (process.stdin.isTTY) {
|
|
16
44
|
resolve('');
|
|
@@ -82,6 +110,18 @@ export async function runDefaultStatuslineRender(options, io) {
|
|
|
82
110
|
// subprocess sat descheduled. Production callers omit the flag.
|
|
83
111
|
const now = options.now !== undefined ? Number(options.now) : Date.now();
|
|
84
112
|
const model = buildStatusLineModel(seeded, now);
|
|
113
|
+
// Slice 2026-09-13-statusline-window-witness (AC1): capture the harness's OWN
|
|
114
|
+
// context numbers from the payload it just piped in. The project root and the
|
|
115
|
+
// canonical session id are taken from the model that was JUST built, so the
|
|
116
|
+
// witness lands under the same session this render already resolved — a second
|
|
117
|
+
// resolution here could disagree with it. Never throws; never changes the
|
|
118
|
+
// rendered line.
|
|
119
|
+
writeHarnessWitness({
|
|
120
|
+
projectRoot: model.projectRoot,
|
|
121
|
+
sessionId: model.sessionId,
|
|
122
|
+
stdin,
|
|
123
|
+
nowMs: now,
|
|
124
|
+
});
|
|
85
125
|
const capability = resolveStatusLineCapability({
|
|
86
126
|
env: process.env,
|
|
87
127
|
isTTY: Boolean(process.stdout.isTTY),
|
|
@@ -136,7 +176,7 @@ export function registerStatusLineCommands(program, io) {
|
|
|
136
176
|
.description("Install the Peaks status line into the adapter's settings.json (project scope by default).")
|
|
137
177
|
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
138
178
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
139
|
-
.option('--ide <id>',
|
|
179
|
+
.option('--ide <id>', resolveIdeOptionHelp())
|
|
140
180
|
.option('--force', 'overwrite an existing non-Peaks statusLine entry')
|
|
141
181
|
.option('--dry-run', 'show what would change without writing')).action((options) => {
|
|
142
182
|
const scope = resolveScope(options);
|
|
@@ -173,7 +213,7 @@ export function registerStatusLineCommands(program, io) {
|
|
|
173
213
|
.description("Remove the Peaks status line from the adapter's settings.json.")
|
|
174
214
|
.option('--global', 'remove from the user-level ~/.claude/settings.json instead of the project')
|
|
175
215
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
176
|
-
.option('--ide <id>',
|
|
216
|
+
.option('--ide <id>', resolveIdeOptionHelp())).action((options) => {
|
|
177
217
|
const scope = resolveScope(options);
|
|
178
218
|
const projectRoot = scope === 'project'
|
|
179
219
|
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|
|
@@ -194,7 +234,7 @@ export function registerStatusLineCommands(program, io) {
|
|
|
194
234
|
.description('Report whether the Peaks status line is installed in the adapter settings.json.')
|
|
195
235
|
.option('--global', 'inspect the user-level ~/.claude/settings.json instead of the project')
|
|
196
236
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
197
|
-
.option('--ide <id>',
|
|
237
|
+
.option('--ide <id>', resolveIdeOptionHelp())).action((options) => {
|
|
198
238
|
const scope = resolveScope(options);
|
|
199
239
|
const projectRoot = scope === 'project'
|
|
200
240
|
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|