peaks-loop 4.0.47 → 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 +24 -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 +112 -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/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -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 +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 +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/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 +130 -65
- 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/code/auto-compact-lifecycle.d.ts +119 -0
- package/dist/services/code/auto-compact-lifecycle.js +169 -0
- package/dist/services/code/auto-compact-orchestrator.js +13 -2
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -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/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/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-state-store.js +7 -0
- 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 +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/scan/karpathy-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/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 +8 -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/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/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
|
@@ -11,6 +11,7 @@ import { ensureSession, getSessionIdCanonical } from '../session/session-manager
|
|
|
11
11
|
// live at `shared/path-safety.ts` if this module ever needs them.
|
|
12
12
|
import { getNextNumber, buildNumberedFilename, slugifyDescription } from '../../shared/incrementing-number.js';
|
|
13
13
|
import { lintRequestArtifact } from './artifact-lint-service.js';
|
|
14
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
14
15
|
import { checkTypeSanity } from '../scan/type-sanity-service.js';
|
|
15
16
|
import { requireUserConfirmation } from '../mode/mode-enforcement.js';
|
|
16
17
|
import { scanFileSize } from '../scan/file-size-scan.js';
|
|
@@ -27,7 +28,14 @@ export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType };
|
|
|
27
28
|
// the local namespace under `isolatedModules`).
|
|
28
29
|
import { renderTemplate } from './artifact-templates.js';
|
|
29
30
|
export { formatHandoffPath, formatCommitBoundaryPath, formatSkillUsageLessonsPath } from './artifact-templates.js';
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* F-1 (slice 025 security): reject rids that contain path separators, null
|
|
33
|
+
* bytes, or traversal sequences. A request id is a single path segment, so
|
|
34
|
+
* anything that builds a filename from one must test it against this first —
|
|
35
|
+
* it is exported so those call sites reuse it instead of re-declaring a copy
|
|
36
|
+
* that can drift.
|
|
37
|
+
*/
|
|
38
|
+
export const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
31
39
|
const VALID_ROLES = new Set(['prd', 'ui', 'rd', 'qa', 'sc']);
|
|
32
40
|
function defaultClock() {
|
|
33
41
|
return new Date().toISOString();
|
|
@@ -60,6 +68,13 @@ export async function createRequestArtifact(options) {
|
|
|
60
68
|
// in the artifact body's frontmatter (under `- change-id:`) for
|
|
61
69
|
// human navigation; it is no longer a filesystem path key.
|
|
62
70
|
const sessionId = options.sessionId ?? await ensureSession(options.projectRoot);
|
|
71
|
+
// Sid axis. The rid axis is guarded three times above (`REQUEST_ID_PATTERN`
|
|
72
|
+
// at :110, and again in the numbered-filename path); the session id was
|
|
73
|
+
// never checked, so `--session-id ../../x` wrote the artifact outside the
|
|
74
|
+
// project root under an `ok: true` envelope.
|
|
75
|
+
if (isUnsafePathInput(sessionId)) {
|
|
76
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
77
|
+
}
|
|
63
78
|
// Slice 2026-06-29-change-id-root-removal: the `current-change`
|
|
64
79
|
// binding file is gone. Resolution order for the change-id (file
|
|
65
80
|
// body metadata) is now:
|
|
@@ -154,17 +169,12 @@ export async function createRequestArtifact(options) {
|
|
|
154
169
|
};
|
|
155
170
|
}
|
|
156
171
|
function extractMetadata(markdown) {
|
|
157
|
-
|
|
172
|
+
const state = readArtifactState(markdown) ?? 'unknown';
|
|
158
173
|
let createdAt;
|
|
159
174
|
let requestType = DEFAULT_REQUEST_TYPE;
|
|
160
175
|
let sessionId;
|
|
161
176
|
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
162
177
|
const line = rawLine.trim();
|
|
163
|
-
const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(line);
|
|
164
|
-
if (stateMatch !== null && stateMatch[1] !== undefined) {
|
|
165
|
-
state = stateMatch[1];
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
178
|
const createdMatch = /^-\s*created:\s*(.+?)\s*$/.exec(line);
|
|
169
179
|
if (createdMatch !== null && createdMatch[1] !== undefined) {
|
|
170
180
|
createdAt = createdMatch[1];
|
|
@@ -343,7 +353,7 @@ async function readRequestArtifact(projectRoot, scope, role, found) {
|
|
|
343
353
|
// the sibling `request-artifact-state-helpers.ts` module — see
|
|
344
354
|
// v2.18.3 file-split for the rationale. Function signatures and
|
|
345
355
|
// behaviour are unchanged (verbatim move).
|
|
346
|
-
import { ALLOWED_STATES_PER_ROLE, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock, } from './request-artifact-state-helpers.js';
|
|
356
|
+
import { ALLOWED_STATES_PER_ROLE, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, readArtifactState, TypeSanityViolationError, updateStatusBlock, } from './request-artifact-state-helpers.js';
|
|
347
357
|
export { allowedStatesForRole, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
348
358
|
export async function transitionRequestArtifact(options) {
|
|
349
359
|
if (!VALID_ROLES.has(options.role)) {
|
|
@@ -45,6 +45,63 @@ export declare class FileSizeViolationError extends Error {
|
|
|
45
45
|
lines: number;
|
|
46
46
|
}>, threshold: number);
|
|
47
47
|
}
|
|
48
|
+
export interface ArtifactStateLocation {
|
|
49
|
+
/** Index of the authoritative `- state:` line, or -1 when the document has none. */
|
|
50
|
+
stateLineIndex: number;
|
|
51
|
+
/** The authoritative state, or null when `stateLineIndex` is -1. */
|
|
52
|
+
state: string | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The ONE rule for "which `state:` line is this artifact's state": the LAST
|
|
56
|
+
* one. A request artifact is an append-only log — each QA round appends a
|
|
57
|
+
* section ending in its own `## Status`, and `request transition` rewrites the
|
|
58
|
+
* newest state line in place — so the last line is the current round and the
|
|
59
|
+
* earlier ones are history.
|
|
60
|
+
*
|
|
61
|
+
* Every reader (`verify-pipeline`, `request show`, the resume detector) and the
|
|
62
|
+
* writer (`updateStatusBlock`) MUST go through this. The 2026-09-14 defect was
|
|
63
|
+
* three readers disagreeing about one file: `verify-pipeline` and the resume
|
|
64
|
+
* detector took the first match, `request show` took the last, so an artifact
|
|
65
|
+
* appended to more than once read as its first round to the checker and the
|
|
66
|
+
* resume detector while reading as its last round to the viewer and the writer.
|
|
67
|
+
*
|
|
68
|
+
* Scoping the search to the last `## Status` block was evaluated as an
|
|
69
|
+
* alternative and rejected — as a SECOND locator, not as a broken rule. It is
|
|
70
|
+
* well defined on the specimen that motivated this slice (four blocks) and
|
|
71
|
+
* returns `verdict-issued`, the right answer; and a trailing appended
|
|
72
|
+
* `- state:` line does not defeat it the way it defeats this rule — on that
|
|
73
|
+
* input the two rules disagree, and the one the block rule then disagrees with
|
|
74
|
+
* is the writer. `updateStatusBlock` rewrites the last `- state:` line wherever
|
|
75
|
+
* it sits and never moves it into the newest block, so a block-scoped reader
|
|
76
|
+
* parts company with the writer as soon as those two positions differ: the
|
|
77
|
+
* writer writes the appended line while the block reader keeps reporting the
|
|
78
|
+
* block's own line. That is this slice's reader-vs-writer divergence on a new
|
|
79
|
+
* axis. Sharing the writer's locator makes the agreement structural, not
|
|
80
|
+
* accidental.
|
|
81
|
+
*
|
|
82
|
+
* Known boundary of this rule, pinned in
|
|
83
|
+
* `request-artifact-state-authority.test.ts` rather than hidden: a process that
|
|
84
|
+
* appends a bare `- state:` line takes over the field — consistent with the
|
|
85
|
+
* writer being its only sanctioned producer.
|
|
86
|
+
*
|
|
87
|
+
* Two narrowings keep that boundary to lines the writer could have produced.
|
|
88
|
+
* A line inside a fenced code region is skipped, and the line must start at
|
|
89
|
+
* column 0. Neither is defensive decoration: a document that *describes* the
|
|
90
|
+
* state machine quotes `- state: qa-block` inside a fence, and this job's own
|
|
91
|
+
* `qa/requests/*.md` artifacts do exactly that — under the unfenced rule the
|
|
92
|
+
* quoted example was an input to the transition checker, and it survived only
|
|
93
|
+
* because the quoted copies happened not to be last. Both narrowings were
|
|
94
|
+
* measured against every `*.md` under `.peaks/` (833 files) and change no
|
|
95
|
+
* artifact's answer, and the writer already satisfies both by construction
|
|
96
|
+
* (`updateStatusBlock` writes `- state: <state>` at column 0), so reader and
|
|
97
|
+
* writer stay the same locator.
|
|
98
|
+
*
|
|
99
|
+
* Fuller record: the slice-3 section of this session's `rd/tech-doc.md` and
|
|
100
|
+
* the repair-round section of `rd/repair2-meta-integrity-fixes.md`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function locateArtifactState(lines: ReadonlyArray<string>): ArtifactStateLocation;
|
|
103
|
+
/** `locateArtifactState` over a whole document. Null when there is no `- state:` line. */
|
|
104
|
+
export declare function readArtifactState(markdown: string): string | null;
|
|
48
105
|
export declare function updateStatusBlock(markdown: string, newState: RequestArtifactState, timestamp: string, reason?: string): {
|
|
49
106
|
updated: string;
|
|
50
107
|
previousState: string;
|
|
@@ -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
|
}
|
|
@@ -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,
|
|
@@ -145,4 +145,123 @@ export declare function settleOpenLifecycleRun(input: {
|
|
|
145
145
|
readonly triggerRatio: number;
|
|
146
146
|
readonly afterRatio: number;
|
|
147
147
|
} | null;
|
|
148
|
+
/**
|
|
149
|
+
* rid `2026-09-13-compact-event-settle`: close out an open compact run because
|
|
150
|
+
* the HARNESS said one completed — `PostCompact` — rather than because a later
|
|
151
|
+
* probe noticed the ratio had fallen.
|
|
152
|
+
*
|
|
153
|
+
* WHY THIS IS A SECOND FUNCTION AND NOT A FLAG ON THE ONE ABOVE. The function
|
|
154
|
+
* above is defined by two MEASUREMENT gates: it refuses when nothing could be
|
|
155
|
+
* measured, and refuses when the number it got has not dropped far enough. Both
|
|
156
|
+
* are correct for a probe, whose ratio is an INFERENCE about whether something
|
|
157
|
+
* happened. Handed a harness event, both are wrong in the same direction —
|
|
158
|
+
* the harness has already stated that the compaction happened, so a probe that
|
|
159
|
+
* could not measure, or measured something larger, contradicts nothing. The
|
|
160
|
+
* event is the evidence; the ratio is a consequence.
|
|
161
|
+
*
|
|
162
|
+
* What survives from the probe path is the ATTRIBUTION gate, and only that:
|
|
163
|
+
* there must be an open run (`compacting` / `armed`) for this event to be
|
|
164
|
+
* about. A `PostCompact` on a session where peaks-loop never dispatched has
|
|
165
|
+
* nothing to settle — objectively, the run the event would complete does not
|
|
166
|
+
* exist. (`queued` / `preparing` are excluded for the probe path's reason: a
|
|
167
|
+
* run that died before dispatch never had a compaction to complete.)
|
|
168
|
+
*
|
|
169
|
+
* `afterRatio` is recorded ONLY when it is a genuine DROP below the ratio the
|
|
170
|
+
* dispatch was made at. Immediately after a compaction, `readContextPercent`
|
|
171
|
+
* prefers the statusline file, which may still hold the PRE-compact value; the
|
|
172
|
+
* one thing this row must not do is launder that stale reading into an
|
|
173
|
+
* `afterRatio` and publish "the context did not shrink" as a measurement. A
|
|
174
|
+
* `null` here means "no honest post-compact number was available at the moment
|
|
175
|
+
* the event fired" — and that is NOT self-healing: the record is left at
|
|
176
|
+
* `completed` with no number, `computeWindowCalibration` skips `observed` rows
|
|
177
|
+
* that carry none, and the probe path refuses a run that is no longer open. The
|
|
178
|
+
* pair is then closed by `fillEventSettledMeasurement` below — but only on the
|
|
179
|
+
* probes that reach it, which is not all of them: that call sits in the
|
|
180
|
+
* BELOW-THRESHOLD branch of `runAutoCompact` (`auto-compact-orchestrator.ts:567`
|
|
181
|
+
* guards it, `:589` calls it). A probe that instead commits to compacting does
|
|
182
|
+
* not merely defer the measurement: `advance('queued')` writes a fresh run to
|
|
183
|
+
* the same one-record-per-session store (`auto-compact-orchestrator.ts:668`), so
|
|
184
|
+
* the `completed`-without-`afterRatio` record this pair was owed is gone and the
|
|
185
|
+
* pair stays unmeasured. That loss is inherent rather than an oversight — once a
|
|
186
|
+
* second compaction has happened, no later ratio can be attributed to the first,
|
|
187
|
+
* so there is nothing honest left to fill — and it is visible as `unmeasured` in
|
|
188
|
+
* `peaks compact history` (QA residual R9). A fabricated number is not
|
|
189
|
+
* recoverable at all, which is why the stale reading is dropped rather than
|
|
190
|
+
* corrected.
|
|
191
|
+
*
|
|
192
|
+
* `verifying` is deliberately NOT emitted: its documented meaning is "we hold a
|
|
193
|
+
* measurement and are checking it", and on this path there may be no
|
|
194
|
+
* measurement at all. Emitting it would move the same untruth from the history
|
|
195
|
+
* row into the lifecycle record.
|
|
196
|
+
*
|
|
197
|
+
* Returns the settled facts, or `null` when there was nothing to settle. `null`
|
|
198
|
+
* means exactly ONE thing here — there was no OPEN run for this event to be
|
|
199
|
+
* about. A run that was open but whose record could not be written is not
|
|
200
|
+
* `null`: it returns the facts read off that run with `lifecycleWritten: false`,
|
|
201
|
+
* because a failed write is not an absent run, and a caller that cannot tell the
|
|
202
|
+
* two apart ends up telling the user a falsehood (see `compact-event-settle.ts`).
|
|
203
|
+
*/
|
|
204
|
+
export declare function settleOpenLifecycleRunOnCompactEvent(input: {
|
|
205
|
+
readonly projectRoot: string;
|
|
206
|
+
readonly sessionId: string;
|
|
207
|
+
readonly measuredRatio: number | null;
|
|
208
|
+
readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
|
|
209
|
+
/** Failure injection for the write below — the seam `CompactLifecyclePublisher` already takes. */
|
|
210
|
+
readonly failLifecycleWrite?: boolean | undefined;
|
|
211
|
+
}): {
|
|
212
|
+
readonly runId: string;
|
|
213
|
+
readonly triggerRatio: number;
|
|
214
|
+
readonly afterRatio: number | null;
|
|
215
|
+
/**
|
|
216
|
+
* `false` when the run could not be marked settled — `writeCompactLifecycle`
|
|
217
|
+
* threw. The three facts above were read off the OPEN run, so they stay true
|
|
218
|
+
* and a caller may still record the observation; what it must not do is report
|
|
219
|
+
* the run as settled. `settleOpenLifecycleRun` above does not suppress its
|
|
220
|
+
* returned record on a failed write either, so the two now agree that a write
|
|
221
|
+
* failure is not "nothing happened". This field exists because this function's
|
|
222
|
+
* caller, unlike the sibling's, renders a sentence about the outcome.
|
|
223
|
+
*/
|
|
224
|
+
readonly lifecycleWritten: boolean;
|
|
225
|
+
} | null;
|
|
226
|
+
/**
|
|
227
|
+
* rid `2026-09-13-compact-event-settle` (repair R1): supply the measurement a
|
|
228
|
+
* harness-settled run was left owing.
|
|
229
|
+
*
|
|
230
|
+
* The function above deliberately refuses to launder a post-compact reading
|
|
231
|
+
* that has not dropped — and right after a compaction that refusal is the
|
|
232
|
+
* NORMAL case, because the statusline still holds the pre-compact value. The
|
|
233
|
+
* run is then closed at `completed` with no `afterRatio`, so the dispatch's
|
|
234
|
+
* calibration pair never closes and "intent vs observed" stays blank for
|
|
235
|
+
* exactly the compactions this slice exists to witness. This function is what
|
|
236
|
+
* makes the function above's promise payable.
|
|
237
|
+
*
|
|
238
|
+
* WHY NOT WIDEN `settleOpenLifecycleRun`. That one re-emits `verifying` before
|
|
239
|
+
* `completed`, which on an already-`completed` record is a backwards stage
|
|
240
|
+
* transition with no observer to serve. This is not a settlement — the run IS
|
|
241
|
+
* settled; only the number is owed. So no stage is rewritten here.
|
|
242
|
+
*
|
|
243
|
+
* WRITING `afterRatio` ONTO THE RECORD IS THE IDEMPOTENCE TOKEN: every later
|
|
244
|
+
* probe finds it present and returns `null`, so however many probes follow, one
|
|
245
|
+
* compaction yields exactly one late measurement.
|
|
246
|
+
*
|
|
247
|
+
* THE DROP GATE IS THE EVENT PATH'S OWN (`measuredRatio < triggerRatio`), not
|
|
248
|
+
* the probe path's `autoFireThreshold`. `afterRatio` has to mean "below the
|
|
249
|
+
* ratio this run was dispatched at" — the rule the event path already enforces
|
|
250
|
+
* — or a run dispatched under the threshold (a forced or banded dispatch) would
|
|
251
|
+
* let a NON-drop through the one path that can still write a `completed` record.
|
|
252
|
+
* `conservative-fallback` is refused for the probe path's reason: its `0` is
|
|
253
|
+
* the absence of a measurement, not an empty context.
|
|
254
|
+
*
|
|
255
|
+
* Returns the filled record, or `null` when no run is owed a measurement.
|
|
256
|
+
*/
|
|
257
|
+
export declare function fillEventSettledMeasurement(input: {
|
|
258
|
+
readonly projectRoot: string;
|
|
259
|
+
readonly sessionId: string;
|
|
260
|
+
readonly measuredRatio: number;
|
|
261
|
+
readonly source: string;
|
|
262
|
+
}): {
|
|
263
|
+
readonly runId: string;
|
|
264
|
+
readonly triggerRatio: number;
|
|
265
|
+
readonly afterRatio: number;
|
|
266
|
+
} | null;
|
|
148
267
|
export {};
|