peaks-loop 4.0.46 → 4.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +139 -16
- package/dist/cli/commands/compact-command.js +241 -1
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
- package/dist/cli/commands/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +14 -5
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/loop-eval-commands.js +15 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +14 -1
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +14 -3
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/slice-integrate-commands.js +5 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/test-commands.js +2 -1
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +10 -3
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +130 -65
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +18 -8
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +130 -1
- package/dist/services/code/auto-compact-lifecycle.js +180 -4
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +166 -34
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +75 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/final-review/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +31 -0
- package/dist/services/ide/ide-registry.js +35 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/best-practice-auto-trigger.js +1 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +81 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +9 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +14 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +99 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +91 -14
- package/dist/services/skills/session-start-hook-constants.d.ts +86 -0
- package/dist/services/skills/session-start-hook-constants.js +86 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +31 -12
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/slice/slice-review-state.js +8 -0
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
- package/dist/services/workflow/pipeline-verify-service.js +24 -23
- package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +124 -9
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +6 -6
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +8 -6
- package/skills/bee/peaks-qa/SKILL.md +7 -7
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +10 -8
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +3 -3
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
|
@@ -26,8 +26,17 @@ const BUG_ANALYSIS = {
|
|
|
26
26
|
description: 'Bug root-cause analysis (reproduction, affected paths, fix approach, regression test plan)',
|
|
27
27
|
mustContain: ['## Root cause', '## Fix approach']
|
|
28
28
|
};
|
|
29
|
+
// Slice `2026-09-14-audit-artifact-rid-scoping`: the rid is part of the
|
|
30
|
+
// filename (`code-review-<rid>.md`). A fixed per-session path cannot hold
|
|
31
|
+
// two slices' evidence at once — the second slice's write silently
|
|
32
|
+
// destroyed the first slice's on 2026-09-13, and the gate stayed green
|
|
33
|
+
// because it only checks that *some* file is there, not whose it is.
|
|
34
|
+
// The bare path stays accepted (see `legacyRelativePath`) so sessions
|
|
35
|
+
// written before this change — and any writer still emitting it — keep
|
|
36
|
+
// passing.
|
|
29
37
|
const CODE_REVIEW = {
|
|
30
|
-
relativePath: 'rd/code-review
|
|
38
|
+
relativePath: 'rd/code-review-<rid>.md',
|
|
39
|
+
legacyRelativePath: 'rd/code-review.md',
|
|
31
40
|
description: 'Code review evidence (CRITICAL/HIGH must be fixed before handoff)',
|
|
32
41
|
mustContain: ['## Findings', 'CRITICAL']
|
|
33
42
|
};
|
|
@@ -52,23 +61,31 @@ const PERF_BASELINE = {
|
|
|
52
61
|
// are dispatched as pre-RD audit runs that consume the public PRD
|
|
53
62
|
// handoff (`prd/handoff.md`) — see `AUDIT_REQUIRES_HANDOFF` below.
|
|
54
63
|
//
|
|
55
|
-
// Back-compat: the canonical location is `audit/security
|
|
56
|
-
// `audit/perf
|
|
57
|
-
// `
|
|
58
|
-
//
|
|
64
|
+
// Back-compat: the canonical location is `audit/security-<rid>.md`
|
|
65
|
+
// (and `audit/perf-<rid>.md`); the pre-rid-scoping `audit/security.md`
|
|
66
|
+
// (and `audit/perf.md`) plus the older `rd/security-review.md` (and
|
|
67
|
+
// `rd/perf-baseline.md`) are accepted via `legacyRelativePaths`, in
|
|
68
|
+
// that order, for the back-compat window.
|
|
59
69
|
const AUDIT_SECURITY = {
|
|
60
|
-
relativePath: 'audit/security
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
relativePath: 'audit/security-<rid>.md',
|
|
71
|
+
// Two legacy tiers behind the rid-scoped path: the pre-rid-scoping
|
|
72
|
+
// v2.12.0 location, then the v2.11.x one. Both are live on disk.
|
|
73
|
+
legacyRelativePath: 'audit/security.md',
|
|
74
|
+
legacyRelativePaths: ['rd/security-review.md'],
|
|
75
|
+
description: 'Independent security audit output (peaks-security-audit skill, v2.12.0+). Replaces the v2.11.x rd/security-review.md slot for fanout-trigger request types. The rid is part of the filename so two slices in one session do not collide; the bare and rd/-prefixed legacy locations are accepted during the back-compat window via legacyRelativePaths.',
|
|
63
76
|
// New canonical path writes a "## Verdict" header on the audit
|
|
64
77
|
// envelope. The legacy rd/security-review.md writes a "## Findings"
|
|
65
78
|
// header. Both pass the gate.
|
|
66
79
|
mustContainAny: ['## Verdict', '## Findings']
|
|
67
80
|
};
|
|
68
81
|
const AUDIT_PERF = {
|
|
69
|
-
relativePath: 'audit/perf
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
relativePath: 'audit/perf-<rid>.md',
|
|
83
|
+
// Same two-tier history as AUDIT_SECURITY. This is the pair that
|
|
84
|
+
// produced the 2026-09-13 evidence: `audit/perf.md` was overwritten by
|
|
85
|
+
// the second slice and the first slice's audit is unrecoverable.
|
|
86
|
+
legacyRelativePath: 'audit/perf.md',
|
|
87
|
+
legacyRelativePaths: ['rd/perf-baseline.md'],
|
|
88
|
+
description: 'Independent perf audit output (peaks-perf-audit skill, v2.12.0+). Replaces the v2.11.x rd/perf-baseline.md slot for fanout-trigger request types. The rid is part of the filename so two slices in one session do not collide; the bare and rd/-prefixed legacy locations are accepted during the back-compat window via legacyRelativePaths.',
|
|
72
89
|
// New schema writes "## Baseline" header; the legacy schema writes
|
|
73
90
|
// "## Results". Both pass the gate, as does the explicit
|
|
74
91
|
// no-perf-surface stub (slices whose surface is purely logic /
|
|
@@ -93,6 +110,16 @@ const AUDIT_PERF = {
|
|
|
93
110
|
// failure. The hard-fail behavior for `passed: false` is preserved
|
|
94
111
|
// (the body still has to carry `"passed": true` when present).
|
|
95
112
|
// v2.14.0 will remove `backCompat` and re-elevate missing → throw.
|
|
113
|
+
//
|
|
114
|
+
// Slice `2026-09-14-audit-artifact-rid-scoping` deliberately does NOT
|
|
115
|
+
// rid-scope this path, unlike the four audit/review artifacts above. The
|
|
116
|
+
// repo's own producer/reader constant is `mutReportPath()` in
|
|
117
|
+
// `packages/peaks-loop-mut/src/services/mut/report-loader.ts`, which names
|
|
118
|
+
// `mut/mut-report.json`, and `peaks mut run`'s `--out` is supplied by the
|
|
119
|
+
// caller. A `<rid>`-templated requirement here would be a name no producer
|
|
120
|
+
// in the repo can write — a gate that exists only in prose, sitting behind
|
|
121
|
+
// `backCompat: true` so it gates nothing at all. If mut ever gains a
|
|
122
|
+
// rid-scoped producer, the constant and this path move together.
|
|
96
123
|
const MUT_REPORT = {
|
|
97
124
|
relativePath: 'mut/mut-report.json',
|
|
98
125
|
description: 'peaks-mut mutation + weak-assertion report (Plan 2, v2.12.0+). v2.13.1: required at rd:qa-handoff for feature / bugfix / refactor slices. v2.13.2: back-compat window — missing file downgrades to a warning (1-minor-release); passed:false still throws. The body must carry `"passed": true` when present; failed runs are blocked at this gate. CONFIG / DOCS / CHORE slices retain the legacy "no acceptance surface" exemption.',
|
|
@@ -105,10 +132,11 @@ const MUT_REPORT = {
|
|
|
105
132
|
backCompat: true
|
|
106
133
|
};
|
|
107
134
|
// v2.12.0 Group B Tier 5 — gate that the peaks-prd handoff (the
|
|
108
|
-
// immutable handoff capsule at `prd/handoff
|
|
135
|
+
// immutable, per-slice handoff capsule at `prd/handoff-<rid>.md`
|
|
136
|
+
// since slice `2026-09-14-prd-capsule-rid-scoping`) exists before any
|
|
109
137
|
// audit skill is allowed to consume it. The peaks-security-audit
|
|
110
|
-
// and peaks-perf-audit CLI commands read frontmatter from
|
|
111
|
-
//
|
|
138
|
+
// and peaks-perf-audit CLI commands read frontmatter from the
|
|
139
|
+
// capsule (AC-2.4 / AC-3.4); if the handoff is missing the
|
|
112
140
|
// audit skill aborts — and so should the prereq gate when those
|
|
113
141
|
// audits are required at rd:qa-handoff.
|
|
114
142
|
//
|
|
@@ -117,8 +145,21 @@ const MUT_REPORT = {
|
|
|
117
145
|
// the old form (no PRD handoff chain — config slices may run before
|
|
118
146
|
// PRD handoff exists for small CONFIG-only commits).
|
|
119
147
|
const AUDIT_REQUIRES_HANDOFF = {
|
|
120
|
-
relativePath: 'prd/handoff
|
|
121
|
-
|
|
148
|
+
relativePath: 'prd/handoff-<rid>.md',
|
|
149
|
+
// Slice `2026-09-14-prd-capsule-rid-scoping`: the rid is part of the
|
|
150
|
+
// filename, like the four audit/review artifacts above. A single slot per
|
|
151
|
+
// SESSION cannot hold two slices' capsules — `peaks prd handoff init`
|
|
152
|
+
// overwrote it on every call, and this gate stayed green because it pinned
|
|
153
|
+
// only `schemaVersion: 2` + `sha256:`, never WHOSE rid the file named.
|
|
154
|
+
// Measured on `2026-09-13-session-21878f`: a four-slice job passed this
|
|
155
|
+
// prerequisite on a capsule written for a different line of work.
|
|
156
|
+
// The bare path stays accepted (see `legacyRelativePath`) so the three
|
|
157
|
+
// sessions on disk that hold only `prd/handoff.md` keep passing. That tier
|
|
158
|
+
// is rid-blind by design — the scoping binds for capsules written from now
|
|
159
|
+
// on, and every producer (`handoff-service.initHandoff`,
|
|
160
|
+
// `handoff-auto-regen`, `evidence generate`) writes the rid-scoped name.
|
|
161
|
+
legacyRelativePath: 'prd/handoff.md',
|
|
162
|
+
description: 'PRD handoff capsule (v2.12.0+) — peaks-security-audit / peaks-perf-audit both read frontmatter from this file. Must exist before audit prereqs are evaluated at rd:qa-handoff. The rid is part of the filename so two slices in one session do not collide; the bare pre-rid-scoping location is accepted as the legacy tier.',
|
|
122
163
|
// Empty `mustContain` is fine — file existence is the contract.
|
|
123
164
|
// The peaks-prd handoff service writes frontmatter with
|
|
124
165
|
// `schemaVersion: 2` and a sha256 fingerprint; we pin a
|
|
@@ -138,7 +179,8 @@ const AUDIT_REQUIRES_HANDOFF = {
|
|
|
138
179
|
// header remains a substring match (it is the file's own gate header, not
|
|
139
180
|
// a structural section anchor).
|
|
140
181
|
const KARPATHY_REVIEW = {
|
|
141
|
-
relativePath: 'rd/karpathy-review
|
|
182
|
+
relativePath: 'rd/karpathy-review-<rid>.md',
|
|
183
|
+
legacyRelativePath: 'rd/karpathy-review.md',
|
|
142
184
|
description: 'RD-side karpathy review (peaks-rd 5-way fanout) — must contain a "## Karpathy-Gate" header AND the 4 guideline section markers (Think Before Coding / Simplicity First / Surgical Changes / Goal-Driven Execution) as actual markdown headings. Per karpathy §1 / §3.',
|
|
143
185
|
mustContain: ['## Karpathy-Gate'],
|
|
144
186
|
headingMustContain: [
|
|
@@ -328,6 +370,57 @@ async function resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, reques
|
|
|
328
370
|
const match = entries.find((name) => /^\d+-/.test(name) && name.endsWith(targetSuffix));
|
|
329
371
|
return match ? join(dir, match) : null;
|
|
330
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* The contract's BODY checks for one prerequisite, applied to `body`. Returns
|
|
375
|
+
* one human-readable message per failed check, empty when the body satisfies
|
|
376
|
+
* the contract.
|
|
377
|
+
*
|
|
378
|
+
* Exported because this contract has a SECOND enforcer: `peaks workflow
|
|
379
|
+
* verify-pipeline` probes the same table for the paths it checks
|
|
380
|
+
* (`pipeline-verify-gate-support.ts`), and a resolver that probed only
|
|
381
|
+
* `existsSync` passed files this function rejects — a guard laxer than the
|
|
382
|
+
* contract it claims to read. Both callers now share this one implementation,
|
|
383
|
+
* so the checker cannot drift from the table again.
|
|
384
|
+
*/
|
|
385
|
+
export function prerequisiteBodyViolations(prerequisite, body) {
|
|
386
|
+
const violations = [];
|
|
387
|
+
const lowered = body.toLowerCase();
|
|
388
|
+
if (prerequisite.mustContain && prerequisite.mustContain.length > 0) {
|
|
389
|
+
const missingMarkers = prerequisite.mustContain.filter((marker) => !lowered.includes(marker.toLowerCase()));
|
|
390
|
+
if (missingMarkers.length > 0) {
|
|
391
|
+
violations.push(`${prerequisite.description} — missing section(s): ${missingMarkers.join(', ')}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (prerequisite.headingMustContain && prerequisite.headingMustContain.length > 0) {
|
|
395
|
+
// Slice 2.6.1.F: a line beginning with `#`, `##`, or `###` followed by
|
|
396
|
+
// the marker (case-insensitive). Fenced code blocks are NOT excluded
|
|
397
|
+
// here — a "heading" inside a code fence is rare and, when present,
|
|
398
|
+
// should still be reported as missing to keep the contract strict.
|
|
399
|
+
const headingLines = body
|
|
400
|
+
.split('\n')
|
|
401
|
+
.map((line) => line.trim())
|
|
402
|
+
.filter((line) => /^#{1,3}\s+/.test(line));
|
|
403
|
+
const loweredHeadings = headingLines.map((h) => h.toLowerCase());
|
|
404
|
+
const missingHeadings = prerequisite.headingMustContain.filter((marker) => !loweredHeadings.some((h) => h.includes(marker.toLowerCase())));
|
|
405
|
+
if (missingHeadings.length > 0) {
|
|
406
|
+
violations.push(`${prerequisite.description} — missing heading(s): ${missingHeadings.join(', ')}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (prerequisite.mustContainAny && prerequisite.mustContainAny.length > 0) {
|
|
410
|
+
const hitAny = prerequisite.mustContainAny.some((marker) => lowered.includes(marker.toLowerCase()));
|
|
411
|
+
if (!hitAny) {
|
|
412
|
+
violations.push(`${prerequisite.description} — none of the escape-hatch markers present: ${prerequisite.mustContainAny.join(', ')}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return violations;
|
|
416
|
+
}
|
|
417
|
+
/** True when `prerequisite` declares any body check at all. Guards the read so
|
|
418
|
+
* a prereq with no body contract is never opened (unchanged behaviour). */
|
|
419
|
+
function hasBodyChecks(prerequisite) {
|
|
420
|
+
return (prerequisite.mustContain?.length ?? 0) > 0
|
|
421
|
+
|| (prerequisite.headingMustContain?.length ?? 0) > 0
|
|
422
|
+
|| (prerequisite.mustContainAny?.length ?? 0) > 0;
|
|
423
|
+
}
|
|
331
424
|
export async function checkPrerequisites(options) {
|
|
332
425
|
const requirements = getPrerequisitesFor(options.role, options.newState, options.requestType);
|
|
333
426
|
if (requirements.length === 0) {
|
|
@@ -369,45 +462,10 @@ export async function checkPrerequisites(options) {
|
|
|
369
462
|
missing.push({ path: relative, description: prerequisite.description });
|
|
370
463
|
continue;
|
|
371
464
|
}
|
|
372
|
-
if (prerequisite
|
|
465
|
+
if (hasBodyChecks(prerequisite)) {
|
|
373
466
|
const body = await readFile(absolute, 'utf8');
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
if (missingMarkers.length > 0) {
|
|
377
|
-
missing.push({
|
|
378
|
-
path: relative,
|
|
379
|
-
description: `${prerequisite.description} — missing section(s): ${missingMarkers.join(', ')}`
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (prerequisite.headingMustContain && prerequisite.headingMustContain.length > 0) {
|
|
384
|
-
const body = await readFile(absolute, 'utf8');
|
|
385
|
-
// Slice 2.6.1.F: a line beginning with `#`, `##`, or `###` followed by
|
|
386
|
-
// the marker (case-insensitive). Fenced code blocks are NOT excluded
|
|
387
|
-
// here — a "heading" inside a code fence is rare and, when present,
|
|
388
|
-
// should still be reported as missing to keep the contract strict.
|
|
389
|
-
const headingLines = body
|
|
390
|
-
.split('\n')
|
|
391
|
-
.map((line) => line.trim())
|
|
392
|
-
.filter((line) => /^#{1,3}\s+/.test(line));
|
|
393
|
-
const loweredHeadings = headingLines.map((h) => h.toLowerCase());
|
|
394
|
-
const missingHeadings = prerequisite.headingMustContain.filter((marker) => !loweredHeadings.some((h) => h.includes(marker.toLowerCase())));
|
|
395
|
-
if (missingHeadings.length > 0) {
|
|
396
|
-
missing.push({
|
|
397
|
-
path: relative,
|
|
398
|
-
description: `${prerequisite.description} — missing heading(s): ${missingHeadings.join(', ')}`
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (prerequisite.mustContainAny && prerequisite.mustContainAny.length > 0) {
|
|
403
|
-
const body = await readFile(absolute, 'utf8');
|
|
404
|
-
const lowered = body.toLowerCase();
|
|
405
|
-
const hitAny = prerequisite.mustContainAny.some((marker) => lowered.includes(marker.toLowerCase()));
|
|
406
|
-
if (!hitAny) {
|
|
407
|
-
missing.push({
|
|
408
|
-
path: relative,
|
|
409
|
-
description: `${prerequisite.description} — none of the escape-hatch markers present: ${prerequisite.mustContainAny.join(', ')}`
|
|
410
|
-
});
|
|
467
|
+
for (const description of prerequisiteBodyViolations(prerequisite, body)) {
|
|
468
|
+
missing.push({ path: relative, description });
|
|
411
469
|
}
|
|
412
470
|
}
|
|
413
471
|
}
|
|
@@ -454,12 +512,13 @@ function emitPrereqTransitionEvent(opts) {
|
|
|
454
512
|
* (e.g. `001-<rid>.md`) at every tier. Returns the matched absolute
|
|
455
513
|
* path, or null when nothing matches.
|
|
456
514
|
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* `audit/security.md`
|
|
515
|
+
* Back-compat: once neither primary session-root tier resolved, the
|
|
516
|
+
* resolver walks `prerequisite.legacyRelativePaths` in declared order
|
|
517
|
+
* and tries each at BOTH session roots before declaring the prereq
|
|
518
|
+
* missing. This covers both artifacts that moved location
|
|
519
|
+
* (`rd/security-review.md` → `audit/security.md`) and ones that later
|
|
520
|
+
* gained a rid in the filename (`audit/security.md` →
|
|
521
|
+
* `audit/security-<rid>.md`, slice `2026-09-14-audit-artifact-rid-scoping`).
|
|
463
522
|
*/
|
|
464
523
|
async function resolvePrerequisiteAbsolutePathWithFallback(canonicalSessionRoot, legacySessionRoot, prerequisite, requestId) {
|
|
465
524
|
const roots = [canonicalSessionRoot, legacySessionRoot];
|
|
@@ -471,13 +530,19 @@ async function resolvePrerequisiteAbsolutePathWithFallback(canonicalSessionRoot,
|
|
|
471
530
|
if (found !== null)
|
|
472
531
|
return found;
|
|
473
532
|
}
|
|
474
|
-
// Second pass:
|
|
475
|
-
// root
|
|
533
|
+
// Second pass: the single previous location, then any further older
|
|
534
|
+
// ones, at every root — newest first, so a session that somehow holds
|
|
535
|
+
// both a pre-rid and a v2.11.x file resolves to the newer shape.
|
|
536
|
+
// Only the relativePath is swapped — the same numbered-prefix
|
|
476
537
|
// tolerance applies via the shared resolver.
|
|
477
|
-
|
|
538
|
+
const legacyTiers = [
|
|
539
|
+
...(prerequisite.legacyRelativePath !== undefined ? [prerequisite.legacyRelativePath] : []),
|
|
540
|
+
...(prerequisite.legacyRelativePaths ?? [])
|
|
541
|
+
];
|
|
542
|
+
for (const legacy of legacyTiers) {
|
|
478
543
|
const legacyPrereq = {
|
|
479
544
|
...prerequisite,
|
|
480
|
-
relativePath:
|
|
545
|
+
relativePath: legacy
|
|
481
546
|
};
|
|
482
547
|
for (const root of roots) {
|
|
483
548
|
if (root === null)
|
|
@@ -50,6 +50,14 @@ export type CreateRequestArtifactResult = {
|
|
|
50
50
|
*/
|
|
51
51
|
scopeDir: string;
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* F-1 (slice 025 security): reject rids that contain path separators, null
|
|
55
|
+
* bytes, or traversal sequences. A request id is a single path segment, so
|
|
56
|
+
* anything that builds a filename from one must test it against this first —
|
|
57
|
+
* it is exported so those call sites reuse it instead of re-declaring a copy
|
|
58
|
+
* that can drift.
|
|
59
|
+
*/
|
|
60
|
+
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
53
61
|
export declare function createRequestArtifact(options: CreateRequestArtifactOptions): Promise<CreateRequestArtifactResult>;
|
|
54
62
|
export type RequestArtifactSummary = {
|
|
55
63
|
role: RequestArtifactRole;
|
|
@@ -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,
|