peaks-loop 4.0.46 → 4.0.48
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 +54 -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.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +139 -16
- package/dist/cli/commands/compact-command.js +241 -1
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- 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/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +14 -5
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/loop-eval-commands.js +15 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +14 -1
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +14 -3
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/slice-integrate-commands.js +5 -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/test-commands.js +2 -1
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +10 -3
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +130 -65
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +18 -8
- 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-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/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +130 -1
- package/dist/services/code/auto-compact-lifecycle.js +180 -4
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +166 -34
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +75 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- 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/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/evidence/evidence-generator.js +86 -49
- 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/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +31 -0
- package/dist/services/ide/ide-registry.js +35 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/best-practice-auto-trigger.js +1 -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 +81 -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 +9 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +14 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +99 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +91 -14
- package/dist/services/skills/session-start-hook-constants.d.ts +86 -0
- package/dist/services/skills/session-start-hook-constants.js +86 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +31 -12
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/slice/slice-review-state.js +8 -0
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
- package/dist/services/workflow/pipeline-verify-service.js +24 -23
- package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- 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/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +124 -9
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +6 -6
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +8 -6
- package/skills/bee/peaks-qa/SKILL.md +7 -7
- 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 +10 -8
- 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/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +3 -3
- 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/sub-agent-dispatch.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
|
@@ -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,14 @@ export function registerWorkflowEvalCommands(program, io) {
|
|
|
165
166
|
process.exitCode = 1;
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
169
|
+
// Sid axis. `--session` reaches two joins below (`dir` and, through
|
|
170
|
+
// `nextEvalCaptureIndex`, the same literal), so one guard here covers
|
|
171
|
+
// the whole `--capture-score` write path.
|
|
172
|
+
if (isUnsafePathInput(options.session)) {
|
|
173
|
+
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
|
+
process.exitCode = 1;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
168
177
|
const score = envelope.degraded ? 0.25 : (envelope.gateAction === 'pass' ? 1.0 : envelope.gateAction === 'warn' ? 0.5 : 0.0);
|
|
169
178
|
const dir = join(projectRoot, '.peaks', '_runtime', options.session, 'loop', rid, 'cycles');
|
|
170
179
|
const n = nextEvalCaptureIndex(projectRoot, options.session, rid);
|
|
@@ -475,6 +484,12 @@ export function registerWorkflowEvalCommands(program, io) {
|
|
|
475
484
|
* writes. The run-driver also writes to the same dir; the next
|
|
476
485
|
* index = max(prior)+1, or 1. */
|
|
477
486
|
function nextEvalCaptureIndex(projectRoot, sid, rid) {
|
|
487
|
+
// Sid axis: this function is the join for the capture-score cycle dir. The
|
|
488
|
+
// action guards `options.session` before calling in, but the seam is callable
|
|
489
|
+
// on its own, so the join states its own contract.
|
|
490
|
+
if (isUnsafePathInput(sid)) {
|
|
491
|
+
throw new Error(`Invalid session id: ${sid} (must be a single path segment)`);
|
|
492
|
+
}
|
|
478
493
|
const dir = join(projectRoot, '.peaks', '_runtime', sid, 'loop', rid, 'cycles');
|
|
479
494
|
if (!existsSync(dir))
|
|
480
495
|
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
|
/**
|
|
@@ -144,7 +156,8 @@ export function spawnPlaywrightMcp(port, browser, userDataDir, projectRoot) {
|
|
|
144
156
|
cwd: projectRoot,
|
|
145
157
|
env: process.env,
|
|
146
158
|
stdio: 'ignore',
|
|
147
|
-
detached: true
|
|
159
|
+
detached: true,
|
|
160
|
+
windowsHide: true
|
|
148
161
|
});
|
|
149
162
|
return { pid: child.pid, child };
|
|
150
163
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rid `2026-09-13-a2-post-compact-reinject` —
|
|
3
|
+
* `peaks session reinject --project <path>`.
|
|
4
|
+
*
|
|
5
|
+
* The transport for `buildPostCompactReinjectionCard`. A Claude Code
|
|
6
|
+
* `SessionStart` hook (installed by `peaks hooks install`, matcher `compact`)
|
|
7
|
+
* runs this and Claude Code adds its stdout to the context — which is the
|
|
8
|
+
* whole mechanism by which the engineering state comes back after a
|
|
9
|
+
* compaction. See `src/services/context/post-compact-reinjection.ts` for what
|
|
10
|
+
* the card says and why it has a byte budget.
|
|
11
|
+
*
|
|
12
|
+
* THE ONE THING THIS FILE HAS TO GET RIGHT
|
|
13
|
+
*
|
|
14
|
+
* stdout IS context here. Every other peaks command treats stdout as a
|
|
15
|
+
* channel for a human or a JSON consumer; this one's stdout is pasted in
|
|
16
|
+
* front of the model as though it were something peaks-loop meant to say.
|
|
17
|
+
* Two consequences, and both are the reason the code below looks the way it
|
|
18
|
+
* does:
|
|
19
|
+
*
|
|
20
|
+
* 1. The card is printed VERBATIM — no `printResult`, no envelope, no
|
|
21
|
+
* `next:` lines, no JSON on the card path. Anything else would be
|
|
22
|
+
* injected too, and a `{"ok":true,...}` blob in the context is noise the
|
|
23
|
+
* model then has to interpret.
|
|
24
|
+
* 2. A FAILURE PRINTS NOTHING. Not an error, not a warning, not a hint. An
|
|
25
|
+
* error message on stdout would be injected into the context as a fact,
|
|
26
|
+
* and a model that reads `REINJECT_FAILED: project root must be
|
|
27
|
+
* canonical` in its own context has been handed a false problem to solve
|
|
28
|
+
* mid-slice. So every failure path exits 0 with empty stdout, and the
|
|
29
|
+
* reason is reported only through `--json` (which the hook never passes).
|
|
30
|
+
* This is `failure-soft` for a SessionStart hook: the session must start
|
|
31
|
+
* whether or not there is any state to re-inject.
|
|
32
|
+
*
|
|
33
|
+
* `--json` exists for the human/diagnostic path — `peaks session reinject
|
|
34
|
+
* --json` is how you inspect what the hook would inject without reading a
|
|
35
|
+
* transcript. It is deliberately NOT what the hook runs.
|
|
36
|
+
*/
|
|
37
|
+
import type { Command } from 'commander';
|
|
38
|
+
import type { ProgramIO } from '../cli-helpers.js';
|
|
39
|
+
import { buildPostCompactReinjectionCard } from '../../services/context/post-compact-reinjection.js';
|
|
40
|
+
export type ReinjectOptions = {
|
|
41
|
+
project: string;
|
|
42
|
+
json?: boolean;
|
|
43
|
+
budgetBytes?: string;
|
|
44
|
+
};
|
|
45
|
+
/** Why no card was produced. Reported through `--json` only. */
|
|
46
|
+
export type ReinjectSkipReason = 'empty-project' | 'invalid-project-root' | 'unreadable-project-root';
|
|
47
|
+
export type ReinjectResult = {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
readonly card: ReturnType<typeof buildPostCompactReinjectionCard>;
|
|
50
|
+
} | {
|
|
51
|
+
readonly ok: false;
|
|
52
|
+
readonly reason: ReinjectSkipReason;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Pure-ish action body, exported so tests can drive it with inputs Commander
|
|
57
|
+
* would mangle (trailing NUL bytes, whitespace-only paths). Never throws and
|
|
58
|
+
* never writes to stdout itself — `runReinjectAction` is the IO edge.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildReinjectResult(opts: ReinjectOptions): ReinjectResult;
|
|
61
|
+
/**
|
|
62
|
+
* The IO edge. Returns the process exit code so callers (CLI path or test
|
|
63
|
+
* path) apply it uniformly. NEVER calls `process.exit`.
|
|
64
|
+
*
|
|
65
|
+
* Both branches return 0 on the failure path — see this file's header. The
|
|
66
|
+
* only way a caller sees `exitCode: 1` is a bug in the `--json` serializer
|
|
67
|
+
* itself, and even that is caught.
|
|
68
|
+
*/
|
|
69
|
+
export declare function runReinjectAction(opts: ReinjectOptions, io: ProgramIO): {
|
|
70
|
+
exitCode: 0 | 1;
|
|
71
|
+
};
|
|
72
|
+
export declare function registerReinjectCommand(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rid `2026-09-13-a2-post-compact-reinject` —
|
|
3
|
+
* `peaks session reinject --project <path>`.
|
|
4
|
+
*
|
|
5
|
+
* The transport for `buildPostCompactReinjectionCard`. A Claude Code
|
|
6
|
+
* `SessionStart` hook (installed by `peaks hooks install`, matcher `compact`)
|
|
7
|
+
* runs this and Claude Code adds its stdout to the context — which is the
|
|
8
|
+
* whole mechanism by which the engineering state comes back after a
|
|
9
|
+
* compaction. See `src/services/context/post-compact-reinjection.ts` for what
|
|
10
|
+
* the card says and why it has a byte budget.
|
|
11
|
+
*
|
|
12
|
+
* THE ONE THING THIS FILE HAS TO GET RIGHT
|
|
13
|
+
*
|
|
14
|
+
* stdout IS context here. Every other peaks command treats stdout as a
|
|
15
|
+
* channel for a human or a JSON consumer; this one's stdout is pasted in
|
|
16
|
+
* front of the model as though it were something peaks-loop meant to say.
|
|
17
|
+
* Two consequences, and both are the reason the code below looks the way it
|
|
18
|
+
* does:
|
|
19
|
+
*
|
|
20
|
+
* 1. The card is printed VERBATIM — no `printResult`, no envelope, no
|
|
21
|
+
* `next:` lines, no JSON on the card path. Anything else would be
|
|
22
|
+
* injected too, and a `{"ok":true,...}` blob in the context is noise the
|
|
23
|
+
* model then has to interpret.
|
|
24
|
+
* 2. A FAILURE PRINTS NOTHING. Not an error, not a warning, not a hint. An
|
|
25
|
+
* error message on stdout would be injected into the context as a fact,
|
|
26
|
+
* and a model that reads `REINJECT_FAILED: project root must be
|
|
27
|
+
* canonical` in its own context has been handed a false problem to solve
|
|
28
|
+
* mid-slice. So every failure path exits 0 with empty stdout, and the
|
|
29
|
+
* reason is reported only through `--json` (which the hook never passes).
|
|
30
|
+
* This is `failure-soft` for a SessionStart hook: the session must start
|
|
31
|
+
* whether or not there is any state to re-inject.
|
|
32
|
+
*
|
|
33
|
+
* `--json` exists for the human/diagnostic path — `peaks session reinject
|
|
34
|
+
* --json` is how you inspect what the hook would inject without reading a
|
|
35
|
+
* transcript. It is deliberately NOT what the hook runs.
|
|
36
|
+
*/
|
|
37
|
+
import { printResult } from '../cli-helpers.js';
|
|
38
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
39
|
+
import { resolveCanonicalProjectRootStrict, InvalidProjectRootError } from '../../services/config/config-safety.js';
|
|
40
|
+
import { POST_COMPACT_REINJECTION_BYTE_BUDGET, buildPostCompactReinjectionCard } from '../../services/context/post-compact-reinjection.js';
|
|
41
|
+
/**
|
|
42
|
+
* Parse `--budget-bytes`. Diagnostic only — the production ceiling is the
|
|
43
|
+
* constant. A value that is not a non-negative integer is REFUSED rather than
|
|
44
|
+
* coerced, because a coerced budget would silently change what the card
|
|
45
|
+
* proves (`--budget-bytes 4KiB` becoming `NaN` and then the default would
|
|
46
|
+
* report a full card while the caller believed they had constrained it).
|
|
47
|
+
*/
|
|
48
|
+
function parseBudget(raw) {
|
|
49
|
+
if (raw === undefined)
|
|
50
|
+
return { ok: true, value: null };
|
|
51
|
+
const parsed = Number(raw);
|
|
52
|
+
if (!Number.isInteger(parsed) || parsed < 0)
|
|
53
|
+
return { ok: false };
|
|
54
|
+
return { ok: true, value: parsed };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pure-ish action body, exported so tests can drive it with inputs Commander
|
|
58
|
+
* would mangle (trailing NUL bytes, whitespace-only paths). Never throws and
|
|
59
|
+
* never writes to stdout itself — `runReinjectAction` is the IO edge.
|
|
60
|
+
*/
|
|
61
|
+
export function buildReinjectResult(opts) {
|
|
62
|
+
if (!opts.project || opts.project.trim() === '') {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: 'empty-project',
|
|
66
|
+
message: '--project <path> is required and must be non-empty'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let projectRoot;
|
|
70
|
+
try {
|
|
71
|
+
projectRoot = resolveCanonicalProjectRootStrict(opts.project);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof InvalidProjectRootError) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
reason: 'invalid-project-root',
|
|
78
|
+
message: `--project is not a usable project root (${error.reason})`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
reason: 'unreadable-project-root',
|
|
84
|
+
message: error instanceof Error ? error.message : String(error)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const budget = parseBudget(opts.budgetBytes);
|
|
88
|
+
if (!budget.ok) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: 'invalid-project-root',
|
|
92
|
+
message: `--budget-bytes must be a non-negative integer, got ${String(opts.budgetBytes)}`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const card = buildPostCompactReinjectionCard({
|
|
96
|
+
projectRoot,
|
|
97
|
+
...(budget.value !== null ? { budgetBytes: budget.value } : {})
|
|
98
|
+
});
|
|
99
|
+
return { ok: true, card };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The IO edge. Returns the process exit code so callers (CLI path or test
|
|
103
|
+
* path) apply it uniformly. NEVER calls `process.exit`.
|
|
104
|
+
*
|
|
105
|
+
* Both branches return 0 on the failure path — see this file's header. The
|
|
106
|
+
* only way a caller sees `exitCode: 1` is a bug in the `--json` serializer
|
|
107
|
+
* itself, and even that is caught.
|
|
108
|
+
*/
|
|
109
|
+
export function runReinjectAction(opts, io) {
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = buildReinjectResult(opts);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
result = {
|
|
116
|
+
ok: false,
|
|
117
|
+
reason: 'unreadable-project-root',
|
|
118
|
+
message: error instanceof Error ? error.message : String(error)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (opts.json === true) {
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
printResult(io, fail('session.reinject', `REINJECT_SKIPPED_${result.reason.replace(/-/g, '_').toUpperCase()}`, result.message, {
|
|
124
|
+
project: opts.project
|
|
125
|
+
}, []), true);
|
|
126
|
+
// A skipped card is a legitimate answer, not a failed command: the hook
|
|
127
|
+
// that runs this must never report a non-zero exit to the harness.
|
|
128
|
+
return { exitCode: 0 };
|
|
129
|
+
}
|
|
130
|
+
const card = result.card;
|
|
131
|
+
printResult(io, ok('session.reinject', {
|
|
132
|
+
sessionId: card.sessionId,
|
|
133
|
+
budgetBytes: card.budgetBytes,
|
|
134
|
+
bytes: card.bytes,
|
|
135
|
+
emittedRanks: card.emittedRanks,
|
|
136
|
+
droppedRanks: card.droppedRanks,
|
|
137
|
+
unresolved: card.unresolved,
|
|
138
|
+
// The payload itself, so a caller can read it without re-running.
|
|
139
|
+
card: card.text
|
|
140
|
+
}), true);
|
|
141
|
+
return { exitCode: 0 };
|
|
142
|
+
}
|
|
143
|
+
// The card path — the one the hook uses. Verbatim, and nothing on failure.
|
|
144
|
+
if (!result.ok)
|
|
145
|
+
return { exitCode: 0 };
|
|
146
|
+
if (result.card.text.length > 0)
|
|
147
|
+
io.stdout(result.card.text);
|
|
148
|
+
return { exitCode: 0 };
|
|
149
|
+
}
|
|
150
|
+
export function registerReinjectCommand(program, io) {
|
|
151
|
+
// Reuse the `session` commander group registered by the auto-registration
|
|
152
|
+
// pass, exactly as `registerPrimerCommand` does — `program.command('session
|
|
153
|
+
// reinject')` would register a single literal command name instead of a
|
|
154
|
+
// child of the group, and creating a second `session` group throws.
|
|
155
|
+
const session = program.commands.find((c) => c.name() === 'session');
|
|
156
|
+
if (!session) {
|
|
157
|
+
throw new Error('registerReinjectCommand: session group not registered; ' +
|
|
158
|
+
'autoRegisterAllCommands must run before registerReinjectCommand');
|
|
159
|
+
}
|
|
160
|
+
session
|
|
161
|
+
.command('reinject')
|
|
162
|
+
.description('Print the post-compact engineering-state card to stdout. ' +
|
|
163
|
+
'Run by the SessionStart hook (matcher: compact) so Claude Code re-injects it ' +
|
|
164
|
+
'as context after a compaction. Read-only; prints nothing (exit 0) when there is ' +
|
|
165
|
+
'no state to re-inject.')
|
|
166
|
+
.requiredOption('--project <path>', 'Project root (must be a non-empty canonical path)')
|
|
167
|
+
.option('--budget-bytes <n>', `Diagnostic override for the card's byte ceiling (default ${POST_COMPACT_REINJECTION_BYTE_BUDGET})`)
|
|
168
|
+
.option('--json', 'emit a JSON envelope { ok, data } to stdout instead of the card')
|
|
169
|
+
.action((opts) => {
|
|
170
|
+
const { exitCode } = runReinjectAction(opts, io);
|
|
171
|
+
if (exitCode !== 0)
|
|
172
|
+
process.exitCode = exitCode;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -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 });
|
|
@@ -277,9 +285,12 @@ export function registerRequestCommands(program, io) {
|
|
|
277
285
|
// 0.85–0.95 pre-compact zone, write a checkpoint BEFORE the
|
|
278
286
|
// transition completes and attach `preCompactCheckpoint: true` to
|
|
279
287
|
// the response envelope. The LLM remains the decision-maker; this
|
|
280
|
-
// hook only surfaces the signal.
|
|
281
|
-
//
|
|
282
|
-
// refuse
|
|
288
|
+
// hook only surfaces the signal. At ratio ≥ 0.95 the auto-compact
|
|
289
|
+
// orchestrator ASKS the harness to compact and reports that it is
|
|
290
|
+
// waiting; it does not refuse anything, because peaks-loop has no
|
|
291
|
+
// executor for a running session and a refusal it cannot enforce only
|
|
292
|
+
// deadlocked the runner (slice
|
|
293
|
+
// 2026-09-13-auto-compact-trigger-ownership).
|
|
283
294
|
let preCompact = null;
|
|
284
295
|
// 2026-09-03-codegraph-autorefresh: auto codegraph re-index on the
|
|
285
296
|
// RD → QA slice-complete boundary. Set only for rd:qa-handoff; null
|
|
@@ -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])
|
|
@@ -46,6 +46,7 @@ export function runShadcnInit(options) {
|
|
|
46
46
|
const result = spawnSync(invocation.command, [...invocation.args], {
|
|
47
47
|
cwd: options.cwd,
|
|
48
48
|
encoding: 'utf8',
|
|
49
|
+
windowsHide: true,
|
|
49
50
|
timeout: options.timeoutMs ?? 300_000,
|
|
50
51
|
maxBuffer: 32 * 1024 * 1024,
|
|
51
52
|
env: invocation.baseEnv
|
|
@@ -9,10 +9,15 @@
|
|
|
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';
|
|
12
13
|
import { integrateSlices } from '../../services/slice/slice-integration.js';
|
|
13
14
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
14
15
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
15
16
|
function loadContractsForSlices(projectRoot, sessionId, sliceIds) {
|
|
17
|
+
// Sid axis: `--session-id` reaches this join unmodified.
|
|
18
|
+
if (isUnsafePathInput(sessionId)) {
|
|
19
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
20
|
+
}
|
|
16
21
|
const dir = resolve(projectRoot, '.peaks', '_runtime', sessionId, 'dispatch', 'contracts');
|
|
17
22
|
if (!existsSync(dir))
|
|
18
23
|
return [];
|
|
@@ -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())
|
|
@@ -15,14 +15,27 @@ export declare function dispatch(f: DispatchFlags): Promise<{
|
|
|
15
15
|
ok: boolean;
|
|
16
16
|
command: string;
|
|
17
17
|
data: {
|
|
18
|
+
expectedCompletionSeconds: number;
|
|
19
|
+
orchestratorVisibleHint: string;
|
|
18
20
|
mode: string;
|
|
19
21
|
vendor: "claude" | "codex" | "copilot" | undefined;
|
|
20
22
|
pid: number;
|
|
21
23
|
dispatchRecordPath: string;
|
|
22
24
|
maxConcurrent: number;
|
|
23
25
|
noThrottle: boolean;
|
|
24
|
-
|
|
26
|
+
} | {
|
|
25
27
|
expectedCompletionSeconds: number;
|
|
28
|
+
spawnError: {
|
|
29
|
+
code: string | undefined;
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
orchestratorVisibleHint: string;
|
|
33
|
+
mode: string;
|
|
34
|
+
vendor: "claude" | "codex" | "copilot" | undefined;
|
|
35
|
+
pid: number;
|
|
36
|
+
dispatchRecordPath: string;
|
|
37
|
+
maxConcurrent: number;
|
|
38
|
+
noThrottle: boolean;
|
|
26
39
|
};
|
|
27
40
|
warnings: string[];
|
|
28
41
|
nextActions: string[];
|