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
|
@@ -75,20 +75,101 @@ export class FileSizeViolationError extends Error {
|
|
|
75
75
|
this.threshold = threshold;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The `- state:` line the artifact templates and `request transition` write.
|
|
80
|
+
* Anchored on both ends so a prose mention (`state: qa-block`, without the
|
|
81
|
+
* leading dash) is not read as the field, and anchored at column 0 because
|
|
82
|
+
* every writer of this field (`updateStatusBlock`, `request init`) emits it
|
|
83
|
+
* as a top-level line — an indented match is a nested list item, not the
|
|
84
|
+
* field. `locateArtifactState` applies it only outside fenced code regions.
|
|
85
|
+
*/
|
|
86
|
+
const STATE_LINE_RE = /^-\s*state:\s*(.+?)\s*$/;
|
|
87
|
+
/** A fenced-code delimiter line: three or more backticks or tildes. */
|
|
88
|
+
const FENCE_LINE_RE = /^(`{3,}|~{3,})/;
|
|
89
|
+
/**
|
|
90
|
+
* The ONE rule for "which `state:` line is this artifact's state": the LAST
|
|
91
|
+
* one. A request artifact is an append-only log — each QA round appends a
|
|
92
|
+
* section ending in its own `## Status`, and `request transition` rewrites the
|
|
93
|
+
* newest state line in place — so the last line is the current round and the
|
|
94
|
+
* earlier ones are history.
|
|
95
|
+
*
|
|
96
|
+
* Every reader (`verify-pipeline`, `request show`, the resume detector) and the
|
|
97
|
+
* writer (`updateStatusBlock`) MUST go through this. The 2026-09-14 defect was
|
|
98
|
+
* three readers disagreeing about one file: `verify-pipeline` and the resume
|
|
99
|
+
* detector took the first match, `request show` took the last, so an artifact
|
|
100
|
+
* appended to more than once read as its first round to the checker and the
|
|
101
|
+
* resume detector while reading as its last round to the viewer and the writer.
|
|
102
|
+
*
|
|
103
|
+
* Scoping the search to the last `## Status` block was evaluated as an
|
|
104
|
+
* alternative and rejected — as a SECOND locator, not as a broken rule. It is
|
|
105
|
+
* well defined on the specimen that motivated this slice (four blocks) and
|
|
106
|
+
* returns `verdict-issued`, the right answer; and a trailing appended
|
|
107
|
+
* `- state:` line does not defeat it the way it defeats this rule — on that
|
|
108
|
+
* input the two rules disagree, and the one the block rule then disagrees with
|
|
109
|
+
* is the writer. `updateStatusBlock` rewrites the last `- state:` line wherever
|
|
110
|
+
* it sits and never moves it into the newest block, so a block-scoped reader
|
|
111
|
+
* parts company with the writer as soon as those two positions differ: the
|
|
112
|
+
* writer writes the appended line while the block reader keeps reporting the
|
|
113
|
+
* block's own line. That is this slice's reader-vs-writer divergence on a new
|
|
114
|
+
* axis. Sharing the writer's locator makes the agreement structural, not
|
|
115
|
+
* accidental.
|
|
116
|
+
*
|
|
117
|
+
* Known boundary of this rule, pinned in
|
|
118
|
+
* `request-artifact-state-authority.test.ts` rather than hidden: a process that
|
|
119
|
+
* appends a bare `- state:` line takes over the field — consistent with the
|
|
120
|
+
* writer being its only sanctioned producer.
|
|
121
|
+
*
|
|
122
|
+
* Two narrowings keep that boundary to lines the writer could have produced.
|
|
123
|
+
* A line inside a fenced code region is skipped, and the line must start at
|
|
124
|
+
* column 0. Neither is defensive decoration: a document that *describes* the
|
|
125
|
+
* state machine quotes `- state: qa-block` inside a fence, and this job's own
|
|
126
|
+
* `qa/requests/*.md` artifacts do exactly that — under the unfenced rule the
|
|
127
|
+
* quoted example was an input to the transition checker, and it survived only
|
|
128
|
+
* because the quoted copies happened not to be last. Both narrowings were
|
|
129
|
+
* measured against every `*.md` under `.peaks/` (833 files) and change no
|
|
130
|
+
* artifact's answer, and the writer already satisfies both by construction
|
|
131
|
+
* (`updateStatusBlock` writes `- state: <state>` at column 0), so reader and
|
|
132
|
+
* writer stay the same locator.
|
|
133
|
+
*
|
|
134
|
+
* Fuller record: the slice-3 section of this session's `rd/tech-doc.md` and
|
|
135
|
+
* the repair-round section of `rd/repair2-meta-integrity-fixes.md`.
|
|
136
|
+
*/
|
|
137
|
+
export function locateArtifactState(lines) {
|
|
81
138
|
let stateLineIndex = -1;
|
|
82
|
-
let
|
|
139
|
+
let state = null;
|
|
140
|
+
let fence = null;
|
|
83
141
|
for (const [index, raw] of lines.entries()) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
142
|
+
const fenceMatch = FENCE_LINE_RE.exec(raw.trim());
|
|
143
|
+
if (fence === null) {
|
|
144
|
+
if (fenceMatch !== null) {
|
|
145
|
+
fence = fenceMatch[1][0];
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (fenceMatch !== null && fenceMatch[1][0] === fence)
|
|
151
|
+
fence = null;
|
|
89
152
|
continue;
|
|
90
153
|
}
|
|
91
|
-
|
|
154
|
+
const match = STATE_LINE_RE.exec(raw);
|
|
155
|
+
if (match?.[1] !== undefined) {
|
|
156
|
+
stateLineIndex = index;
|
|
157
|
+
state = match[1];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { stateLineIndex, state };
|
|
161
|
+
}
|
|
162
|
+
/** `locateArtifactState` over a whole document. Null when there is no `- state:` line. */
|
|
163
|
+
export function readArtifactState(markdown) {
|
|
164
|
+
return locateArtifactState(markdown.split(/\r?\n/)).state;
|
|
165
|
+
}
|
|
166
|
+
export function updateStatusBlock(markdown, newState, timestamp, reason) {
|
|
167
|
+
const lines = markdown.split(/\r?\n/);
|
|
168
|
+
const { stateLineIndex, state } = locateArtifactState(lines);
|
|
169
|
+
const previousState = state ?? 'unknown';
|
|
170
|
+
let lastUpdateLineIndex = -1;
|
|
171
|
+
for (const [index, raw] of lines.entries()) {
|
|
172
|
+
if (/^-\s*last update:\s*/.test(raw.trim())) {
|
|
92
173
|
lastUpdateLineIndex = index;
|
|
93
174
|
}
|
|
94
175
|
}
|
|
@@ -64,7 +64,20 @@ export function resolveActiveSkillForCaller(projectRoot, opts) {
|
|
|
64
64
|
// single skill per resolution. When the lease dir is empty (e.g.
|
|
65
65
|
// ad-hoc / pre-migration projects) we fall through to the legacy
|
|
66
66
|
// walk below.
|
|
67
|
-
|
|
67
|
+
// `getSessionDir` refuses an unsafe session id by throwing (slice
|
|
68
|
+
// 2026-09-14-getsessiondir-guard). This function's contract is the
|
|
69
|
+
// resolution order's "graceful degradation — never throws", so an unsafe
|
|
70
|
+
// id degrades to the same `source: 'none'` shape an absent session dir
|
|
71
|
+
// produces, exactly as it did before that guard existed. Without this,
|
|
72
|
+
// the throw escapes to the nearest caller `catch` — for `hook handle`
|
|
73
|
+
// that catch is a fail-open that skips the SOP gate.
|
|
74
|
+
let sessionDir;
|
|
75
|
+
try {
|
|
76
|
+
sessionDir = getSessionDir(projectRoot, sessionId);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { skill: null, callerId: null, sessionId: null, mode: null, source: 'none' };
|
|
80
|
+
}
|
|
68
81
|
if (!existsSync(sessionDir)) {
|
|
69
82
|
return { skill: null, callerId: null, sessionId, mode: null, source: 'none' };
|
|
70
83
|
}
|
|
@@ -101,6 +101,15 @@ export declare function readPerfTemplate(projectRoot: string): string | null;
|
|
|
101
101
|
export declare function detectPerfAudit(input: {
|
|
102
102
|
readonly projectRoot: string;
|
|
103
103
|
readonly sessionId: string;
|
|
104
|
+
/**
|
|
105
|
+
* The slice whose capsule this run audits. Slice
|
|
106
|
+
* `2026-09-14-prd-capsule-rid-scoping` put the rid in the capsule's
|
|
107
|
+
* filename, so a caller that knows it must pass it or the probe resolves
|
|
108
|
+
* only the pre-rid-scoping bare name. Both callers pass it:
|
|
109
|
+
* `runPerfAudit` always did, and `peaks perf-audit detect` forwards its
|
|
110
|
+
* long-standing `--rid` flag as of the post-verification repair round.
|
|
111
|
+
*/
|
|
112
|
+
readonly requestId?: string;
|
|
104
113
|
readonly dispatchError?: unknown;
|
|
105
114
|
readonly envelope?: unknown;
|
|
106
115
|
}): PerfAuditDetectResult;
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
24
24
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
|
+
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
27
|
+
import { resolveHandoffPath } from '../prd/handoff-service.js';
|
|
26
28
|
/**
|
|
27
29
|
* Validate a raw value as a PerfAuditEnvelope. Mirrors the
|
|
28
30
|
* `isSecurityAuditEnvelope` strict-shape pattern.
|
|
@@ -134,8 +136,12 @@ export function readPerfTemplate(projectRoot) {
|
|
|
134
136
|
export function detectPerfAudit(input) {
|
|
135
137
|
const warnings = [];
|
|
136
138
|
const nextActions = [];
|
|
137
|
-
const handoffPath =
|
|
138
|
-
|
|
139
|
+
const handoffPath = resolveHandoffPath({
|
|
140
|
+
projectRoot: input.projectRoot,
|
|
141
|
+
sessionId: input.sessionId,
|
|
142
|
+
...(input.requestId !== undefined ? { requestId: input.requestId } : {})
|
|
143
|
+
});
|
|
144
|
+
const handoffPresent = handoffPath !== null;
|
|
139
145
|
const templatePath = join(input.projectRoot, '.peaks', 'project-scan', 'perf-template.md');
|
|
140
146
|
const templatePresent = existsSync(templatePath);
|
|
141
147
|
if (!handoffPresent) {
|
|
@@ -143,7 +149,12 @@ export function detectPerfAudit(input) {
|
|
|
143
149
|
state: 'handoff-missing',
|
|
144
150
|
handoffPresent: false,
|
|
145
151
|
templatePresent,
|
|
146
|
-
warnings: [
|
|
152
|
+
warnings: [
|
|
153
|
+
`peaks-prd handoff not found under ${join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'prd')}`,
|
|
154
|
+
...(input.requestId === undefined
|
|
155
|
+
? ['No --rid was supplied, so only the pre-rid-scoping `prd/handoff.md` could be probed. Pass --rid to resolve this slice\'s `prd/handoff-<rid>.md`.']
|
|
156
|
+
: [])
|
|
157
|
+
],
|
|
147
158
|
nextActions: [
|
|
148
159
|
'Run peaks-prd handoff init to produce a sha256-locked handoff before running peaks perf-audit.',
|
|
149
160
|
'Until the handoff exists, peaks-perf-audit cannot start (gate fail).'
|
|
@@ -268,6 +279,12 @@ export function renderPerfAuditArtifact(env, opts) {
|
|
|
268
279
|
* Returns the absolute path on success.
|
|
269
280
|
*/
|
|
270
281
|
export function writePerfAuditArtifact(projectRoot, sessionId, rid, body) {
|
|
282
|
+
// The rid is a filename below, and the write is tmp+rename, so an
|
|
283
|
+
// unvalidated rid can OVERWRITE an arbitrary `.md` rather than merely create
|
|
284
|
+
// one. Guarded here (not only at the CLI boundary) so no caller can skip it.
|
|
285
|
+
if (!REQUEST_ID_PATTERN.test(rid)) {
|
|
286
|
+
throw new Error(`Invalid request id: ${rid} (expected letters, digits, dots, underscores, or dashes)`);
|
|
287
|
+
}
|
|
271
288
|
const targetDir = join(projectRoot, '.peaks', '_runtime', sessionId, 'audit');
|
|
272
289
|
mkdirSync(targetDir, { recursive: true });
|
|
273
290
|
const targetPath = join(targetDir, `perf-${rid}.md`);
|
|
@@ -290,6 +307,7 @@ export function runPerfAudit(input) {
|
|
|
290
307
|
const detect = detectPerfAudit({
|
|
291
308
|
projectRoot: input.projectRoot,
|
|
292
309
|
sessionId: input.sessionId,
|
|
310
|
+
requestId: input.rid,
|
|
293
311
|
...(input.dispatchError !== undefined ? { dispatchError: input.dispatchError } : {}),
|
|
294
312
|
...(input.envelope !== undefined ? { envelope: input.envelope } : {})
|
|
295
313
|
});
|
|
@@ -298,8 +316,12 @@ export function runPerfAudit(input) {
|
|
|
298
316
|
}
|
|
299
317
|
// detect.state === 'ready' implies input.envelope passed isPerfAuditEnvelope.
|
|
300
318
|
const env = input.envelope;
|
|
301
|
-
const handoffPath =
|
|
302
|
-
|
|
319
|
+
const handoffPath = resolveHandoffPath({
|
|
320
|
+
projectRoot: input.projectRoot,
|
|
321
|
+
sessionId: input.sessionId,
|
|
322
|
+
requestId: input.rid
|
|
323
|
+
});
|
|
324
|
+
const verified = handoffPath === null ? null : readAndVerifyHandoff(handoffPath, input.projectRoot);
|
|
303
325
|
const handoffHash = verified?.frontmatter.sha256 ?? 'unknown';
|
|
304
326
|
const rendered = renderPerfAuditArtifact(env, {
|
|
305
327
|
rid: input.rid,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The service owns:
|
|
9
9
|
* - 5-state detection of the security-audit runtime
|
|
10
10
|
* (handoff-missing / template-missing / dispatch-failed / template-malformed / ready)
|
|
11
|
-
* - Loading + sha256 verification of the prd/handoff
|
|
11
|
+
* - Loading + sha256 verification of the slice's prd/handoff-<rid>.md
|
|
12
12
|
* - Loading the project-level security-template.md
|
|
13
13
|
* - Producing the audit envelope (verdict + violations) to write
|
|
14
14
|
* to `.peaks/_runtime/<sid>/audit/security-<rid>.md`
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
* 5-state detection result. Mirrors `detectEcc` in `services/code-review/ecc-bridge.ts`.
|
|
37
37
|
*
|
|
38
38
|
* - `ready` — handoff + template + project all present
|
|
39
|
-
* - `handoff-missing` — `.peaks/_runtime/<sid>/prd/handoff
|
|
39
|
+
* - `handoff-missing` — this slice's `.peaks/_runtime/<sid>/prd/handoff-<rid>.md`
|
|
40
|
+
* (or the pre-rid-scoping `prd/handoff.md`) absent
|
|
40
41
|
* - `template-missing` — `.peaks/project-scan/security-template.md` absent
|
|
41
42
|
* - `dispatch-failed` — parent LLM threw before returning the audit envelope
|
|
42
43
|
* - `envelope-malformed` — parent LLM returned a value that fails `isSecurityAuditEnvelope`
|
|
@@ -107,6 +108,15 @@ export declare function readSecurityTemplate(projectRoot: string): string | null
|
|
|
107
108
|
export declare function detectSecurityAudit(input: {
|
|
108
109
|
readonly projectRoot: string;
|
|
109
110
|
readonly sessionId: string;
|
|
111
|
+
/**
|
|
112
|
+
* The slice whose capsule this run audits. Slice
|
|
113
|
+
* `2026-09-14-prd-capsule-rid-scoping` put the rid in the capsule's
|
|
114
|
+
* filename, so a caller that knows it must pass it or the probe resolves
|
|
115
|
+
* only the pre-rid-scoping bare name. Both callers pass it:
|
|
116
|
+
* `runSecurityAudit` always did, and `peaks security-audit detect` forwards
|
|
117
|
+
* its long-standing `--rid` flag as of the post-verification repair round.
|
|
118
|
+
*/
|
|
119
|
+
readonly requestId?: string;
|
|
110
120
|
readonly dispatchError?: unknown;
|
|
111
121
|
readonly envelope?: unknown;
|
|
112
122
|
}): SecurityAuditDetectResult;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The service owns:
|
|
9
9
|
* - 5-state detection of the security-audit runtime
|
|
10
10
|
* (handoff-missing / template-missing / dispatch-failed / template-malformed / ready)
|
|
11
|
-
* - Loading + sha256 verification of the prd/handoff
|
|
11
|
+
* - Loading + sha256 verification of the slice's prd/handoff-<rid>.md
|
|
12
12
|
* - Loading the project-level security-template.md
|
|
13
13
|
* - Producing the audit envelope (verdict + violations) to write
|
|
14
14
|
* to `.peaks/_runtime/<sid>/audit/security-<rid>.md`
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
36
36
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
37
37
|
import { createHash } from 'node:crypto';
|
|
38
|
+
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
39
|
+
import { resolveHandoffPath } from '../prd/handoff-service.js';
|
|
38
40
|
/**
|
|
39
41
|
* Validate a raw value as a SecurityAuditEnvelope. Mirrors the
|
|
40
42
|
* `isEccEnvelope` strict-shape pattern.
|
|
@@ -145,8 +147,12 @@ export function readSecurityTemplate(projectRoot) {
|
|
|
145
147
|
export function detectSecurityAudit(input) {
|
|
146
148
|
const warnings = [];
|
|
147
149
|
const nextActions = [];
|
|
148
|
-
const handoffPath =
|
|
149
|
-
|
|
150
|
+
const handoffPath = resolveHandoffPath({
|
|
151
|
+
projectRoot: input.projectRoot,
|
|
152
|
+
sessionId: input.sessionId,
|
|
153
|
+
...(input.requestId !== undefined ? { requestId: input.requestId } : {})
|
|
154
|
+
});
|
|
155
|
+
const handoffPresent = handoffPath !== null;
|
|
150
156
|
const templatePath = join(input.projectRoot, '.peaks', 'project-scan', 'security-template.md');
|
|
151
157
|
const templatePresent = existsSync(templatePath);
|
|
152
158
|
if (!handoffPresent) {
|
|
@@ -154,7 +160,12 @@ export function detectSecurityAudit(input) {
|
|
|
154
160
|
state: 'handoff-missing',
|
|
155
161
|
handoffPresent: false,
|
|
156
162
|
templatePresent,
|
|
157
|
-
warnings: [
|
|
163
|
+
warnings: [
|
|
164
|
+
`peaks-prd handoff not found under ${join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'prd')}`,
|
|
165
|
+
...(input.requestId === undefined
|
|
166
|
+
? ['No --rid was supplied, so only the pre-rid-scoping `prd/handoff.md` could be probed. Pass --rid to resolve this slice\'s `prd/handoff-<rid>.md`.']
|
|
167
|
+
: [])
|
|
168
|
+
],
|
|
158
169
|
nextActions: [
|
|
159
170
|
'Run peaks-prd handoff init to produce a sha256-locked handoff before running peaks security-audit.',
|
|
160
171
|
'Until the handoff exists, peaks-security-audit cannot start (gate fail).'
|
|
@@ -268,6 +279,12 @@ export function renderSecurityAuditArtifact(env, opts) {
|
|
|
268
279
|
* Returns the absolute path on success.
|
|
269
280
|
*/
|
|
270
281
|
export function writeSecurityAuditArtifact(projectRoot, sessionId, rid, body) {
|
|
282
|
+
// The rid is a filename below, and the write is tmp+rename, so an
|
|
283
|
+
// unvalidated rid can OVERWRITE an arbitrary `.md` rather than merely create
|
|
284
|
+
// one. Guarded here (not only at the CLI boundary) so no caller can skip it.
|
|
285
|
+
if (!REQUEST_ID_PATTERN.test(rid)) {
|
|
286
|
+
throw new Error(`Invalid request id: ${rid} (expected letters, digits, dots, underscores, or dashes)`);
|
|
287
|
+
}
|
|
271
288
|
const targetDir = join(projectRoot, '.peaks', '_runtime', sessionId, 'audit');
|
|
272
289
|
mkdirSync(targetDir, { recursive: true });
|
|
273
290
|
const targetPath = join(targetDir, `security-${rid}.md`);
|
|
@@ -290,6 +307,7 @@ export function runSecurityAudit(input) {
|
|
|
290
307
|
const detect = detectSecurityAudit({
|
|
291
308
|
projectRoot: input.projectRoot,
|
|
292
309
|
sessionId: input.sessionId,
|
|
310
|
+
requestId: input.rid,
|
|
293
311
|
...(input.dispatchError !== undefined ? { dispatchError: input.dispatchError } : {}),
|
|
294
312
|
...(input.envelope !== undefined ? { envelope: input.envelope } : {})
|
|
295
313
|
});
|
|
@@ -298,8 +316,12 @@ export function runSecurityAudit(input) {
|
|
|
298
316
|
}
|
|
299
317
|
// detect.state === 'ready' implies input.envelope passed isSecurityAuditEnvelope.
|
|
300
318
|
const env = input.envelope;
|
|
301
|
-
const handoffPath =
|
|
302
|
-
|
|
319
|
+
const handoffPath = resolveHandoffPath({
|
|
320
|
+
projectRoot: input.projectRoot,
|
|
321
|
+
sessionId: input.sessionId,
|
|
322
|
+
requestId: input.rid
|
|
323
|
+
});
|
|
324
|
+
const verified = handoffPath === null ? null : readAndVerifyHandoff(handoffPath, input.projectRoot);
|
|
303
325
|
const handoffHash = verified?.frontmatter.sha256 ?? 'unknown';
|
|
304
326
|
const rendered = renderSecurityAuditArtifact(env, {
|
|
305
327
|
rid: input.rid,
|
|
@@ -51,6 +51,65 @@ export declare function resolveDispatchedStage(input: {
|
|
|
51
51
|
* attempts inside the same millisecond distinct.
|
|
52
52
|
*/
|
|
53
53
|
export declare function newCompactRunId(now: Date): string;
|
|
54
|
+
/**
|
|
55
|
+
* rid `2026-09-14-compact-dispatch-backoff`: the compact run this session has
|
|
56
|
+
* already DISPATCHED and whose outcome is still unknown — the backoff token.
|
|
57
|
+
*
|
|
58
|
+
* WHY A BACKOFF IS NEEDED AT ALL. Once the ratio crosses the auto-fire
|
|
59
|
+
* threshold it STAYS crossed: nothing in peaks-loop can compact a running
|
|
60
|
+
* session, and the harness fires only at its own red line. So the dispatch
|
|
61
|
+
* obligation became unsatisfiable and fired on every probe. Measured in one
|
|
62
|
+
* real session (2026-09-13T22:43:33Z → 2026-09-14T14:15:02Z, ~15.5 h):
|
|
63
|
+
* 1035 `dispatch` rows, 444 checkpoints, and ZERO compactions — 1075 by
|
|
64
|
+
* 14:22:09.112Z. The dispatch itself is idempotent — `ide-native` only
|
|
65
|
+
* installs a PreToolUse hook, and a second install of the same hook is a
|
|
66
|
+
* documented no-op — so 1074 of those rows installed nothing (their own
|
|
67
|
+
* `dispatchMessage` says `already installed`) and carried no new information.
|
|
68
|
+
* Only noise: a signal that fires a thousand times is not a signal.
|
|
69
|
+
*
|
|
70
|
+
* WHY NO NEW STORE. The one-record-per-session lifecycle store already holds
|
|
71
|
+
* the one fact the backoff needs: is a compact attempt dispatched and not yet
|
|
72
|
+
* superseded? `armed` and `compacting` are exactly those two stages, and
|
|
73
|
+
* `completed` / `failed` mean the attempt is over and the next crossing is a
|
|
74
|
+
* legitimate new one. The state lives here rather than in
|
|
75
|
+
* `compact-history.jsonl`, which is append-only and never rewritten — so the
|
|
76
|
+
* backoff cannot be built by editing history, which is the point.
|
|
77
|
+
*
|
|
78
|
+
* STALENESS IS DELIBERATELY IGNORED, for `settleOpenLifecycleRun`'s reason: a
|
|
79
|
+
* probe arriving after `staleAfterMs` has learned nothing about whether the run
|
|
80
|
+
* is still live. Age cannot make re-dispatching honest, so the stale window
|
|
81
|
+
* must not decide it. (`settleOpenLifecycleRun` reads with the same window for
|
|
82
|
+
* the same reason; the two must not grow two policies.)
|
|
83
|
+
*
|
|
84
|
+
* `queued` / `preparing` / `verifying` are NOT open here: they are transient
|
|
85
|
+
* stages inside a dispatch or a settle, and a process that died in one of them
|
|
86
|
+
* left no compact attempt to protect — the next probe should be free to
|
|
87
|
+
* dispatch. `failed` is not open either, by the same argument: a failure is a
|
|
88
|
+
* reason to try again, not a reason to stay quiet.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* The answer to "is a compact attempt outstanding for this session?".
|
|
92
|
+
*
|
|
93
|
+
* `none` and `unresolvable` must not be the same value. The caller uses
|
|
94
|
+
* `none` to ADMIT a dispatch; `unresolvable` means the question could
|
|
95
|
+
* not be asked, and a caller that admits on "could not ask" has a gate
|
|
96
|
+
* that reads a string rather than the artifact it names.
|
|
97
|
+
*/
|
|
98
|
+
export type OpenDispatchRunRead = {
|
|
99
|
+
readonly kind: 'open';
|
|
100
|
+
readonly runId: string;
|
|
101
|
+
readonly stage: 'armed' | 'compacting';
|
|
102
|
+
readonly triggerRatio: number;
|
|
103
|
+
} | {
|
|
104
|
+
readonly kind: 'none';
|
|
105
|
+
} | {
|
|
106
|
+
readonly kind: 'unresolvable';
|
|
107
|
+
readonly reason: string;
|
|
108
|
+
};
|
|
109
|
+
export declare function readOpenDispatchRun(input: {
|
|
110
|
+
readonly projectRoot: string;
|
|
111
|
+
readonly sessionId: string;
|
|
112
|
+
}): OpenDispatchRunRead;
|
|
54
113
|
/**
|
|
55
114
|
* Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
|
|
56
115
|
* builder. Carries `runId`, `triggerRatio` and `redLine` forward from
|
|
@@ -132,6 +191,18 @@ export declare function summarizeLifecycleError(error: unknown): string;
|
|
|
132
191
|
* `afterRatio` to append the "observed compaction point" row that makes the
|
|
133
192
|
* intent-vs-observed gap readable after a real session. The return value is
|
|
134
193
|
* telemetry only — callers that ignore it are unaffected.
|
|
194
|
+
*
|
|
195
|
+
* `lifecycleWritten` (repair R6) makes the returned record honest about
|
|
196
|
+
* whether the RUN actually moved. Before it, a failed write still returned
|
|
197
|
+
* the full envelope, so a store that could not be written reported a settle
|
|
198
|
+
* on every probe, forever: the run stayed `armed`, the next probe found it
|
|
199
|
+
* open again, and each one added another "observed compaction point" row —
|
|
200
|
+
* an unbounded append driven by a write that never happened. A failed write
|
|
201
|
+
* is not an absent run (the facts are real and stay true, so the caller may
|
|
202
|
+
* still record the measurement), but it is also not a settled one. This is
|
|
203
|
+
* the same field `settleOpenLifecycleRunOnCompactEvent` carries, for the
|
|
204
|
+
* same reason, so the two settle paths no longer disagree about what a write
|
|
205
|
+
* failure means.
|
|
135
206
|
*/
|
|
136
207
|
export declare function settleOpenLifecycleRun(input: {
|
|
137
208
|
readonly projectRoot: string;
|
|
@@ -140,6 +211,129 @@ export declare function settleOpenLifecycleRun(input: {
|
|
|
140
211
|
readonly source: string;
|
|
141
212
|
readonly autoFireThreshold: number;
|
|
142
213
|
readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
|
|
214
|
+
/** Failure injection for the write below — the seam `CompactLifecyclePublisher` already takes. */
|
|
215
|
+
readonly failLifecycleWrite?: boolean | undefined;
|
|
216
|
+
}): {
|
|
217
|
+
readonly runId: string;
|
|
218
|
+
readonly triggerRatio: number;
|
|
219
|
+
readonly afterRatio: number;
|
|
220
|
+
/** `false` when the `completed` write threw, so the run is STILL open. */
|
|
221
|
+
readonly lifecycleWritten: boolean;
|
|
222
|
+
} | null;
|
|
223
|
+
/**
|
|
224
|
+
* rid `2026-09-13-compact-event-settle`: close out an open compact run because
|
|
225
|
+
* the HARNESS said one completed — `PostCompact` — rather than because a later
|
|
226
|
+
* probe noticed the ratio had fallen.
|
|
227
|
+
*
|
|
228
|
+
* WHY THIS IS A SECOND FUNCTION AND NOT A FLAG ON THE ONE ABOVE. The function
|
|
229
|
+
* above is defined by two MEASUREMENT gates: it refuses when nothing could be
|
|
230
|
+
* measured, and refuses when the number it got has not dropped far enough. Both
|
|
231
|
+
* are correct for a probe, whose ratio is an INFERENCE about whether something
|
|
232
|
+
* happened. Handed a harness event, both are wrong in the same direction —
|
|
233
|
+
* the harness has already stated that the compaction happened, so a probe that
|
|
234
|
+
* could not measure, or measured something larger, contradicts nothing. The
|
|
235
|
+
* event is the evidence; the ratio is a consequence.
|
|
236
|
+
*
|
|
237
|
+
* What survives from the probe path is the ATTRIBUTION gate, and only that:
|
|
238
|
+
* there must be an open run (`compacting` / `armed`) for this event to be
|
|
239
|
+
* about. A `PostCompact` on a session where peaks-loop never dispatched has
|
|
240
|
+
* nothing to settle — objectively, the run the event would complete does not
|
|
241
|
+
* exist. (`queued` / `preparing` are excluded for the probe path's reason: a
|
|
242
|
+
* run that died before dispatch never had a compaction to complete.)
|
|
243
|
+
*
|
|
244
|
+
* `afterRatio` is recorded ONLY when it is a genuine DROP below the ratio the
|
|
245
|
+
* dispatch was made at. Immediately after a compaction, `readContextPercent`
|
|
246
|
+
* prefers the statusline file, which may still hold the PRE-compact value; the
|
|
247
|
+
* one thing this row must not do is launder that stale reading into an
|
|
248
|
+
* `afterRatio` and publish "the context did not shrink" as a measurement. A
|
|
249
|
+
* `null` here means "no honest post-compact number was available at the moment
|
|
250
|
+
* the event fired" — and that is NOT self-healing: the record is left at
|
|
251
|
+
* `completed` with no number, `computeWindowCalibration` skips `observed` rows
|
|
252
|
+
* that carry none, and the probe path refuses a run that is no longer open. The
|
|
253
|
+
* pair is then closed by `fillEventSettledMeasurement` below — but only on the
|
|
254
|
+
* probes that reach it, which is not all of them: that call sits in the
|
|
255
|
+
* BELOW-THRESHOLD branch of `runAutoCompact` (`auto-compact-orchestrator.ts:567`
|
|
256
|
+
* guards it, `:589` calls it). A probe that instead commits to compacting does
|
|
257
|
+
* not merely defer the measurement: `advance('queued')` writes a fresh run to
|
|
258
|
+
* the same one-record-per-session store (`auto-compact-orchestrator.ts:668`), so
|
|
259
|
+
* the `completed`-without-`afterRatio` record this pair was owed is gone and the
|
|
260
|
+
* pair stays unmeasured. That loss is inherent rather than an oversight — once a
|
|
261
|
+
* second compaction has happened, no later ratio can be attributed to the first,
|
|
262
|
+
* so there is nothing honest left to fill — and it is visible as `unmeasured` in
|
|
263
|
+
* `peaks compact history` (QA residual R9). A fabricated number is not
|
|
264
|
+
* recoverable at all, which is why the stale reading is dropped rather than
|
|
265
|
+
* corrected.
|
|
266
|
+
*
|
|
267
|
+
* `verifying` is deliberately NOT emitted: its documented meaning is "we hold a
|
|
268
|
+
* measurement and are checking it", and on this path there may be no
|
|
269
|
+
* measurement at all. Emitting it would move the same untruth from the history
|
|
270
|
+
* row into the lifecycle record.
|
|
271
|
+
*
|
|
272
|
+
* Returns the settled facts, or `null` when there was nothing to settle. `null`
|
|
273
|
+
* means exactly ONE thing here — there was no OPEN run for this event to be
|
|
274
|
+
* about. A run that was open but whose record could not be written is not
|
|
275
|
+
* `null`: it returns the facts read off that run with `lifecycleWritten: false`,
|
|
276
|
+
* because a failed write is not an absent run, and a caller that cannot tell the
|
|
277
|
+
* two apart ends up telling the user a falsehood (see `compact-event-settle.ts`).
|
|
278
|
+
*/
|
|
279
|
+
export declare function settleOpenLifecycleRunOnCompactEvent(input: {
|
|
280
|
+
readonly projectRoot: string;
|
|
281
|
+
readonly sessionId: string;
|
|
282
|
+
readonly measuredRatio: number | null;
|
|
283
|
+
readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
|
|
284
|
+
/** Failure injection for the write below — the seam `CompactLifecyclePublisher` already takes. */
|
|
285
|
+
readonly failLifecycleWrite?: boolean | undefined;
|
|
286
|
+
}): {
|
|
287
|
+
readonly runId: string;
|
|
288
|
+
readonly triggerRatio: number;
|
|
289
|
+
readonly afterRatio: number | null;
|
|
290
|
+
/**
|
|
291
|
+
* `false` when the run could not be marked settled — `writeCompactLifecycle`
|
|
292
|
+
* threw. The three facts above were read off the OPEN run, so they stay true
|
|
293
|
+
* and a caller may still record the observation; what it must not do is report
|
|
294
|
+
* the run as settled. `settleOpenLifecycleRun` above does not suppress its
|
|
295
|
+
* returned record on a failed write either, so the two now agree that a write
|
|
296
|
+
* failure is not "nothing happened". This field exists because this function's
|
|
297
|
+
* caller, unlike the sibling's, renders a sentence about the outcome.
|
|
298
|
+
*/
|
|
299
|
+
readonly lifecycleWritten: boolean;
|
|
300
|
+
} | null;
|
|
301
|
+
/**
|
|
302
|
+
* rid `2026-09-13-compact-event-settle` (repair R1): supply the measurement a
|
|
303
|
+
* harness-settled run was left owing.
|
|
304
|
+
*
|
|
305
|
+
* The function above deliberately refuses to launder a post-compact reading
|
|
306
|
+
* that has not dropped — and right after a compaction that refusal is the
|
|
307
|
+
* NORMAL case, because the statusline still holds the pre-compact value. The
|
|
308
|
+
* run is then closed at `completed` with no `afterRatio`, so the dispatch's
|
|
309
|
+
* calibration pair never closes and "intent vs observed" stays blank for
|
|
310
|
+
* exactly the compactions this slice exists to witness. This function is what
|
|
311
|
+
* makes the function above's promise payable.
|
|
312
|
+
*
|
|
313
|
+
* WHY NOT WIDEN `settleOpenLifecycleRun`. That one re-emits `verifying` before
|
|
314
|
+
* `completed`, which on an already-`completed` record is a backwards stage
|
|
315
|
+
* transition with no observer to serve. This is not a settlement — the run IS
|
|
316
|
+
* settled; only the number is owed. So no stage is rewritten here.
|
|
317
|
+
*
|
|
318
|
+
* WRITING `afterRatio` ONTO THE RECORD IS THE IDEMPOTENCE TOKEN: every later
|
|
319
|
+
* probe finds it present and returns `null`, so however many probes follow, one
|
|
320
|
+
* compaction yields exactly one late measurement.
|
|
321
|
+
*
|
|
322
|
+
* THE DROP GATE IS THE EVENT PATH'S OWN (`measuredRatio < triggerRatio`), not
|
|
323
|
+
* the probe path's `autoFireThreshold`. `afterRatio` has to mean "below the
|
|
324
|
+
* ratio this run was dispatched at" — the rule the event path already enforces
|
|
325
|
+
* — or a run dispatched under the threshold (a forced or banded dispatch) would
|
|
326
|
+
* let a NON-drop through the one path that can still write a `completed` record.
|
|
327
|
+
* `conservative-fallback` is refused for the probe path's reason: its `0` is
|
|
328
|
+
* the absence of a measurement, not an empty context.
|
|
329
|
+
*
|
|
330
|
+
* Returns the filled record, or `null` when no run is owed a measurement.
|
|
331
|
+
*/
|
|
332
|
+
export declare function fillEventSettledMeasurement(input: {
|
|
333
|
+
readonly projectRoot: string;
|
|
334
|
+
readonly sessionId: string;
|
|
335
|
+
readonly measuredRatio: number;
|
|
336
|
+
readonly source: string;
|
|
143
337
|
}): {
|
|
144
338
|
readonly runId: string;
|
|
145
339
|
readonly triggerRatio: number;
|