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
|
@@ -8,17 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* File budget: ≤ 400 lines (rid-006 split).
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { getPrerequisitesFor, prerequisiteBodyViolations } from '../artifacts/artifact-prerequisites.js';
|
|
13
14
|
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
14
|
-
import {
|
|
15
|
+
import { readArtifactState } from '../artifacts/request-artifact-state-helpers.js';
|
|
15
16
|
export function extractState(markdown) {
|
|
16
|
-
|
|
17
|
-
const match = /^-\s*state:\s*(.+?)\s*$/.exec(rawLine.trim());
|
|
18
|
-
if (match?.[1])
|
|
19
|
-
return match[1];
|
|
20
|
-
}
|
|
21
|
-
return 'unknown';
|
|
17
|
+
return readArtifactState(markdown) ?? 'unknown';
|
|
22
18
|
}
|
|
23
19
|
/**
|
|
24
20
|
* As of slice 2026-06-05-change-id-as-unit-of-work, the file's durable
|
|
@@ -44,12 +40,91 @@ export async function findRequestFile(projectRoot, role, rid) {
|
|
|
44
40
|
}
|
|
45
41
|
return { path: artifact.path, content: artifact.content, sessionId };
|
|
46
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Where the CURRENT contract puts one gate's evidence.
|
|
45
|
+
*
|
|
46
|
+
* The contract is the `artifact-prerequisites.ts` table — the table
|
|
47
|
+
* `peaks request transition` enforces and the one the peaks-qa prose
|
|
48
|
+
* contract mirrors (`skills/bee/peaks-qa/references/qa-transition-gates.md`).
|
|
49
|
+
* `probeName` is the location this checker probes today. The contract's
|
|
50
|
+
* `relativePath` is returned first, then its accepted historical locations in
|
|
51
|
+
* the table's own declared order — `legacyRelativePath`, then
|
|
52
|
+
* `legacyRelativePaths` (the same composition `artifact-prerequisites.ts`' own
|
|
53
|
+
* resolver performs, so a hit here and a hit there land on the same file).
|
|
54
|
+
*
|
|
55
|
+
* Returns `null` when the contract carries no prerequisite for that name at
|
|
56
|
+
* all — the artifact was retired and no gate may fail on it.
|
|
57
|
+
*
|
|
58
|
+
* This lookup exists because hard-coding the paths here is what let
|
|
59
|
+
* `verify-pipeline` fall a minor release behind the gate: it kept demanding
|
|
60
|
+
* `rd/security-review.md` as mandatory after the table had demoted that file
|
|
61
|
+
* to a `legacyRelativePath` fallback of `audit/security.md`, and it kept
|
|
62
|
+
* demanding `qa/security-findings-<rid>.md` / `qa/performance-findings-<rid>.md`
|
|
63
|
+
* after the v2.11.0 D1/D4 trim dropped them from `qa:verdict-issued`
|
|
64
|
+
* altogether (rid 2026-09-14-verify-pipeline-contract-drift).
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* The prerequisite the contract carries for `probeName`, or null when it
|
|
68
|
+
* carries none. `contractEvidencePaths` is this object's path projection; the
|
|
69
|
+
* BODY contract (`mustContain` / `mustContainAny` / `headingMustContain`) rides
|
|
70
|
+
* on the same object, which is why `contractBodyViolations` below consults it
|
|
71
|
+
* instead of probing for the file's existence alone.
|
|
72
|
+
*/
|
|
73
|
+
export function contractPrerequisite(role, state, requestType, probeName) {
|
|
74
|
+
for (const prereq of getPrerequisitesFor(role, state, requestType)) {
|
|
75
|
+
const legacyPaths = [
|
|
76
|
+
...(prereq.legacyRelativePath !== undefined ? [prereq.legacyRelativePath] : []),
|
|
77
|
+
...(prereq.legacyRelativePaths ?? [])
|
|
78
|
+
];
|
|
79
|
+
if (prereq.relativePath !== probeName && !legacyPaths.includes(probeName))
|
|
80
|
+
continue;
|
|
81
|
+
return prereq;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
export function contractEvidencePaths(role, state, requestType, probeName) {
|
|
86
|
+
const prereq = contractPrerequisite(role, state, requestType, probeName);
|
|
87
|
+
if (prereq === null)
|
|
88
|
+
return null;
|
|
89
|
+
const legacyPaths = [
|
|
90
|
+
...(prereq.legacyRelativePath !== undefined ? [prereq.legacyRelativePath] : []),
|
|
91
|
+
...(prereq.legacyRelativePaths ?? [])
|
|
92
|
+
];
|
|
93
|
+
return [prereq.relativePath, ...legacyPaths.filter((legacy) => legacy !== prereq.relativePath)];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The contract's BODY checks for the prerequisite named by `probeName`, applied
|
|
97
|
+
* to the file that resolved. Empty when the name matches nothing in the table or
|
|
98
|
+
* the table pins no body markers for it.
|
|
99
|
+
*
|
|
100
|
+
* `existsSync` alone made this checker laxer than the contract it claims to
|
|
101
|
+
* derive from: it reported `prd-handoff passed = true` for a handoff carrying
|
|
102
|
+
* `schemaVersion: 1` and no `sha256:` line — the exact file
|
|
103
|
+
* `AUDIT_REQUIRES_HANDOFF` refuses at `rd:qa-handoff`. Deriving the *path* from
|
|
104
|
+
* the table while dropping the table's *body* contract is the drift this module
|
|
105
|
+
* exists to remove, so the check goes through the table's own implementation
|
|
106
|
+
* (`prerequisiteBodyViolations`) rather than a second copy of the markers.
|
|
107
|
+
*/
|
|
108
|
+
function contractBodyViolations(role, state, requestType, probeName, absolutePath) {
|
|
109
|
+
const prerequisite = contractPrerequisite(role, state, requestType, probeName);
|
|
110
|
+
if (prerequisite === null)
|
|
111
|
+
return [];
|
|
112
|
+
return prerequisiteBodyViolations(prerequisite, readFileSync(absolutePath, 'utf8'));
|
|
113
|
+
}
|
|
47
114
|
export function rdGatesForType(requestType) {
|
|
48
115
|
const gates = [
|
|
49
116
|
{ name: 'rd-request-exists', description: 'RD request artifact created', passed: false, detail: '' }
|
|
50
117
|
];
|
|
51
|
-
|
|
52
|
-
|
|
118
|
+
// This slot used to pin a `tech-doc` gate to `rd/tech-doc.md`, an artifact
|
|
119
|
+
// v2.11.0 Group A retired and the table no longer carries — so the gate
|
|
120
|
+
// failed trees `peaks request transition` accepts (QA repair cycle 1). The
|
|
121
|
+
// design / scope record the contract DOES name at `rd:qa-handoff` is
|
|
122
|
+
// `prd/handoff.md`, so that is the gate: table-derived, like the others.
|
|
123
|
+
// (`rd/tech-doc.md` still has two non-gate readers — final-review's evidence
|
|
124
|
+
// budget and the resume detector. Neither is served by failing a compliant
|
|
125
|
+
// slice.)
|
|
126
|
+
if (contractEvidencePaths('rd', 'qa-handoff', requestType, 'prd/handoff.md') !== null) {
|
|
127
|
+
gates.push({ name: 'prd-handoff', description: 'PRD handoff capsule (approved scope + non-goals)', passed: false, detail: '' });
|
|
53
128
|
}
|
|
54
129
|
if (requestType === 'bugfix') {
|
|
55
130
|
gates.push({ name: 'bug-analysis', description: 'Bug root-cause analysis', passed: false, detail: '' });
|
|
@@ -60,6 +135,17 @@ export function rdGatesForType(requestType) {
|
|
|
60
135
|
if (requestType === 'feature' || requestType === 'refactor' || requestType === 'bugfix' || requestType === 'config') {
|
|
61
136
|
gates.push({ name: 'security-review', description: 'Security review evidence', passed: false, detail: '' });
|
|
62
137
|
}
|
|
138
|
+
// The perf evidence the contract requires at `rd:qa-handoff` is `AUDIT_PERF`
|
|
139
|
+
// — `audit/perf-<rid>.md`, with `audit/perf.md` and `rd/perf-baseline.md` as
|
|
140
|
+
// its two declared legacy tiers (rid-scoped since slice
|
|
141
|
+
// `2026-09-14-audit-artifact-rid-scoping`). This gate used to live on the QA
|
|
142
|
+
// side as `performance-findings`, checking `qa/performance-findings-<rid>.md`
|
|
143
|
+
// — a path the v2.11.0 D1/D4 trim dropped. Relocating the gate (rather than
|
|
144
|
+
// deleting it) is what keeps AC3's control meaningful: perf evidence that is
|
|
145
|
+
// genuinely absent must still fail.
|
|
146
|
+
if (contractEvidencePaths('rd', 'qa-handoff', requestType, 'rd/perf-baseline.md') !== null) {
|
|
147
|
+
gates.push({ name: 'perf-baseline', description: 'Performance audit evidence', passed: false, detail: '' });
|
|
148
|
+
}
|
|
63
149
|
return gates;
|
|
64
150
|
}
|
|
65
151
|
export function qaGatesForType(requestType) {
|
|
@@ -70,124 +156,157 @@ export function qaGatesForType(requestType) {
|
|
|
70
156
|
gates.push({ name: 'test-cases', description: 'QA test cases', passed: false, detail: '' });
|
|
71
157
|
gates.push({ name: 'test-report', description: 'QA test report with execution results', passed: false, detail: '' });
|
|
72
158
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
159
|
+
// The `security-findings` / `performance-findings` gates were removed here
|
|
160
|
+
// (rid 2026-09-14-verify-pipeline-contract-drift). peaks-qa does not own
|
|
161
|
+
// security review or performance review — `peaks-qa/SKILL.md` says so, and
|
|
162
|
+
// the v2.11.0 D1/D4 trim dropped both from every `qa:verdict-issued` table.
|
|
163
|
+
// The evidence they used to demand is now checked on the RD side, at the
|
|
164
|
+
// paths the contract actually names (`audit/security-<rid>.md`,
|
|
165
|
+
// `audit/perf-<rid>.md`; rid-scoped since slice
|
|
166
|
+
// `2026-09-14-audit-artifact-rid-scoping`, with `audit/security.md` /
|
|
167
|
+
// `audit/perf.md` and `rd/security-review.md` / `rd/perf-baseline.md` as the
|
|
168
|
+
// declared legacy tiers behind them).
|
|
79
169
|
return gates;
|
|
80
170
|
}
|
|
81
171
|
export const RD_QA_HANDOFF_STATES = new Set(['qa-handoff', 'handed-off', 'implemented']);
|
|
82
172
|
export const QA_COMPLETE_STATES = new Set(['verdict-issued']);
|
|
83
|
-
/** Resolve RD evidence files (
|
|
173
|
+
/** Resolve RD evidence files (prd-handoff / bug-analysis / code-review / security-review / perf-baseline)
|
|
84
174
|
* by probing canonical + legacy paths for each gate. Mutates the gate.passed /
|
|
85
175
|
* gate.detail fields in place and pushes violations / nextActions. Returns the
|
|
86
176
|
* updated tracker. Verbatim-move from `pipeline-verify-service.ts`. */
|
|
87
|
-
export function resolveRdEvidencePaths(gates, rdEvidenceDir, projectRoot, violations, nextActions, tracker) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
177
|
+
export function resolveRdEvidencePaths(gates, rdEvidenceDir, projectRoot, rid, requestType, violations, nextActions, tracker) {
|
|
178
|
+
// The path each gate is about. The contract is consulted first — see
|
|
179
|
+
// `contractEvidencePaths`; `rdGatesForType` only builds a gate when the
|
|
180
|
+
// contract carries its prerequisite, so this is the name the gate stands
|
|
181
|
+
// for, not a second path table.
|
|
182
|
+
const RD_EVIDENCE_PROBE = {
|
|
183
|
+
'prd-handoff': 'prd/handoff.md',
|
|
184
|
+
'bug-analysis': 'rd/bug-analysis.md',
|
|
185
|
+
'code-review': 'rd/code-review.md',
|
|
186
|
+
'security-review': 'rd/security-review.md',
|
|
187
|
+
'perf-baseline': 'rd/perf-baseline.md'
|
|
93
188
|
};
|
|
94
189
|
let anyEvidenceResolved = tracker.anyEvidenceResolved;
|
|
95
190
|
let allResolvedPathsCanonical = tracker.allResolvedPathsCanonical;
|
|
96
191
|
for (const gate of gates.slice(1)) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
192
|
+
const probeName = RD_EVIDENCE_PROBE[gate.name];
|
|
193
|
+
const relativePaths = (contractEvidencePaths('rd', 'qa-handoff', requestType, probeName) ?? [probeName])
|
|
194
|
+
.map((relative) => relative.replace('<rid>', rid));
|
|
195
|
+
const primaryRelativePath = relativePaths[0];
|
|
196
|
+
const canonicalPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, primaryRelativePath);
|
|
197
|
+
// Two independent axes, kept apart because they mean different things:
|
|
198
|
+
// - `legacyRoot` — the file sits under a pre-F3 session home
|
|
199
|
+
// (`.peaks/<sid>/…` or `.peaks/_runtime/change/<sid>/…`). That is a
|
|
200
|
+
// misplaced write, and it is the DEPRECATION violation's subject.
|
|
201
|
+
// - `legacyForm` — the file sits under one of the contract's declared
|
|
202
|
+
// `legacyRelativePaths`. The contract accepts those on purpose ("a
|
|
203
|
+
// legacy hit must keep the gate open, or existing sessions would fail
|
|
204
|
+
// the transition on upgrade"), so this is reported through
|
|
205
|
+
// `acceptedForm` and is *not* a violation.
|
|
206
|
+
const candidates = [];
|
|
207
|
+
for (const relative of relativePaths) {
|
|
208
|
+
const legacyForm = relative !== primaryRelativePath;
|
|
209
|
+
for (const [root, legacyRoot] of [
|
|
210
|
+
[join(projectRoot, '.peaks', '_runtime', rdEvidenceDir), false],
|
|
211
|
+
[join(projectRoot, '.peaks', rdEvidenceDir), true],
|
|
212
|
+
[join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir), true]
|
|
213
|
+
]) {
|
|
214
|
+
candidates.push({ path: join(root, relative), legacyRoot, legacyForm });
|
|
108
215
|
}
|
|
109
216
|
}
|
|
110
|
-
|
|
217
|
+
const hit = candidates.find((candidate) => existsSync(candidate.path));
|
|
218
|
+
if (hit !== undefined) {
|
|
111
219
|
anyEvidenceResolved = true;
|
|
112
|
-
if (
|
|
220
|
+
if (hit.legacyRoot)
|
|
113
221
|
allResolvedPathsCanonical = false;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
222
|
+
// Existing is not the contract — the file must also satisfy the body
|
|
223
|
+
// markers the table pins for this prereq (see `contractBodyViolations`).
|
|
224
|
+
const bodyViolations = contractBodyViolations('rd', 'qa-handoff', requestType, probeName, hit.path);
|
|
225
|
+
if (bodyViolations.length > 0) {
|
|
226
|
+
gate.passed = false;
|
|
227
|
+
gate.detail = `${hit.path} — ${bodyViolations.join('; ')}`;
|
|
228
|
+
violations.push(`RD evidence does not satisfy the contract: ${gate.description} (${primaryRelativePath}) — ${bodyViolations.join('; ')}`);
|
|
229
|
+
nextActions.push(`Fix .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} to satisfy the contract: ${bodyViolations.join('; ')}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
gate.passed = true;
|
|
233
|
+
gate.detail = hit.path
|
|
234
|
+
+ (hit.legacyRoot ? ' [DEPRECATION_LEGACY_PATH_USED]' : '')
|
|
235
|
+
+ (hit.legacyForm ? ' [LEGACY_EVIDENCE_PATH]' : '');
|
|
236
|
+
}
|
|
237
|
+
if (hit.legacyRoot) {
|
|
238
|
+
violations.push(`DEPRECATION_LEGACY_PATH_USED: ${hit.path} — move the file into .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} (the canonical location) so subsequent runs resolve on the canonical path. The legacy \`peaks workspace migrate-change-scope\` helper was removed in v2.19.0; use \`peaks workspace migrate\` to relocate misplaced content.`);
|
|
118
239
|
}
|
|
119
240
|
}
|
|
120
241
|
else {
|
|
121
242
|
gate.detail = `missing: ${canonicalPath}`;
|
|
122
|
-
violations.push(`RD evidence missing: ${gate.description} (${
|
|
123
|
-
nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}
|
|
243
|
+
violations.push(`RD evidence missing: ${gate.description} (${primaryRelativePath})`);
|
|
244
|
+
nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath}`);
|
|
124
245
|
}
|
|
125
246
|
}
|
|
126
247
|
return { anyEvidenceResolved, allResolvedPathsCanonical };
|
|
127
248
|
}
|
|
128
|
-
/** Resolve QA evidence files (test-cases / test-report
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
249
|
+
/** Resolve QA evidence files (test-cases / test-report) by probing canonical
|
|
250
|
+
* + legacy paths for each gate. Mutates the gate.passed / gate.detail fields
|
|
251
|
+
* in place and pushes violations / nextActions. Returns the updated tracker.
|
|
252
|
+
* Verbatim-move from `pipeline-verify-service.ts`.
|
|
253
|
+
*
|
|
254
|
+
* The `security-findings` / `performance-findings` branches that used to live
|
|
255
|
+
* here are gone: those artifacts are not in any `qa:verdict-issued` table
|
|
256
|
+
* (v2.11.0 D1/D4), and the evidence they stood for is resolved on the RD side
|
|
257
|
+
* at the contract's own paths (`audit/security-<rid>.md`,
|
|
258
|
+
* `audit/perf-<rid>.md`). */
|
|
259
|
+
export function resolveQaEvidencePaths(gates, projectRoot, rdEvidenceDir, requestType, rid, violations, nextActions, tracker) {
|
|
260
|
+
const QA_EVIDENCE_PROBE = {
|
|
261
|
+
'test-cases': 'qa/test-cases/<rid>.md',
|
|
262
|
+
'test-report': 'qa/test-reports/<rid>.md'
|
|
140
263
|
};
|
|
141
264
|
let anyEvidenceResolved = tracker.anyEvidenceResolved;
|
|
142
265
|
let allResolvedPathsCanonical = tracker.allResolvedPathsCanonical;
|
|
143
266
|
for (const gate of gates.slice(1)) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
else {
|
|
158
|
-
gate.detail = `missing: ${resolved.path}`;
|
|
159
|
-
violations.push(`QA evidence missing: ${gate.description} (${resolved.path})`);
|
|
160
|
-
nextActions.push(`Create ${resolved.path} (or use the legacy non-suffixed form during the 1-minor-release back-compat window)`);
|
|
161
|
-
}
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
const fileName = QA_EVIDENCE_FILE[gate.name];
|
|
165
|
-
const canonicalQaPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, 'qa', fileName);
|
|
166
|
-
const legacyMisplacedQaPath = join(projectRoot, '.peaks', rdEvidenceDir, 'qa', fileName);
|
|
167
|
-
const legacyChangeAxisQaPath = join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir, 'qa', fileName);
|
|
168
|
-
let resolvedQaPath = null;
|
|
169
|
-
let usedLegacyQa = false;
|
|
170
|
-
for (const candidate of [canonicalQaPath, legacyMisplacedQaPath, legacyChangeAxisQaPath]) {
|
|
171
|
-
if (existsSync(candidate)) {
|
|
172
|
-
resolvedQaPath = candidate;
|
|
173
|
-
usedLegacyQa = candidate !== canonicalQaPath;
|
|
174
|
-
break;
|
|
267
|
+
const probeName = QA_EVIDENCE_PROBE[gate.name];
|
|
268
|
+
const relativePaths = (contractEvidencePaths('qa', 'verdict-issued', requestType, probeName) ?? [probeName])
|
|
269
|
+
.map((relative) => relative.replace('<rid>', rid));
|
|
270
|
+
const primaryRelativePath = relativePaths[0];
|
|
271
|
+
const canonicalQaPath = join(projectRoot, '.peaks', '_runtime', rdEvidenceDir, primaryRelativePath);
|
|
272
|
+
const candidates = [];
|
|
273
|
+
for (const relative of relativePaths) {
|
|
274
|
+
for (const [root, legacyRoot] of [
|
|
275
|
+
[join(projectRoot, '.peaks', '_runtime', rdEvidenceDir), false],
|
|
276
|
+
[join(projectRoot, '.peaks', rdEvidenceDir), true],
|
|
277
|
+
[join(projectRoot, '.peaks', '_runtime', 'change', rdEvidenceDir), true]
|
|
278
|
+
]) {
|
|
279
|
+
candidates.push({ path: join(root, relative), legacyRoot });
|
|
175
280
|
}
|
|
176
281
|
}
|
|
177
|
-
|
|
282
|
+
const hit = candidates.find((candidate) => existsSync(candidate.path));
|
|
283
|
+
if (hit !== undefined) {
|
|
178
284
|
anyEvidenceResolved = true;
|
|
179
|
-
if (
|
|
285
|
+
if (hit.legacyRoot)
|
|
180
286
|
allResolvedPathsCanonical = false;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
287
|
+
// Same contract check as the RD side: the `test-report` / `test-cases`
|
|
288
|
+
// prereqs pin `## Test execution` / `## Test cases` / `test(`, and a
|
|
289
|
+
// checker that only probed existence reported green on files the
|
|
290
|
+
// `qa:verdict-issued` gate rejects.
|
|
291
|
+
const bodyViolations = contractBodyViolations('qa', 'verdict-issued', requestType, probeName, hit.path);
|
|
292
|
+
if (bodyViolations.length > 0) {
|
|
293
|
+
gate.passed = false;
|
|
294
|
+
gate.detail = `${hit.path} — ${bodyViolations.join('; ')}`;
|
|
295
|
+
violations.push(`QA evidence does not satisfy the contract: ${gate.description} (${primaryRelativePath}) — ${bodyViolations.join('; ')}`);
|
|
296
|
+
nextActions.push(`Fix .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} to satisfy the contract: ${bodyViolations.join('; ')}`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
gate.passed = true;
|
|
300
|
+
gate.detail = hit.path + (hit.legacyRoot ? ' [DEPRECATION_LEGACY_PATH_USED]' : '');
|
|
301
|
+
}
|
|
302
|
+
if (hit.legacyRoot) {
|
|
303
|
+
violations.push(`DEPRECATION_LEGACY_PATH_USED: ${hit.path} — move the file into .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath} (the canonical location) so subsequent runs resolve on the canonical path. The legacy \`peaks workspace migrate-change-scope\` helper was removed in v2.19.0; use \`peaks workspace migrate\` to relocate misplaced content.`);
|
|
185
304
|
}
|
|
186
305
|
}
|
|
187
306
|
else {
|
|
188
307
|
gate.detail = `missing: ${canonicalQaPath}`;
|
|
189
|
-
violations.push(`QA evidence missing: ${gate.description} (${
|
|
190
|
-
nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}
|
|
308
|
+
violations.push(`QA evidence missing: ${gate.description} (${primaryRelativePath})`);
|
|
309
|
+
nextActions.push(`Create .peaks/_runtime/${rdEvidenceDir}/${primaryRelativePath}`);
|
|
191
310
|
}
|
|
192
311
|
}
|
|
193
312
|
return { anyEvidenceResolved, allResolvedPathsCanonical };
|
|
@@ -96,7 +96,7 @@ export async function verifyPipeline(options) {
|
|
|
96
96
|
// to `options.rid` would make every missing-evidence path look like
|
|
97
97
|
// a per-rid scope dir.
|
|
98
98
|
const rdEvidenceDir = resolvedChangeId || options.sessionId || getSessionIdCanonical(options.projectRoot) || options.rid;
|
|
99
|
-
const rdTracker = resolveRdEvidencePaths(rdGates, rdEvidenceDir, options.projectRoot, violations, nextActions, { anyEvidenceResolved: false, allResolvedPathsCanonical: true });
|
|
99
|
+
const rdTracker = resolveRdEvidencePaths(rdGates, rdEvidenceDir, options.projectRoot, options.rid, requestType, violations, nextActions, { anyEvidenceResolved: false, allResolvedPathsCanonical: true });
|
|
100
100
|
// Check if RD reached qa-handoff
|
|
101
101
|
if (rdInvoked && !RD_QA_HANDOFF_STATES.has(rdState)) {
|
|
102
102
|
violations.push(`RD not ready for QA: state is "${rdState}" — must reach "qa-handoff" (unit tests, karpathy-guidelines §1 Think / §2 Simplicity / §3 Surgical / §4 Goal-Driven, code review, security review complete)`);
|
|
@@ -118,16 +118,11 @@ export async function verifyPipeline(options) {
|
|
|
118
118
|
nextActions.push('Invoke Skill(skill="peaks-qa") with the request-id for functional/performance/security testing');
|
|
119
119
|
qaGates[0].detail = 'not found';
|
|
120
120
|
}
|
|
121
|
-
// Check QA evidence files.
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
// v2.17.0 home; the legacy `_runtime/change/<sessionId>/qa/...` probe
|
|
127
|
-
// should only fire for pre-v2.17.0 workspaces, not as a default for
|
|
128
|
-
// new requests.
|
|
129
|
-
const changeIdForResolver = resolvedChangeId || getSessionIdCanonical(options.projectRoot) || rdEvidenceDir;
|
|
130
|
-
const qaTracker = resolveQaEvidencePaths(qaGates, options.projectRoot, rdEvidenceDir, changeIdForResolver, options.rid, violations, nextActions, rdTracker);
|
|
121
|
+
// Check QA evidence files. (The v2.18.1 bug #5 `changeIdForResolver`
|
|
122
|
+
// fallback — current session id when no RD/QA artifact is on disk yet —
|
|
123
|
+
// was dropped by rid 2026-09-14-verify-pipeline-contract-drift along with
|
|
124
|
+
// the security/perf findings branch that was its only consumer.)
|
|
125
|
+
const qaTracker = resolveQaEvidencePaths(qaGates, options.projectRoot, rdEvidenceDir, requestType, options.rid, violations, nextActions, rdTracker);
|
|
131
126
|
const anyEvidenceResolved = qaTracker.anyEvidenceResolved;
|
|
132
127
|
const allResolvedPathsCanonical = qaTracker.allResolvedPathsCanonical;
|
|
133
128
|
// Check if QA reached verdict-issued
|
|
@@ -225,20 +220,26 @@ export async function verifyPipeline(options) {
|
|
|
225
220
|
const complete = rdInvoked && qaInvoked && allRdGatesPassed && allQaGatesPassed && allFeedbackGatesPassed
|
|
226
221
|
&& RD_QA_HANDOFF_STATES.has(rdState) && QA_COMPLETE_STATES.has(qaState);
|
|
227
222
|
// Slice 025 — derive the `acceptedForm` and `gateC` verdict. The form is
|
|
228
|
-
// 'suffixed'
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
223
|
+
// 'suffixed' when the contract's current path served the file and 'legacy'
|
|
224
|
+
// when the deprecated fallback did; 'none' if neither gate passed.
|
|
225
|
+
//
|
|
226
|
+
// rid 2026-09-14-verify-pipeline-contract-drift: the two gates moved from the
|
|
227
|
+
// QA phase to the RD phase, because that is where the current contract puts
|
|
228
|
+
// the evidence (`AUDIT_SECURITY` / `AUDIT_PERF` at `rd:qa-handoff`). The
|
|
229
|
+
// `-<rid>.md` suffix that used to distinguish the forms is a real one again
|
|
230
|
+
// after slice `2026-09-14-audit-artifact-rid-scoping` rid-scoped the audit
|
|
231
|
+
// paths; the resolver marks which form served the file with
|
|
232
|
+
// `[LEGACY_EVIDENCE_PATH]`.
|
|
233
|
+
const secGate = rdGates.find((g) => g.name === 'security-review');
|
|
234
|
+
const perfGate = rdGates.find((g) => g.name === 'perf-baseline');
|
|
235
|
+
const formOf = (gate) => gate?.passed === true && !gate.detail.includes('LEGACY_EVIDENCE_PATH') ? 'suffixed' : 'legacy';
|
|
236
|
+
const secForm = formOf(secGate);
|
|
237
|
+
const perfForm = formOf(perfGate);
|
|
235
238
|
const acceptedForm = !secGate?.passed && !perfGate?.passed
|
|
236
239
|
? 'none'
|
|
237
|
-
: (secForm === '
|
|
238
|
-
? '
|
|
239
|
-
:
|
|
240
|
-
? 'legacy'
|
|
241
|
-
: 'suffixed';
|
|
240
|
+
: (secForm === 'legacy' || perfForm === 'legacy')
|
|
241
|
+
? 'legacy'
|
|
242
|
+
: 'suffixed';
|
|
242
243
|
const gateC = allQaGatesPassed ? 'pass' : 'fail';
|
|
243
244
|
const gateH = allFeedbackGatesPassed ? 'pass' : 'fail';
|
|
244
245
|
// Slice 2026-06-28-code-mode-bypass-fix (defect #3): `true` when
|
|
@@ -59,9 +59,16 @@ export type PipelineVerification = {
|
|
|
59
59
|
};
|
|
60
60
|
violations: string[];
|
|
61
61
|
nextActions: string[];
|
|
62
|
-
/** Form of the security/performance
|
|
63
|
-
* (slice 025). `'suffixed'`
|
|
64
|
-
*
|
|
62
|
+
/** Form of the security/performance evidence the RD gates accepted
|
|
63
|
+
* (slice 025). `'suffixed'` when the current contract's path served the
|
|
64
|
+
* file, `'legacy'` when a deprecated fallback did, `'none'` when neither
|
|
65
|
+
* gate passed. The per-rid `<rid>.md` suffix the union was named after was
|
|
66
|
+
* retired with `qa/security-findings-<rid>.md` (v2.11.0 D1/D4); the evidence
|
|
67
|
+
* now lives at `audit/security-<rid>.md` / `audit/perf-<rid>.md`
|
|
68
|
+
* (rid-scoped since slice `2026-09-14-audit-artifact-rid-scoping`) with
|
|
69
|
+
* `audit/security.md` / `audit/perf.md` and
|
|
70
|
+
* `rd/security-review.md` / `rd/perf-baseline.md` as the declared legacy
|
|
71
|
+
* fallbacks (rid 2026-09-14-verify-pipeline-contract-drift). */
|
|
65
72
|
acceptedForm?: 'suffixed' | 'legacy' | 'none';
|
|
66
73
|
/** `gateC` is the pre-computed verdict string (AC7 dogfood shape). */
|
|
67
74
|
gateC?: 'pass' | 'fail';
|
|
@@ -186,7 +186,8 @@ function resolveSkipAppliedBy() {
|
|
|
186
186
|
const { execFileSync } = require('node:child_process');
|
|
187
187
|
const out = execFileSync('git', ['config', '--global', 'user.name'], {
|
|
188
188
|
encoding: 'utf8',
|
|
189
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
189
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
190
|
+
windowsHide: true
|
|
190
191
|
});
|
|
191
192
|
const trimmed = out.trim();
|
|
192
193
|
if (trimmed.length > 0) {
|
|
@@ -83,15 +83,35 @@ export declare const CLAUDE_SETTINGS_LOCAL_FILENAME = ".claude/settings.local.js
|
|
|
83
83
|
*/
|
|
84
84
|
export declare const TEMPLATE_VERSION = "1.7.0";
|
|
85
85
|
/**
|
|
86
|
-
* Compare two serialized template strings
|
|
87
|
-
*
|
|
86
|
+
* Compare two serialized template strings: does the on-disk file already
|
|
87
|
+
* declare every entry the generated tree declares?
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
89
|
+
* OWNERSHIP IS PER ENTRY, NOT PER KEY (rid 2026-09-13-two-decisions item ②).
|
|
90
|
+
* This comparator answers "is each entry the GENERATED tree declares present
|
|
91
|
+
* on disk?", NOT "are the two `hooks` trees identical". Extra on-disk entries
|
|
92
|
+
* are IGNORED, so an entry another writer put in this file never makes it look
|
|
93
|
+
* drifted.
|
|
94
|
+
*
|
|
95
|
+
* That is the deliberate other half of the entry-level merge in
|
|
96
|
+
* `mergeTemplateOwnedHooks` / `workspace-claude-settings-materializer.ts`.
|
|
97
|
+
* `.claude/settings.local.json` has a SECOND writer of `hooks.PreToolUse`:
|
|
98
|
+
* `installAutoCompactHook` appends a `Bash|Task` entry. Under the previous
|
|
99
|
+
* exact-tree rule the merged file carried 4 entries against a 3-entry
|
|
100
|
+
* generated tree, so every `peaks workspace init` answered "drifted",
|
|
101
|
+
* rewrote, and reported `refreshed` forever — precisely the state whole-key
|
|
102
|
+
* ownership existed to prevent, and the reason the merge could not ship alone.
|
|
103
|
+
*
|
|
104
|
+
* Matching is order-insensitive AND multiset-aware: the template declares TWO
|
|
105
|
+
* `Bash` entries, and each must have its own counterpart on disk, so a file
|
|
106
|
+
* carrying only one of them is still reported as drifted (the previous
|
|
107
|
+
* index-by-index loop had the same property; it is load-bearing, not a
|
|
108
|
+
* detail).
|
|
109
|
+
*
|
|
110
|
+
* Returns `true` iff both strings parse to objects whose `hooks.PreToolUse`
|
|
111
|
+
* arrays satisfy that containment AND the on-disk `env` already carries every
|
|
112
|
+
* exemption the template declares (extra on-disk keys and extra globs are
|
|
113
|
+
* allowed — a user may exempt other trees, and a requirement the file already
|
|
114
|
+
* exceeds must not re-trigger a write).
|
|
95
115
|
*
|
|
96
116
|
* Returns `false` on any `JSON.parse` error, shape mismatch, or
|
|
97
117
|
* missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
|
|
@@ -101,6 +121,34 @@ export declare const TEMPLATE_VERSION = "1.7.0";
|
|
|
101
121
|
* refresh a stale `.peaks/.claude-settings-template.json` on disk.
|
|
102
122
|
*/
|
|
103
123
|
export declare function templateContentMatches(generated: string, onDisk: string): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Merge the on-disk `hooks.PreToolUse` list with the template's.
|
|
126
|
+
*
|
|
127
|
+
* THE OWNERSHIP RULE (rid 2026-09-13-two-decisions item ②): this template owns
|
|
128
|
+
* the entries IT DECLARES — and nothing else. Every other on-disk entry is
|
|
129
|
+
* carried across verbatim, whatever its matcher, because the template has no
|
|
130
|
+
* opinion about it:
|
|
131
|
+
*
|
|
132
|
+
* - a `matcher` the template does not declare (`Bash|Task`, the auto-compact
|
|
133
|
+
* hook `installAutoCompactHook` appends) is never touched;
|
|
134
|
+
* - surplus entries BEYOND the template's count for a declared matcher (a
|
|
135
|
+
* user's own `Bash` hook) are surplus too, and survive;
|
|
136
|
+
* - an on-disk entry that fills a declared slot is REPLACED by the template's
|
|
137
|
+
* entry for it. That is what makes a hand-edited (or older-release) entry
|
|
138
|
+
* self-heal instead of lingering next to a correct copy of itself.
|
|
139
|
+
*
|
|
140
|
+
* Slot counting is per `matcher` and positional within it: the template
|
|
141
|
+
* declares TWO `Bash` entries, so the first two on-disk `Bash` entries are
|
|
142
|
+
* theirs and a third is the user's. The template's entries are emitted first,
|
|
143
|
+
* in template order, then the preserved ones in their on-disk order — which is
|
|
144
|
+
* a fixed point: re-merging the result yields the result (the template's own
|
|
145
|
+
* entries are encountered first and refill their own slots).
|
|
146
|
+
*
|
|
147
|
+
* Non-conforming entries (no string `matcher`, no `hooks` array) are preserved
|
|
148
|
+
* rather than dropped: guessing at their shape is how a user's entry gets
|
|
149
|
+
* deleted.
|
|
150
|
+
*/
|
|
151
|
+
export declare function mergeTemplateOwnedHooks(onDisk: ReadonlyArray<unknown>, template: ReadonlyArray<unknown>): unknown[];
|
|
104
152
|
/**
|
|
105
153
|
* Absolute path of the shipped Write|Edit|MultiEdit gate script.
|
|
106
154
|
*
|