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
|
@@ -7,22 +7,34 @@
|
|
|
7
7
|
* gates plus the verify-pipeline, so the orchestrator no longer hand-writes
|
|
8
8
|
* them per mechanical file-split (or similar) slice.
|
|
9
9
|
*
|
|
10
|
-
* The content markers emitted here are the exact strings the CLI gates
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `src/services/workflow/
|
|
16
|
-
*
|
|
10
|
+
* The content markers emitted here are the exact strings the CLI gates
|
|
11
|
+
* mechanically check — see `src/services/artifacts/artifact-prerequisites.ts`
|
|
12
|
+
* (mustContain / mustContainAny / headingMustContain — the authoritative
|
|
13
|
+
* table). `peaks workflow verify-pipeline` reads that same table for both the
|
|
14
|
+
* evidence paths it probes and the markers it enforces
|
|
15
|
+
* (`src/services/workflow/pipeline-verify-gate-support.ts#contractEvidencePaths`).
|
|
16
|
+
* Do NOT reword the marker lines.
|
|
17
|
+
*
|
|
18
|
+
* This paragraph used to send editors to `src/services/workflow/artifact-paths.ts`
|
|
19
|
+
* as the checker of "suffixed security/performance findings". That is no longer
|
|
20
|
+
* true: the `security-findings-<rid>.md` / `performance-findings-<rid>.md` gates
|
|
21
|
+
* were removed in rid `2026-09-14-verify-pipeline-contract-drift` (no
|
|
22
|
+
* `qa:verdict-issued` table names those paths; security and perf evidence is
|
|
23
|
+
* resolved on the RD side at `audit/security-<rid>.md` / `audit/perf-<rid>.md`),
|
|
24
|
+
* and `artifact-paths.ts` has had zero code consumers since. It is not a marker
|
|
25
|
+
* authority, so it is not a pointer worth keeping.
|
|
17
26
|
*
|
|
18
27
|
* Karpathy §2 (Simplicity First): a single generator function + a handful of
|
|
19
28
|
* pure body builders. No speculative options, no validation beyond what the
|
|
20
29
|
* reference prototype performs.
|
|
21
30
|
*/
|
|
22
|
-
import { createHash } from 'node:crypto';
|
|
23
31
|
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
24
32
|
import { join } from 'node:path';
|
|
33
|
+
import { serializeHandoffFrontmatter } from '../prd/handoff-frontmatter.js';
|
|
34
|
+
import { handoffRelativePath, sha256OfBody } from '../prd/handoff-service.js';
|
|
25
35
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
36
|
+
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
37
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
26
38
|
/** Comma/space tolerant `--files` splitter. */
|
|
27
39
|
export function parseFiles(raw) {
|
|
28
40
|
return raw
|
|
@@ -235,40 +247,20 @@ function buildQaRequest(rid, sid, files) {
|
|
|
235
247
|
`;
|
|
236
248
|
}
|
|
237
249
|
/**
|
|
238
|
-
* Build the `prd/handoff
|
|
239
|
-
*
|
|
240
|
-
* `
|
|
241
|
-
*
|
|
250
|
+
* Build the `prd/handoff-<rid>.md` frontmatter + body. The frontmatter comes from the
|
|
251
|
+
* ONE canonical serializer and `handoffHash === sha256(body)` — the same
|
|
252
|
+
* pairing `handoff-service.initHandoff` and `handoff-auto-regen.ts` use, so
|
|
253
|
+
* this producer cannot drift from them.
|
|
254
|
+
*
|
|
255
|
+
* This function used to hand-roll a third, divergent frontmatter: a
|
|
256
|
+
* `handoffHash: sha256:<hex>` value with **no line beginning `sha256:`**, over
|
|
257
|
+
* a hash computed on `frontmatter + body` rather than the body. The
|
|
258
|
+
* `AUDIT_REQUIRES_HANDOFF` gate (a substring check) accepted that file while
|
|
259
|
+
* `readAndVerifyHandoff` in both audit skills returned `null` — the very
|
|
260
|
+
* producer/consumer divergence rid `2026-09-14-handoff-writer-gate-divergence`
|
|
261
|
+
* exists to remove, surviving in a function the same slice edited.
|
|
242
262
|
*/
|
|
243
263
|
function buildHandoff(rid, sid, title, files, lineCounts) {
|
|
244
|
-
const bulletList = files.map((f) => ` - ${f}`).join('\n');
|
|
245
|
-
const frontmatter = `---
|
|
246
|
-
requestId: ${rid}
|
|
247
|
-
scope:
|
|
248
|
-
${bulletList}
|
|
249
|
-
files:
|
|
250
|
-
${bulletList}
|
|
251
|
-
handoffPath: .peaks/_runtime/${sid}/prd/handoff.md
|
|
252
|
-
handoffHash: sha256:PLACEHOLDER
|
|
253
|
-
decisions:
|
|
254
|
-
- id: D1
|
|
255
|
-
summary: "Mechanical verbatim module split"
|
|
256
|
-
rationale: "Satisfy the 800-line file-size gate."
|
|
257
|
-
risks:
|
|
258
|
-
- id: R1
|
|
259
|
-
description: "Public exports may be imported elsewhere"
|
|
260
|
-
mitigation: "Import sites updated; tsc clean."
|
|
261
|
-
nextActions:
|
|
262
|
-
- "peaks-qa validates the regression matrix"
|
|
263
|
-
gateEvidence:
|
|
264
|
-
projectScan: .peaks/project-scan/project-scan.md
|
|
265
|
-
prdHandoff: .peaks/_runtime/${sid}/prd/handoff.md
|
|
266
|
-
codeReview: .peaks/_runtime/${sid}/rd/code-review.md
|
|
267
|
-
securityReview: .peaks/_runtime/${sid}/rd/security-review.md
|
|
268
|
-
perfBaseline: .peaks/_runtime/${sid}/audit/perf.md
|
|
269
|
-
schemaVersion: 2
|
|
270
|
-
---
|
|
271
|
-
`;
|
|
272
264
|
const body = `# PRD Handoff — ${rid}
|
|
273
265
|
|
|
274
266
|
${title}. Mechanical verbatim module split; behavior-preserving.
|
|
@@ -279,10 +271,19 @@ ${filesMd(files)}
|
|
|
279
271
|
Line counts:
|
|
280
272
|
${lineCountsMd(lineCounts)}
|
|
281
273
|
`;
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
const handoffHash = sha256OfBody(body);
|
|
275
|
+
const frontmatter = {
|
|
276
|
+
requestId: rid,
|
|
277
|
+
sessionId: sid,
|
|
278
|
+
schemaVersion: '2',
|
|
279
|
+
handoffHash,
|
|
280
|
+
writtenAt: new Date().toISOString(),
|
|
281
|
+
goals: [],
|
|
282
|
+
acceptanceCriteria: [],
|
|
283
|
+
preservedBehavior: [],
|
|
284
|
+
handoffPath: handoffRelativePath(sid, rid)
|
|
285
|
+
};
|
|
286
|
+
return { content: `${serializeHandoffFrontmatter(frontmatter)}${body}`, hash: handoffHash };
|
|
286
287
|
}
|
|
287
288
|
/** Find the existing numbered QA request file for `rid`, else the default. */
|
|
288
289
|
async function resolveQaRequestPath(qaDir, rid) {
|
|
@@ -300,6 +301,21 @@ async function resolveQaRequestPath(qaDir, rid) {
|
|
|
300
301
|
}
|
|
301
302
|
export async function generateEvidence(options) {
|
|
302
303
|
const { projectRoot, rid, title, files, lineCounts, sessionId } = options;
|
|
304
|
+
// Both values become path segments below — the rid as a filename, the sid as
|
|
305
|
+
// the session directory. Guard them BEFORE the first mkdir so a rejected run
|
|
306
|
+
// leaves nothing behind. `REQUEST_ID_PATTERN` is the repo's own request-id
|
|
307
|
+
// control (`request-artifact-service.ts`, F-1 slice 025 security); the sid
|
|
308
|
+
// gets the segment check because `--session-id` has no pinned format.
|
|
309
|
+
// Measured before these lines existed: `--rid '../../../pwned'` wrote
|
|
310
|
+
// `.peaks/_runtime/pwned.md`, outside the per-session evidence dir, and a
|
|
311
|
+
// deeper rid wrote above the project root with the string echoed into the
|
|
312
|
+
// artifact body.
|
|
313
|
+
if (!REQUEST_ID_PATTERN.test(rid)) {
|
|
314
|
+
throw new Error(`Invalid request id: ${rid} (expected letters, digits, dots, underscores, or dashes)`);
|
|
315
|
+
}
|
|
316
|
+
if (isUnsafePathInput(sessionId)) {
|
|
317
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
318
|
+
}
|
|
303
319
|
const sessionRoot = getSessionDir(projectRoot, sessionId);
|
|
304
320
|
const rdDir = join(sessionRoot, 'rd');
|
|
305
321
|
const qaDir = join(sessionRoot, 'qa');
|
|
@@ -319,18 +335,39 @@ export async function generateEvidence(options) {
|
|
|
319
335
|
}
|
|
320
336
|
const qaRequestPath = await resolveQaRequestPath(qaDir, rid);
|
|
321
337
|
const handoff = buildHandoff(rid, sessionId, title, files, lineCounts);
|
|
338
|
+
// Four of these twelve paths carry the rid because the `rd:qa-handoff` gate
|
|
339
|
+
// requires `<rid>` in them: every slice in a session shares `rd/` and
|
|
340
|
+
// `audit/`, so writing the ridless name silently destroyed the previous
|
|
341
|
+
// slice's evidence on 2026-09-13 while the gate stayed green (slice
|
|
342
|
+
// `2026-09-14-audit-artifact-rid-scoping`).
|
|
322
343
|
const writes = [
|
|
323
|
-
[join(rdDir,
|
|
344
|
+
[join(rdDir, `code-review-${rid}.md`), buildCodeReview(rid, title, files, lineCounts)],
|
|
345
|
+
[join(auditDir, `security-${rid}.md`), buildSecurityReview(rid)],
|
|
346
|
+
// ...but the `config` type is the one whose `rd:qa-handoff` row is
|
|
347
|
+
// `SECURITY_REVIEW` — the genuinely ridless `rd/security-review.md` — and
|
|
348
|
+
// this generator is request-type-agnostic (no `--request-type`). So it
|
|
349
|
+
// writes BOTH names: the rid-scoped one the fanout types resolve
|
|
350
|
+
// (`AUDIT_SECURITY`) and the bare one `config` resolves. Nothing is lost
|
|
351
|
+
// by having both; a `config` slice has nothing else to fall back on, and
|
|
352
|
+
// dropping this write made a `config` slice fail its own gate while the
|
|
353
|
+
// generator reported success (repair round: measured `missing:
|
|
354
|
+
// ['rd/security-review.md']` with the real generator and the real gate).
|
|
324
355
|
[join(rdDir, 'security-review.md'), buildSecurityReview(rid)],
|
|
325
|
-
[join(rdDir,
|
|
356
|
+
[join(rdDir, `karpathy-review-${rid}.md`), buildKarpathyReview(rid, lineCounts)],
|
|
357
|
+
// `rd/tech-doc.md` stays ridless on purpose: the TECH_DOC prereq was
|
|
358
|
+
// removed in v2.11.0, so nothing gates it and there is no rid-scoped
|
|
359
|
+
// sibling that anything reads.
|
|
326
360
|
[join(rdDir, 'tech-doc.md'), buildTechDoc(rid, title, files, lineCounts)],
|
|
327
|
-
[join(auditDir,
|
|
361
|
+
[join(auditDir, `perf-${rid}.md`), buildPerfAudit(rid)],
|
|
328
362
|
[join(qaDir, 'test-cases', `${rid}.md`), buildTestCases(rid)],
|
|
329
363
|
[join(qaDir, 'test-reports', `${rid}.md`), buildTestReport(rid)],
|
|
330
364
|
[join(qaDir, `security-findings-${rid}.md`), buildSecurityFindings(rid)],
|
|
331
365
|
[join(qaDir, `performance-findings-${rid}.md`), buildPerformanceFindings(rid)],
|
|
332
366
|
[qaRequestPath, buildQaRequest(rid, sessionId, files)],
|
|
333
|
-
|
|
367
|
+
// The handoff capsule carries the rid for the same reason as the four
|
|
368
|
+
// above (slice `2026-09-14-prd-capsule-rid-scoping`): one slot per
|
|
369
|
+
// session means the second slice's capsule overwrites the first's.
|
|
370
|
+
[join(prdDir, `handoff-${rid}.md`), handoff.content]
|
|
334
371
|
];
|
|
335
372
|
const writtenFiles = [];
|
|
336
373
|
for (const [path, content] of writes) {
|
|
@@ -341,7 +378,7 @@ export async function generateEvidence(options) {
|
|
|
341
378
|
rid,
|
|
342
379
|
sessionId,
|
|
343
380
|
sessionRoot,
|
|
344
|
-
handoffPath: join(prdDir,
|
|
381
|
+
handoffPath: join(prdDir, `handoff-${rid}.md`),
|
|
345
382
|
handoffHash: handoff.hash,
|
|
346
383
|
writtenFiles,
|
|
347
384
|
createdDirectories
|
|
@@ -252,6 +252,15 @@ interface EvidenceSource {
|
|
|
252
252
|
readonly label: string;
|
|
253
253
|
/** Path segments under `.peaks/_runtime/<sessionId>/`. */
|
|
254
254
|
readonly segments: readonly string[];
|
|
255
|
+
/**
|
|
256
|
+
* An older location of the SAME artifact, tried only when `segments` is not
|
|
257
|
+
* on disk. Slice `2026-09-14-prd-capsule-rid-scoping` moved the PRD handoff
|
|
258
|
+
* capsule to `prd/handoff-<rid>.md`; sessions written before it hold only
|
|
259
|
+
* the bare `prd/handoff.md`, and this module's delivery gate keys on that
|
|
260
|
+
* source — so a source that goes missing does not fail the gate, it stops
|
|
261
|
+
* it (`enforceScopeContractDelivery` returns early on `missing`).
|
|
262
|
+
*/
|
|
263
|
+
readonly legacySegments?: readonly string[];
|
|
255
264
|
/** Dimensions this source can supply evidence for. */
|
|
256
265
|
readonly supports: readonly DimensionKind[];
|
|
257
266
|
/** What DELIVERED means for this source. See `isDelivered()` — every source
|
|
@@ -474,7 +474,10 @@ function evidenceSourcesFor(rid, prePostDiffAvailable) {
|
|
|
474
474
|
{
|
|
475
475
|
key: SCOPE_CONTRACT_SOURCE_KEY,
|
|
476
476
|
label: 'PRD handoff (approved scope + non-goals)',
|
|
477
|
-
|
|
477
|
+
// One capsule per slice since `2026-09-14-prd-capsule-rid-scoping`; the
|
|
478
|
+
// bare name is the pre-scoping tier and still lives on 3 sessions.
|
|
479
|
+
segments: ['prd', `handoff-${rid}.md`],
|
|
480
|
+
legacySegments: ['prd', 'handoff.md'],
|
|
478
481
|
supports: ['functional-completeness', 'existing-functionality-intact'],
|
|
479
482
|
delivery: { kind: 'whole' }
|
|
480
483
|
}
|
|
@@ -508,22 +511,43 @@ function classifyReadFailure(error) {
|
|
|
508
511
|
function readEvidence(projectRoot, sessionId, rid, prePostDiffAvailable) {
|
|
509
512
|
const runtimeRoot = join(projectRoot, '.peaks', '_runtime', sessionId);
|
|
510
513
|
return evidenceSourcesFor(rid, prePostDiffAvailable).map(source => {
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
// The canonical location first; an older one is tried only when the
|
|
515
|
+
// canonical file is genuinely ABSENT (see `legacySegments`). A file that
|
|
516
|
+
// is present but unreadable stops the walk — falling through to an older
|
|
517
|
+
// copy would silently swap the evidence this run reports.
|
|
518
|
+
const attempts = [
|
|
519
|
+
source.segments,
|
|
520
|
+
...(source.legacySegments !== undefined ? [source.legacySegments] : [])
|
|
521
|
+
];
|
|
522
|
+
// The path reported when nothing resolved stays the CANONICAL one, so the
|
|
523
|
+
// operator is sent to where the artifact belongs, not to its old home.
|
|
524
|
+
let resolved = source.segments;
|
|
513
525
|
let raw = null;
|
|
514
526
|
let error = '';
|
|
515
|
-
let read = '
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
527
|
+
let read = 'missing';
|
|
528
|
+
for (const segments of attempts) {
|
|
529
|
+
try {
|
|
530
|
+
raw = readFileSync(join(runtimeRoot, ...segments));
|
|
531
|
+
read = 'ok';
|
|
532
|
+
resolved = segments;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
read = classifyReadFailure(err);
|
|
537
|
+
error = err instanceof Error ? err.message : String(err);
|
|
538
|
+
// A file that exists but cannot be read IS this source's file, so it
|
|
539
|
+
// is also the path the report must name — walking on to an older copy
|
|
540
|
+
// would silently swap the evidence this run reports.
|
|
541
|
+
if (read === 'unreadable') {
|
|
542
|
+
resolved = segments;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
522
546
|
}
|
|
523
547
|
return {
|
|
524
548
|
source,
|
|
525
|
-
relativePath,
|
|
526
|
-
absolutePath,
|
|
549
|
+
relativePath: ['.peaks', '_runtime', sessionId, ...resolved].join('/'),
|
|
550
|
+
absolutePath: join(runtimeRoot, ...resolved),
|
|
527
551
|
raw,
|
|
528
552
|
read,
|
|
529
553
|
error,
|
|
@@ -15,6 +15,25 @@ export declare function getAdapter(ide: IdeId): IdeAdapter;
|
|
|
15
15
|
export declare function tryGetAdapter(ide: string): IdeAdapter | undefined;
|
|
16
16
|
/** All registered adapter ids (insertion order). */
|
|
17
17
|
export declare function listAdapterIds(): readonly IdeId[];
|
|
18
|
+
/**
|
|
19
|
+
* Help text for every `--ide <id>` option, derived from the registry.
|
|
20
|
+
*
|
|
21
|
+
* It used to be a hand-written literal — `"target adapter id (claude-code |
|
|
22
|
+
* trae); default: auto-detect from env/cwd"` — copy-pasted into six option
|
|
23
|
+
* declarations across `hooks-commands.ts` and `statusline-commands.ts`. By the
|
|
24
|
+
* time anyone read it the registry had nine adapters, so the help named two of
|
|
25
|
+
* the nine values the option accepts and stayed silent about the other seven.
|
|
26
|
+
* A help string that enumerates a set it does not own cannot be kept true by
|
|
27
|
+
* discipline; deriving it here is the only form that cannot drift.
|
|
28
|
+
*
|
|
29
|
+
* Note for whoever is tempted to point the help at `peaks adapter list`
|
|
30
|
+
* instead: that command lists USER-REGISTERED vendor adapters from
|
|
31
|
+
* `.peaks/runtime/adapters.json` (a different registry, empty on a fresh
|
|
32
|
+
* project). It is not this set. The nine ids below have no CLI surface of
|
|
33
|
+
* their own; `peaks ide model --current` is the closest read-only viewer and
|
|
34
|
+
* it reports one adapter, not the list.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveIdeOptionHelp(): string;
|
|
18
37
|
/** All registered adapters (insertion order). */
|
|
19
38
|
export declare function listAdapters(): readonly IdeAdapter[];
|
|
20
39
|
/**
|
|
@@ -57,6 +57,27 @@ export function tryGetAdapter(ide) {
|
|
|
57
57
|
export function listAdapterIds() {
|
|
58
58
|
return Array.from(ADAPTERS.keys());
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Help text for every `--ide <id>` option, derived from the registry.
|
|
62
|
+
*
|
|
63
|
+
* It used to be a hand-written literal — `"target adapter id (claude-code |
|
|
64
|
+
* trae); default: auto-detect from env/cwd"` — copy-pasted into six option
|
|
65
|
+
* declarations across `hooks-commands.ts` and `statusline-commands.ts`. By the
|
|
66
|
+
* time anyone read it the registry had nine adapters, so the help named two of
|
|
67
|
+
* the nine values the option accepts and stayed silent about the other seven.
|
|
68
|
+
* A help string that enumerates a set it does not own cannot be kept true by
|
|
69
|
+
* discipline; deriving it here is the only form that cannot drift.
|
|
70
|
+
*
|
|
71
|
+
* Note for whoever is tempted to point the help at `peaks adapter list`
|
|
72
|
+
* instead: that command lists USER-REGISTERED vendor adapters from
|
|
73
|
+
* `.peaks/runtime/adapters.json` (a different registry, empty on a fresh
|
|
74
|
+
* project). It is not this set. The nine ids below have no CLI surface of
|
|
75
|
+
* their own; `peaks ide model --current` is the closest read-only viewer and
|
|
76
|
+
* it reports one adapter, not the list.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveIdeOptionHelp() {
|
|
79
|
+
return `target adapter id (${listAdapterIds().join(' | ')}); default: auto-detect from env/cwd`;
|
|
80
|
+
}
|
|
60
81
|
/** All registered adapters (insertion order). */
|
|
61
82
|
export function listAdapters() {
|
|
62
83
|
return Array.from(ADAPTERS.values());
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { JobStateSchema } from './job-types.js';
|
|
4
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
4
5
|
export class JobStateStore {
|
|
5
6
|
rootDir;
|
|
6
7
|
constructor(rootDir) {
|
|
7
8
|
this.rootDir = rootDir;
|
|
8
9
|
}
|
|
9
10
|
jobDir(jobId) {
|
|
11
|
+
// JobId axis. This is the only join the store performs, so one guard here
|
|
12
|
+
// covers every `peaks job *` subcommand — ten CLI call sites hand the
|
|
13
|
+
// store a caller-supplied `--job-id` and none of them checked it.
|
|
14
|
+
if (isUnsafePathInput(jobId)) {
|
|
15
|
+
throw new Error(`Invalid job id: ${jobId} (must be a single path segment)`);
|
|
16
|
+
}
|
|
10
17
|
return join(this.rootDir, jobId);
|
|
11
18
|
}
|
|
12
19
|
init(input) {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
28
29
|
/** Compute the child-side path where the artifact should be mirrored.
|
|
29
30
|
* Mirrors the parent shape exactly: `.peaks/_runtime/<sid>/<role>/`. */
|
|
30
31
|
function childArtifactPath(childRoot, sid, role, sourcePath) {
|
|
@@ -37,6 +38,16 @@ function ensureDirFor(file) {
|
|
|
37
38
|
export function dispatchArtifact(opts) {
|
|
38
39
|
const warnings = [];
|
|
39
40
|
const perChild = [];
|
|
41
|
+
// `sid` becomes a path segment in every child (`childArtifactPath`). It has no
|
|
42
|
+
// pinned format (`--sid` is a free-form override), so it gets the repo's
|
|
43
|
+
// segment check — the same `isUnsafePathInput` `peaks evidence generate` and
|
|
44
|
+
// `peaks verdict aggregate` apply to their session id. Guard before the first
|
|
45
|
+
// mkdir so a rejected dispatch leaves nothing behind. Measured before this
|
|
46
|
+
// line existed: `--sid '../../../../../../PWNED-SID-ESCAPE'` wrote
|
|
47
|
+
// `<parent-of-fixture>/PWNED-SID-ESCAPE/prd/src.md`, above every project root.
|
|
48
|
+
if (isUnsafePathInput(opts.sid)) {
|
|
49
|
+
throw new Error(`Invalid session id: ${opts.sid} (must be a single path segment)`);
|
|
50
|
+
}
|
|
40
51
|
// Validate targets against the manifest.
|
|
41
52
|
const knownIds = new Set(opts.manifest.children.map((c) => c.id));
|
|
42
53
|
const validTargets = [];
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* v2.13.2 AC-4 — prd/handoff
|
|
2
|
+
* v2.13.2 AC-4 — prd/handoff-<rid>.md auto-regen on prd:handed-off.
|
|
3
3
|
*
|
|
4
4
|
* When `peaks request transition --role prd --state handed-off` succeeds
|
|
5
|
-
* and
|
|
5
|
+
* and this slice's capsule is missing, this helper writes a sha256-locked
|
|
6
6
|
* handoff (schemaVersion: 2) using the request artifact body as the
|
|
7
|
-
* handoff body. If the
|
|
7
|
+
* handoff body. If the capsule already exists, it's NOT overwritten —
|
|
8
8
|
* the existing handoff is canonical (it may carry a richer body that
|
|
9
9
|
* peaks-prd produced in an earlier session).
|
|
10
10
|
*
|
|
11
|
+
* Slice `2026-09-14-prd-capsule-rid-scoping`: the path carries the rid, so
|
|
12
|
+
* "already exists" is now asked per SLICE. The pre-rid-scoping bare name is
|
|
13
|
+
* deliberately NOT consulted — writing there would recreate the
|
|
14
|
+
* one-slot-per-session collision for the next slice in the session.
|
|
15
|
+
*
|
|
11
16
|
* Karpathy §3 (Surgical Changes): this file only owns the auto-regen
|
|
12
17
|
* path. The other 11 transitions are untouched.
|
|
13
18
|
*/
|
|
@@ -15,7 +20,8 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
15
20
|
import { dirname, join } from 'node:path';
|
|
16
21
|
import { createHash } from 'node:crypto';
|
|
17
22
|
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
18
|
-
import {
|
|
23
|
+
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
24
|
+
import { handoffRelativePath, sha256OfBody } from './handoff-service.js';
|
|
19
25
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
20
26
|
/**
|
|
21
27
|
* Auto-regen the prd/handoff.md under `.peaks/_runtime/<sid>/prd/`.
|
|
@@ -25,7 +31,7 @@ export async function autoRegenPrdHandoff(opts) {
|
|
|
25
31
|
if (opts.role !== 'prd') {
|
|
26
32
|
return { status: 'failed', reason: 'role must be prd' };
|
|
27
33
|
}
|
|
28
|
-
const handoffPath = join(opts.projectRoot,
|
|
34
|
+
const handoffPath = join(opts.projectRoot, handoffRelativePath(opts.sessionId, opts.requestId));
|
|
29
35
|
if (existsSync(handoffPath)) {
|
|
30
36
|
return { status: 'skipped-exists', path: handoffPath };
|
|
31
37
|
}
|
|
@@ -41,28 +47,26 @@ export async function autoRegenPrdHandoff(opts) {
|
|
|
41
47
|
const body = artifact.content;
|
|
42
48
|
const sha256 = sha256OfBody(body);
|
|
43
49
|
// v2.13.3 AC-4 — align with `AUDIT_REQUIRES_HANDOFF` prereq which
|
|
44
|
-
// pins `mustContain: ['schemaVersion: 2', 'sha256:']`.
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
].join('\n');
|
|
65
|
-
const content = `${frontmatter}${body}`;
|
|
50
|
+
// pins `mustContain: ['schemaVersion: 2', 'sha256:']`. Primary field is
|
|
51
|
+
// `sha256`; `handoffHash` is kept as a literal alias for the readers that
|
|
52
|
+
// still use the old key.
|
|
53
|
+
//
|
|
54
|
+
// Slice `2026-09-14-handoff-writer-gate-divergence`: this block used to be
|
|
55
|
+
// hand-rolled here while `handoff-service.serializeHandoff` rendered the
|
|
56
|
+
// SAME contract a second, incompatible way. Both now go through
|
|
57
|
+
// `serializeHandoffFrontmatter`, so the two producers cannot drift again.
|
|
58
|
+
const frontmatter = {
|
|
59
|
+
requestId: opts.requestId,
|
|
60
|
+
sessionId: opts.sessionId,
|
|
61
|
+
schemaVersion: '2',
|
|
62
|
+
handoffHash: sha256,
|
|
63
|
+
writtenAt: new Date().toISOString(),
|
|
64
|
+
goals: [],
|
|
65
|
+
acceptanceCriteria: [],
|
|
66
|
+
preservedBehavior: [],
|
|
67
|
+
handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, '')
|
|
68
|
+
};
|
|
69
|
+
const content = `${serializeHandoffFrontmatter(frontmatter)}${body}`;
|
|
66
70
|
mkdirSync(dirname(handoffPath), { recursive: true });
|
|
67
71
|
writeFileSync(handoffPath, content, 'utf8');
|
|
68
72
|
const recomputed = createHash('sha256').update(body, 'utf8').digest('hex');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE canonical serialization of `prd/handoff.md` frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists (slice `2026-09-14-handoff-writer-gate-divergence`):
|
|
5
|
+
* the handoff capsule is written by two producers and read by four
|
|
6
|
+
* consumers, and they did not agree on the bytes:
|
|
7
|
+
*
|
|
8
|
+
* | consumer / producer | requirement |
|
|
9
|
+
* |---|---|
|
|
10
|
+
* | `AUDIT_REQUIRES_HANDOFF` (`artifact-prerequisites.ts`) | SUBSTRING `schemaVersion: 2` (unquoted) + `sha256:` |
|
|
11
|
+
* | `audit-independent/{security,perf}-audit-service.ts` | anchored `^schemaVersion:\s*(\d+)\s*$` and `^sha256:\s*([a-f0-9]{64})\s*$` |
|
|
12
|
+
* | `handoff-service.readHandoff` | YAML string `handoffHash` |
|
|
13
|
+
* | `handoff-service.verifyHandoff` | `handoffHash` === sha256(body), bare hex |
|
|
14
|
+
*
|
|
15
|
+
* `handoff-service.serializeHandoff` fed the frontmatter through
|
|
16
|
+
* `yaml.stringify`, which emits `schemaVersion: "2"` and a bare
|
|
17
|
+
* `handoffHash:` — failing the gate AND both loaders. `handoff-auto-regen.ts`
|
|
18
|
+
* hand-rolled a second serialization that happened to satisfy all four. Two
|
|
19
|
+
* producers, two facts about the same contract, one of them wrong: that is
|
|
20
|
+
* the defect, and copying the right shape into a third place would keep it.
|
|
21
|
+
*
|
|
22
|
+
* So both producers call THIS function. `sha256` is emitted only here, and
|
|
23
|
+
* it is emitted plain, because the two audit loaders anchor a regex on it.
|
|
24
|
+
*
|
|
25
|
+
* Scalar-quoting rule — one rule, one documented exception set:
|
|
26
|
+
* - every ordinary string is emitted through `yamlScalar` (a JSON
|
|
27
|
+
* double-quoted scalar, which is valid YAML 1.2 and round-trips
|
|
28
|
+
* backslashes correctly on Windows paths);
|
|
29
|
+
* - `schemaVersion` and `sha256` are the exception: emitted PLAIN, because
|
|
30
|
+
* they are the two scalars the gate and the loaders match textually and a
|
|
31
|
+
* quoted scalar is invisible to all three. Both values are structurally
|
|
32
|
+
* constrained (a version digit and 64 hex chars), so plain is unambiguous.
|
|
33
|
+
*
|
|
34
|
+
* `handoffHash` is quoted even though it is also a 64-hex value: the reader
|
|
35
|
+
* parses the block through YAML and requires a STRING, and a bare all-digit
|
|
36
|
+
* sha256 would parse as a YAML number and be refused by the shape check.
|
|
37
|
+
*/
|
|
38
|
+
import type { HandoffFrontmatter } from './handoff-types.js';
|
|
39
|
+
/**
|
|
40
|
+
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
41
|
+
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
42
|
+
* the sha256 of the body independent of how the frontmatter renders.
|
|
43
|
+
*/
|
|
44
|
+
export declare function serializeHandoffFrontmatter(frontmatter: HandoffFrontmatter): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE canonical serialization of `prd/handoff.md` frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists (slice `2026-09-14-handoff-writer-gate-divergence`):
|
|
5
|
+
* the handoff capsule is written by two producers and read by four
|
|
6
|
+
* consumers, and they did not agree on the bytes:
|
|
7
|
+
*
|
|
8
|
+
* | consumer / producer | requirement |
|
|
9
|
+
* |---|---|
|
|
10
|
+
* | `AUDIT_REQUIRES_HANDOFF` (`artifact-prerequisites.ts`) | SUBSTRING `schemaVersion: 2` (unquoted) + `sha256:` |
|
|
11
|
+
* | `audit-independent/{security,perf}-audit-service.ts` | anchored `^schemaVersion:\s*(\d+)\s*$` and `^sha256:\s*([a-f0-9]{64})\s*$` |
|
|
12
|
+
* | `handoff-service.readHandoff` | YAML string `handoffHash` |
|
|
13
|
+
* | `handoff-service.verifyHandoff` | `handoffHash` === sha256(body), bare hex |
|
|
14
|
+
*
|
|
15
|
+
* `handoff-service.serializeHandoff` fed the frontmatter through
|
|
16
|
+
* `yaml.stringify`, which emits `schemaVersion: "2"` and a bare
|
|
17
|
+
* `handoffHash:` — failing the gate AND both loaders. `handoff-auto-regen.ts`
|
|
18
|
+
* hand-rolled a second serialization that happened to satisfy all four. Two
|
|
19
|
+
* producers, two facts about the same contract, one of them wrong: that is
|
|
20
|
+
* the defect, and copying the right shape into a third place would keep it.
|
|
21
|
+
*
|
|
22
|
+
* So both producers call THIS function. `sha256` is emitted only here, and
|
|
23
|
+
* it is emitted plain, because the two audit loaders anchor a regex on it.
|
|
24
|
+
*
|
|
25
|
+
* Scalar-quoting rule — one rule, one documented exception set:
|
|
26
|
+
* - every ordinary string is emitted through `yamlScalar` (a JSON
|
|
27
|
+
* double-quoted scalar, which is valid YAML 1.2 and round-trips
|
|
28
|
+
* backslashes correctly on Windows paths);
|
|
29
|
+
* - `schemaVersion` and `sha256` are the exception: emitted PLAIN, because
|
|
30
|
+
* they are the two scalars the gate and the loaders match textually and a
|
|
31
|
+
* quoted scalar is invisible to all three. Both values are structurally
|
|
32
|
+
* constrained (a version digit and 64 hex chars), so plain is unambiguous.
|
|
33
|
+
*
|
|
34
|
+
* `handoffHash` is quoted even though it is also a 64-hex value: the reader
|
|
35
|
+
* parses the block through YAML and requires a STRING, and a bare all-digit
|
|
36
|
+
* sha256 would parse as a YAML number and be refused by the shape check.
|
|
37
|
+
*/
|
|
38
|
+
/** Render a string as a YAML double-quoted scalar. JSON string escapes are a
|
|
39
|
+
* subset of YAML 1.2's double-quoted escapes, so this is valid YAML and
|
|
40
|
+
* handles `\` (Windows paths), quotes, colons and newlines in one step. */
|
|
41
|
+
function yamlScalar(value) {
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
/** Render `key: []` or a YAML block sequence of quoted scalars. */
|
|
45
|
+
function blockSequence(key, values) {
|
|
46
|
+
if (values.length === 0)
|
|
47
|
+
return [`${key}: []`];
|
|
48
|
+
return [`${key}:`, ...values.map((value) => ` - ${yamlScalar(value)}`)];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
52
|
+
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
53
|
+
* the sha256 of the body independent of how the frontmatter renders.
|
|
54
|
+
*/
|
|
55
|
+
export function serializeHandoffFrontmatter(frontmatter) {
|
|
56
|
+
const lines = [
|
|
57
|
+
'---',
|
|
58
|
+
`requestId: ${yamlScalar(frontmatter.requestId)}`,
|
|
59
|
+
`sessionId: ${yamlScalar(frontmatter.sessionId)}`,
|
|
60
|
+
// PLAIN by construction (`HandoffSchemaVersion` is the literal '2').
|
|
61
|
+
`schemaVersion: ${frontmatter.schemaVersion}`,
|
|
62
|
+
// PLAIN: this is the field both audit loaders read, via an anchored
|
|
63
|
+
// regex that a quoted scalar would not match.
|
|
64
|
+
`sha256: ${frontmatter.handoffHash}`,
|
|
65
|
+
// Quoted: `readHandoff` requires a YAML string here.
|
|
66
|
+
`handoffHash: ${yamlScalar(frontmatter.handoffHash)}`,
|
|
67
|
+
`writtenAt: ${yamlScalar(frontmatter.writtenAt)}`,
|
|
68
|
+
...blockSequence('goals', frontmatter.goals),
|
|
69
|
+
...blockSequence('acceptanceCriteria', frontmatter.acceptanceCriteria),
|
|
70
|
+
...blockSequence('preservedBehavior', frontmatter.preservedBehavior),
|
|
71
|
+
`handoffPath: ${yamlScalar(frontmatter.handoffPath)}`,
|
|
72
|
+
'---',
|
|
73
|
+
];
|
|
74
|
+
return `${lines.join('\n')}\n`;
|
|
75
|
+
}
|