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
|
@@ -86,15 +86,35 @@ export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
|
|
|
86
86
|
*/
|
|
87
87
|
export const TEMPLATE_VERSION = '1.7.0';
|
|
88
88
|
/**
|
|
89
|
-
* Compare two serialized template strings
|
|
90
|
-
*
|
|
89
|
+
* Compare two serialized template strings: does the on-disk file already
|
|
90
|
+
* declare every entry the generated tree declares?
|
|
91
91
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
92
|
+
* OWNERSHIP IS PER ENTRY, NOT PER KEY (rid 2026-09-13-two-decisions item ②).
|
|
93
|
+
* This comparator answers "is each entry the GENERATED tree declares present
|
|
94
|
+
* on disk?", NOT "are the two `hooks` trees identical". Extra on-disk entries
|
|
95
|
+
* are IGNORED, so an entry another writer put in this file never makes it look
|
|
96
|
+
* drifted.
|
|
97
|
+
*
|
|
98
|
+
* That is the deliberate other half of the entry-level merge in
|
|
99
|
+
* `mergeTemplateOwnedHooks` / `workspace-claude-settings-materializer.ts`.
|
|
100
|
+
* `.claude/settings.local.json` has a SECOND writer of `hooks.PreToolUse`:
|
|
101
|
+
* `installAutoCompactHook` appends a `Bash|Task` entry. Under the previous
|
|
102
|
+
* exact-tree rule the merged file carried 4 entries against a 3-entry
|
|
103
|
+
* generated tree, so every `peaks workspace init` answered "drifted",
|
|
104
|
+
* rewrote, and reported `refreshed` forever — precisely the state whole-key
|
|
105
|
+
* ownership existed to prevent, and the reason the merge could not ship alone.
|
|
106
|
+
*
|
|
107
|
+
* Matching is order-insensitive AND multiset-aware: the template declares TWO
|
|
108
|
+
* `Bash` entries, and each must have its own counterpart on disk, so a file
|
|
109
|
+
* carrying only one of them is still reported as drifted (the previous
|
|
110
|
+
* index-by-index loop had the same property; it is load-bearing, not a
|
|
111
|
+
* detail).
|
|
112
|
+
*
|
|
113
|
+
* Returns `true` iff both strings parse to objects whose `hooks.PreToolUse`
|
|
114
|
+
* arrays satisfy that containment AND the on-disk `env` already carries every
|
|
115
|
+
* exemption the template declares (extra on-disk keys and extra globs are
|
|
116
|
+
* allowed — a user may exempt other trees, and a requirement the file already
|
|
117
|
+
* exceeds must not re-trigger a write).
|
|
98
118
|
*
|
|
99
119
|
* Returns `false` on any `JSON.parse` error, shape mismatch, or
|
|
100
120
|
* missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
|
|
@@ -121,20 +141,15 @@ export function templateContentMatches(generated, onDisk) {
|
|
|
121
141
|
if (!isTemplateShape(parsedGenerated) || !isTemplateShape(parsedOnDisk)) {
|
|
122
142
|
return false;
|
|
123
143
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const a = generatedEntries[i];
|
|
131
|
-
const b = onDiskEntries[i];
|
|
132
|
-
if (a.matcher !== b.matcher) {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
if (!sameHooksArray(a.hooks, b.hooks)) {
|
|
144
|
+
// Multiset containment: consume one on-disk entry per generated entry so a
|
|
145
|
+
// file holding a single copy of a doubly-declared entry still fails.
|
|
146
|
+
const unmatched = [...parsedOnDisk.hooks.PreToolUse];
|
|
147
|
+
for (const required of parsedGenerated.hooks.PreToolUse) {
|
|
148
|
+
const at = unmatched.findIndex((candidate) => sameEntry(required, candidate));
|
|
149
|
+
if (at === -1) {
|
|
136
150
|
return false;
|
|
137
151
|
}
|
|
152
|
+
unmatched.splice(at, 1);
|
|
138
153
|
}
|
|
139
154
|
// A project installed by a release that predates a template-declared
|
|
140
155
|
// exemption still needs the refresh this comparator gates — otherwise the
|
|
@@ -143,6 +158,69 @@ export function templateContentMatches(generated, onDisk) {
|
|
|
143
158
|
// uses, so the two writers cannot drift apart.
|
|
144
159
|
return hasExternalGateExemptions({ env: parsedOnDisk.env });
|
|
145
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Merge the on-disk `hooks.PreToolUse` list with the template's.
|
|
163
|
+
*
|
|
164
|
+
* THE OWNERSHIP RULE (rid 2026-09-13-two-decisions item ②): this template owns
|
|
165
|
+
* the entries IT DECLARES — and nothing else. Every other on-disk entry is
|
|
166
|
+
* carried across verbatim, whatever its matcher, because the template has no
|
|
167
|
+
* opinion about it:
|
|
168
|
+
*
|
|
169
|
+
* - a `matcher` the template does not declare (`Bash|Task`, the auto-compact
|
|
170
|
+
* hook `installAutoCompactHook` appends) is never touched;
|
|
171
|
+
* - surplus entries BEYOND the template's count for a declared matcher (a
|
|
172
|
+
* user's own `Bash` hook) are surplus too, and survive;
|
|
173
|
+
* - an on-disk entry that fills a declared slot is REPLACED by the template's
|
|
174
|
+
* entry for it. That is what makes a hand-edited (or older-release) entry
|
|
175
|
+
* self-heal instead of lingering next to a correct copy of itself.
|
|
176
|
+
*
|
|
177
|
+
* Slot counting is per `matcher` and positional within it: the template
|
|
178
|
+
* declares TWO `Bash` entries, so the first two on-disk `Bash` entries are
|
|
179
|
+
* theirs and a third is the user's. The template's entries are emitted first,
|
|
180
|
+
* in template order, then the preserved ones in their on-disk order — which is
|
|
181
|
+
* a fixed point: re-merging the result yields the result (the template's own
|
|
182
|
+
* entries are encountered first and refill their own slots).
|
|
183
|
+
*
|
|
184
|
+
* Non-conforming entries (no string `matcher`, no `hooks` array) are preserved
|
|
185
|
+
* rather than dropped: guessing at their shape is how a user's entry gets
|
|
186
|
+
* deleted.
|
|
187
|
+
*/
|
|
188
|
+
export function mergeTemplateOwnedHooks(onDisk, template) {
|
|
189
|
+
const slots = new Map();
|
|
190
|
+
for (const entry of template) {
|
|
191
|
+
if (!isPreToolUseEntry(entry))
|
|
192
|
+
continue;
|
|
193
|
+
slots.set(entry.matcher, (slots.get(entry.matcher) ?? 0) + 1);
|
|
194
|
+
}
|
|
195
|
+
const taken = new Map();
|
|
196
|
+
const preserved = [];
|
|
197
|
+
for (const entry of onDisk) {
|
|
198
|
+
// Unowned by construction: not a shape the template could have declared.
|
|
199
|
+
if (!isPreToolUseEntry(entry)) {
|
|
200
|
+
preserved.push(entry);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const declared = slots.get(entry.matcher) ?? 0;
|
|
204
|
+
const used = taken.get(entry.matcher) ?? 0;
|
|
205
|
+
if (used >= declared) {
|
|
206
|
+
preserved.push(entry);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
taken.set(entry.matcher, used + 1);
|
|
210
|
+
}
|
|
211
|
+
return [...template, ...preserved];
|
|
212
|
+
}
|
|
213
|
+
/** Structural equality of two `PreToolUse` entries. */
|
|
214
|
+
function sameEntry(a, b) {
|
|
215
|
+
return a.matcher === b.matcher && sameHooksArray(a.hooks, b.hooks);
|
|
216
|
+
}
|
|
217
|
+
function isPreToolUseEntry(value) {
|
|
218
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const candidate = value;
|
|
222
|
+
return typeof candidate.matcher === 'string' && Array.isArray(candidate.hooks);
|
|
223
|
+
}
|
|
146
224
|
function isTemplateShape(value) {
|
|
147
225
|
if (typeof value !== 'object' || value === null) {
|
|
148
226
|
return false;
|
|
@@ -426,7 +426,7 @@ async function gitMv(from, to, projectRoot) {
|
|
|
426
426
|
// command must be run from inside the project so it can locate
|
|
427
427
|
// .git/ (the migrate target may be a temp dir created by tests).
|
|
428
428
|
try {
|
|
429
|
-
execFileSync('git', ['mv', from, to], { cwd: projectRoot, stdio: 'pipe' });
|
|
429
|
+
execFileSync('git', ['mv', from, to], { cwd: projectRoot, stdio: 'pipe', windowsHide: true });
|
|
430
430
|
}
|
|
431
431
|
catch {
|
|
432
432
|
throw error;
|
|
@@ -13,7 +13,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
13
13
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { withExternalGateExemptions } from '../skills/hooks-codegate-superpowers.js';
|
|
16
|
-
import { buildClaudeSettingsLocalJson, CLAUDE_SETTINGS_LOCAL_FILENAME, templateContentMatches } from './claude-settings-template.js';
|
|
16
|
+
import { buildClaudeSettingsLocalJson, CLAUDE_SETTINGS_LOCAL_FILENAME, mergeTemplateOwnedHooks, templateContentMatches } from './claude-settings-template.js';
|
|
17
17
|
/** Read a file as text, or `undefined` when it cannot be read. */
|
|
18
18
|
function readTextIfPresent(filePath) {
|
|
19
19
|
try {
|
|
@@ -24,22 +24,115 @@ function readTextIfPresent(filePath) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* The
|
|
28
|
-
* is malformed or
|
|
29
|
-
* must not stop the materialization.
|
|
27
|
+
* The parsed top-level object of a serialized settings file, or `undefined`
|
|
28
|
+
* when the file is malformed or is not a JSON object. Tolerant on purpose: a
|
|
29
|
+
* bad on-disk file must not stop the materialization.
|
|
30
30
|
*/
|
|
31
|
-
function
|
|
31
|
+
function readSettingsObject(serialized) {
|
|
32
32
|
try {
|
|
33
33
|
const parsed = JSON.parse(serialized);
|
|
34
34
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
35
35
|
return undefined;
|
|
36
|
-
|
|
37
|
-
return typeof env === 'object' && env !== null && !Array.isArray(env) ? env : undefined;
|
|
36
|
+
return parsed;
|
|
38
37
|
}
|
|
39
38
|
catch {
|
|
40
39
|
return undefined;
|
|
41
40
|
}
|
|
42
41
|
}
|
|
42
|
+
/** The `env` object of a serialized settings file, or `undefined`. */
|
|
43
|
+
function readEnvObject(serialized) {
|
|
44
|
+
const env = readSettingsObject(serialized)?.env;
|
|
45
|
+
return typeof env === 'object' && env !== null && !Array.isArray(env) ? env : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The top-level keys this function's template is allowed to DECIDE. Every other
|
|
49
|
+
* key on disk belongs to whoever put it there and is carried across verbatim.
|
|
50
|
+
*
|
|
51
|
+
* - `hooks` — the tree this function exists to keep in sync, ENTRY BY ENTRY.
|
|
52
|
+
* `peaks workspace init` converges a consumer's file on the current
|
|
53
|
+
* release's handler set, and `templateContentMatches` (the drift detector
|
|
54
|
+
* that decides whether to rewrite at all) asks whether every entry the
|
|
55
|
+
* generated tree declares is present — not whether the trees are equal.
|
|
56
|
+
* Letting the disk win outright would make the rewrite a no-op that reports
|
|
57
|
+
* `refreshed` forever AND would never deliver a changed handler; letting the
|
|
58
|
+
* template win outright is what deleted the auto-compact hook. See
|
|
59
|
+
* `mergeHooksTree` / `mergeTemplateOwnedHooks` for the rule that does
|
|
60
|
+
* neither.
|
|
61
|
+
* - `env` — jointly owned with `peaks hooks install`, which unions the user's
|
|
62
|
+
* exemption globs into it. Handled as a union below, not by either side
|
|
63
|
+
* winning outright.
|
|
64
|
+
*
|
|
65
|
+
* This is deliberately an owned-key LIST and not a preserved-key whitelist: the
|
|
66
|
+
* direction matters. A whitelist drops every key it was not told about — which
|
|
67
|
+
* is how `permissions` was lost — whereas anything absent from this list is
|
|
68
|
+
* preserved by default, including keys no release of peaks-loop knows about.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ `hooks` USED TO BE OWNED WHOLE — every entry in it was deleted by the next
|
|
71
|
+
* `peaks workspace init` unless the template declared it, silently:
|
|
72
|
+
* `templateContentMatches` saw the extra entry, answered "drifted", and the
|
|
73
|
+
* rewrite emitted `{...template}`. That was not hypothetical.
|
|
74
|
+
* `.claude/settings.local.json` has a second writer of peaks' OWN hooks:
|
|
75
|
+
* `installAutoCompactHook` (`src/services/hooks/auto-compact-hook-install.ts`),
|
|
76
|
+
* reached from `peaks code auto-compact` on an adapter declaring
|
|
77
|
+
* `compactPathway: 'ide-native'` — which `claude-code` does. Measured on a
|
|
78
|
+
* throwaway project root (rid 2026-09-13-two-decisions item ②):
|
|
79
|
+
*
|
|
80
|
+
* init (written, 3 PreToolUse entries: Write|Edit|MultiEdit, Bash, Bash)
|
|
81
|
+
* → installAutoCompactHook (installed, 4: … | Bash|Task)
|
|
82
|
+
* → init again (REFRESHED, 3: … ) ← the Bash|Task entry is gone
|
|
83
|
+
*
|
|
84
|
+
* and nothing re-installs it: the hook's whole job was to fire on the next
|
|
85
|
+
* Bash/Task call, so once it is deleted the auto-compact contract stops
|
|
86
|
+
* silently.
|
|
87
|
+
*
|
|
88
|
+
* FIXED 2026-09-13 (user-decided): this template now owns only the entries IT
|
|
89
|
+
* DECLARES. `mergeHooksTree` below unions the rest of the on-disk `hooks` tree
|
|
90
|
+
* across verbatim, and `templateContentMatches` — the drift detector — was
|
|
91
|
+
* changed in the same slice from "the trees are identical" to "every entry the
|
|
92
|
+
* generated tree declares is present". The two halves are one change: the merge
|
|
93
|
+
* alone would leave the comparator comparing a 3-entry generated tree against a
|
|
94
|
+
* 4-entry file on EVERY init and reporting `refreshed` forever, which is the
|
|
95
|
+
* state whole-key ownership existed to prevent. See
|
|
96
|
+
* `mergeTemplateOwnedHooks` in `claude-settings-template.ts` for the ownership
|
|
97
|
+
* rule and `templateContentMatches` for the containment rule it implies.
|
|
98
|
+
*/
|
|
99
|
+
const TEMPLATE_OWNED_KEYS = new Set(['hooks', 'env']);
|
|
100
|
+
/** `template`'s own keys, then every on-disk key the template does not own. */
|
|
101
|
+
function carryUserOwnedKeys(onDisk, template) {
|
|
102
|
+
const merged = { ...template };
|
|
103
|
+
for (const [key, value] of Object.entries(onDisk)) {
|
|
104
|
+
if (TEMPLATE_OWNED_KEYS.has(key))
|
|
105
|
+
continue;
|
|
106
|
+
merged[key] = value;
|
|
107
|
+
}
|
|
108
|
+
merged.hooks = mergeHooksTree(onDisk.hooks, template.hooks);
|
|
109
|
+
return merged;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Merge the on-disk `hooks` tree with the template's, one event at a time.
|
|
113
|
+
*
|
|
114
|
+
* Only the events the template DECLARES are merged (and only their declared
|
|
115
|
+
* entries — see `mergeTemplateOwnedHooks`); every other event, and every other
|
|
116
|
+
* key under `hooks`, is carried across from the disk untouched. The template
|
|
117
|
+
* currently declares `PreToolUse` alone, so this is what keeps a hand-added or
|
|
118
|
+
* future-installer `SessionStart` entry in this file instead of deleting it —
|
|
119
|
+
* the latent half of the hazard the header describes.
|
|
120
|
+
*/
|
|
121
|
+
function mergeHooksTree(onDiskHooks, templateHooks) {
|
|
122
|
+
const onDisk = isPlainRecord(onDiskHooks) ? { ...onDiskHooks } : {};
|
|
123
|
+
const template = isPlainRecord(templateHooks) ? templateHooks : {};
|
|
124
|
+
for (const [event, declared] of Object.entries(template)) {
|
|
125
|
+
const current = onDisk[event];
|
|
126
|
+
onDisk[event] = Array.isArray(declared)
|
|
127
|
+
? mergeTemplateOwnedHooks(Array.isArray(current) ? current : [], declared)
|
|
128
|
+
: declared;
|
|
129
|
+
}
|
|
130
|
+
return onDisk;
|
|
131
|
+
}
|
|
132
|
+
/** A JSON object, as opposed to a null / array / primitive. */
|
|
133
|
+
function isPlainRecord(value) {
|
|
134
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
135
|
+
}
|
|
43
136
|
/**
|
|
44
137
|
* The peaks-managed snippet appended to the consumer project's
|
|
45
138
|
* `.peaks/.gitignore` so the local-only settings file never lands
|
|
@@ -102,8 +195,18 @@ export async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks)
|
|
|
102
195
|
// the rewrite this function is about to do, or a refresh would silently
|
|
103
196
|
// drop someone else's exemptions. The template's own row is added on top, so
|
|
104
197
|
// the result is a union either way.
|
|
198
|
+
//
|
|
199
|
+
// The same argument applies to every OTHER top-level key on disk, which the
|
|
200
|
+
// rewrite used to drop wholesale: `{ ...template }` emitted only the keys the
|
|
201
|
+
// template declares, so a user's `permissions.allow`, `statusLine`, `model`,
|
|
202
|
+
// … were deleted by an init that had no opinion about them. The user's
|
|
203
|
+
// permission rules are not this function's to remove, and the removal was
|
|
204
|
+
// silent — the envelope reported only `refreshed`. See `carryUserOwnedKeys`
|
|
205
|
+
// for which keys the template still decides.
|
|
206
|
+
const onDisk = existing === undefined ? undefined : readSettingsObject(existing);
|
|
105
207
|
const onDiskEnv = existing === undefined ? undefined : readEnvObject(existing);
|
|
106
|
-
const
|
|
208
|
+
const merged = onDisk === undefined ? template : carryUserOwnedKeys(onDisk, template);
|
|
209
|
+
const serialized = JSON.stringify(withExternalGateExemptions(onDiskEnv === undefined ? merged : { ...merged, env: onDiskEnv }), null, 2) + '\n';
|
|
107
210
|
// Always drop (or self-heal) a copy of the template under .peaks/
|
|
108
211
|
// so the --no-claude-hooks recovery flow has a known source-of-truth
|
|
109
212
|
// on disk. The file is gitignored by the snippet below.
|
|
@@ -169,6 +272,17 @@ export async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks)
|
|
|
169
272
|
* Returns the action taken so the caller can surface it in the
|
|
170
273
|
* envelope. Read failures are treated as drift so a malformed
|
|
171
274
|
* on-disk file always self-heals on the next init.
|
|
275
|
+
*
|
|
276
|
+
* WHAT IS COMPARED (rid 2026-09-13-two-decisions item ②): the copy is checked
|
|
277
|
+
* against the TEMPLATE'S OWN entries — `buildClaudeSettingsLocalJson()` — not
|
|
278
|
+
* against `serialized`, the merged LOCAL file content it is written from. This
|
|
279
|
+
* file is a copy of the template (its name and this doc both say so), so
|
|
280
|
+
* "is it current?" is a question about the template's entries only; asking it
|
|
281
|
+
* against the merged local file made the copy report `refreshed` once for every
|
|
282
|
+
* entry another writer had added to `.claude/settings.local.json` — drift noise
|
|
283
|
+
* about a file the copy does not own, on the very init that is supposed to be a
|
|
284
|
+
* no-op. With entry-containment semantics the copy is current as soon as it
|
|
285
|
+
* declares every template entry, whatever else it carries.
|
|
172
286
|
*/
|
|
173
287
|
async function writeOfflineTemplateCopy(projectRoot, serialized) {
|
|
174
288
|
const copyPath = join(projectRoot, '.peaks', '.claude-settings-template.json');
|
|
@@ -178,7 +292,8 @@ async function writeOfflineTemplateCopy(projectRoot, serialized) {
|
|
|
178
292
|
try {
|
|
179
293
|
const { readFile } = await import('node:fs/promises');
|
|
180
294
|
const existing = await readFile(copyPath, 'utf8');
|
|
181
|
-
|
|
295
|
+
const declared = JSON.stringify(buildClaudeSettingsLocalJson());
|
|
296
|
+
if (templateContentMatches(declared, existing)) {
|
|
182
297
|
action = 'already-current';
|
|
183
298
|
}
|
|
184
299
|
else {
|
|
@@ -6,6 +6,7 @@ import { getSessionIdCanonical, setCurrentSessionBinding, setSessionMeta } from
|
|
|
6
6
|
import { updateCallerBindingSessionId } from '../session/caller-binding-service.js';
|
|
7
7
|
import { resolveCallerProjection } from '../session/resolve-caller-id.js';
|
|
8
8
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
9
|
+
import { assertWritableProjectRoot } from '../config/config-safety.js';
|
|
9
10
|
/**
|
|
10
11
|
* Slice 2026-09-10 (rid=rebind-must-update-caller-binding): repoint the
|
|
11
12
|
* calling process's own per-caller binding at the session id this init just
|
|
@@ -180,6 +181,13 @@ export function validateSessionId(sessionId) {
|
|
|
180
181
|
}
|
|
181
182
|
export async function initWorkspace(options) {
|
|
182
183
|
validateSessionId(options.sessionId);
|
|
184
|
+
// Before anything is created: a project tree must never be materialized into
|
|
185
|
+
// the user's home directory. `--project .` from a fresh terminal (whose cwd
|
|
186
|
+
// is `$HOME`) resolved to `$HOME` and did exactly that — `.peaks/`,
|
|
187
|
+
// `.gitignore`, `.claude/settings.local.json` and a codegraph index, all in
|
|
188
|
+
// the user's home. Enforced here as well as at the CLI so a non-CLI caller
|
|
189
|
+
// (`peaks upgrade`) cannot reach the same write by another door.
|
|
190
|
+
assertWritableProjectRoot(options.projectRoot);
|
|
183
191
|
// Phase 6 refactor (slice 2026-06-05-change-id-as-unit-of-work) +
|
|
184
192
|
// slice 006 (2026-06-06-change-folder-simplify-and-lazy-role-subdirs) +
|
|
185
193
|
// slice 2026-06-22-top-level-change-id-cleanup (2.8.3):
|
|
@@ -14,8 +14,9 @@ export function removeRegisteredWorktree(input) {
|
|
|
14
14
|
execFileSync('git', ['worktree', 'remove', '--force', input.worktreePath], {
|
|
15
15
|
cwd: input.projectRoot,
|
|
16
16
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
windowsHide: true,
|
|
17
18
|
});
|
|
18
|
-
execFileSync('git', ['worktree', 'prune'], { cwd: input.projectRoot, stdio: 'ignore' });
|
|
19
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: input.projectRoot, stdio: 'ignore', windowsHide: true });
|
|
19
20
|
return { removed: !existsSync(input.worktreePath), strategy: 'git' };
|
|
20
21
|
}
|
|
21
22
|
catch (error) {
|
|
@@ -24,7 +25,7 @@ export function removeRegisteredWorktree(input) {
|
|
|
24
25
|
}
|
|
25
26
|
try {
|
|
26
27
|
rmSync(windowsLongPath(input.worktreePath), { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
27
|
-
execFileSync('git', ['worktree', 'prune'], { cwd: input.projectRoot, stdio: 'ignore' });
|
|
28
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: input.projectRoot, stdio: 'ignore', windowsHide: true });
|
|
28
29
|
return { removed: !existsSync(input.worktreePath), strategy: 'windows-long-path' };
|
|
29
30
|
}
|
|
30
31
|
catch (fallbackError) {
|
package/dist/shared/process.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
export async function execCommand(command, args, options) {
|
|
3
3
|
return new Promise((resolve, reject) => {
|
|
4
|
-
execFile(command, args, { cwd: options?.cwd, env: options?.env }, (error, stdout) => {
|
|
4
|
+
execFile(command, args, { cwd: options?.cwd, env: options?.env, windowsHide: true }, (error, stdout) => {
|
|
5
5
|
if (error) {
|
|
6
6
|
reject(error);
|
|
7
7
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.48",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -102,10 +102,10 @@
|
|
|
102
102
|
"picomatch": "4.0.4",
|
|
103
103
|
"yaml": "^2.9.0",
|
|
104
104
|
"zod": "^4.4.3",
|
|
105
|
-
"peaks-loop-internal-runtime": "0.0.
|
|
106
|
-
"peaks-loop-shared": "0.0.
|
|
107
|
-
"peaks-loop-shared
|
|
108
|
-
"peaks-loop-mut": "0.1.
|
|
105
|
+
"peaks-loop-internal-runtime": "0.0.33",
|
|
106
|
+
"peaks-loop-shared-channel": "0.0.50",
|
|
107
|
+
"peaks-loop-shared": "0.0.82",
|
|
108
|
+
"peaks-loop-mut": "0.1.46"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@changesets/cli": "2.31.1",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"test:dev": "vitest run tests/unit",
|
|
142
142
|
"test:dev:cli": "vitest run tests/unit",
|
|
143
143
|
"test:unit": "vitest run tests/unit",
|
|
144
|
-
"test:integration": "vitest run tests/integration",
|
|
144
|
+
"test:integration": "vitest run --config vitest.config.integration.ts tests/integration",
|
|
145
145
|
"test:capability-guard": "vitest run --config vitest.config.integration.ts tests/integration/capability-guard",
|
|
146
146
|
"test:cli": "vitest run tests/unit",
|
|
147
147
|
"test:workflow": "vitest run tests/unit",
|
package/scripts/watch.mjs
CHANGED
|
@@ -30,7 +30,9 @@ export function runCommand(command, args, options = {}) {
|
|
|
30
30
|
let child;
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
// `windowsHide` is a default, not a lock: a caller that passes its own
|
|
34
|
+
// value in `options` still wins, because the spread comes after it.
|
|
35
|
+
child = spawn(command, args, { stdio: 'inherit', windowsHide: true, ...options });
|
|
34
36
|
} catch (error) {
|
|
35
37
|
rejectPromise(error);
|
|
36
38
|
return;
|
|
@@ -12,7 +12,7 @@ metadata:
|
|
|
12
12
|
- schema: .peaks/project-scan/audit-output-schema.md
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
<!-- peaks:loop-hygiene —
|
|
15
|
+
<!-- peaks:loop-hygiene — HAND-MAINTAINED block, NOT generated; byte-identical in every SKILL.md, enforced by tests/unit/skills/loop-hygiene-block.test.ts. Edit one, edit all 22. -->
|
|
16
16
|
|
|
17
17
|
## Loop hygiene (every turn — MANDATORY)
|
|
18
18
|
|
|
@@ -8,7 +8,7 @@ description: |
|
|
|
8
8
|
visibility: internal
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
<!-- peaks:loop-hygiene —
|
|
11
|
+
<!-- peaks:loop-hygiene — HAND-MAINTAINED block, NOT generated; byte-identical in every SKILL.md, enforced by tests/unit/skills/loop-hygiene-block.test.ts. Edit one, edit all 22. -->
|
|
12
12
|
|
|
13
13
|
## Loop hygiene (every turn — MANDATORY)
|
|
14
14
|
|
|
@@ -122,7 +122,7 @@ Then display: `Peaks-Loop Skill: peaks-prd | Peaks-Loop Gate: startup | Next: <o
|
|
|
122
122
|
|
|
123
123
|
## Mandatory per-request artifact
|
|
124
124
|
|
|
125
|
-
Every PRD invocation — feature, bug, refactor, clarification — must write a durable artifact at `.peaks/_runtime/<session-id>/prd/requests/<request-id>.md`. The artifact is the canonical trace; the chat transcript is not. Handoff to RD/UI/QA is blocked while the artifact is missing or in `draft` state. After user confirmation, the **immutable handoff** is written separately at `.peaks/_runtime/<sid>/prd/handoff
|
|
125
|
+
Every PRD invocation — feature, bug, refactor, clarification — must write a durable artifact at `.peaks/_runtime/<session-id>/prd/requests/<request-id>.md`. The artifact is the canonical trace; the chat transcript is not. Handoff to RD/UI/QA is blocked while the artifact is missing or in `draft` state. After user confirmation, the **immutable handoff** is written separately at `.peaks/_runtime/<sid>/prd/handoff-<rid>.md` (Step 5.5 below) — sha256-locked, schemaVersion: 2 — and is the source of truth for RD, QA, and the 4 audit sub-agents (code-reviewer / security-reviewer / karpathy-reviewer / qa-test-cases-writer).
|
|
126
126
|
|
|
127
127
|
Use `<request-id>` of the form `YYYY-MM-DD-<kebab-slug>` (or whatever id the user assigned) so PRD/UI/RD/QA/SC can cross-link the same request.
|
|
128
128
|
|
|
@@ -198,15 +198,17 @@ peaks request show <request-id> --role prd --project <repo> --json
|
|
|
198
198
|
|
|
199
199
|
# 5.5 — write the immutable handoff (sha256-locked; BLOCKING before RD/QA handoff)
|
|
200
200
|
# Reads the PRD request artifact body, computes sha256, writes a v2.11.0 handoff
|
|
201
|
-
# at .peaks/_runtime/<sid>/prd/handoff
|
|
202
|
-
# sub-agents consume as the authoritative source of truth.
|
|
203
|
-
#
|
|
201
|
+
# at .peaks/_runtime/<sid>/prd/handoff-<rid>.md that downstream RD + QA + 4 audit
|
|
202
|
+
# sub-agents consume as the authoritative source of truth. The rid is part of
|
|
203
|
+
# the filename: one capsule per SLICE, so a second slice in the same session
|
|
204
|
+
# cannot overwrite the first's. Dry-run by default (omit --apply) so the
|
|
205
|
+
# operator can review before commit.
|
|
204
206
|
peaks prd handoff init \
|
|
205
207
|
--rid <request-id> --sid <session-id> --change-id <change-id> \
|
|
206
208
|
--body "@.peaks/_runtime/<session-id>/prd/requests/<request-id>.md" \
|
|
207
209
|
--goals <G-ids> --ac <AC-ids> --preserve <P-ids> \
|
|
208
210
|
[--project <repo>] [--apply]
|
|
209
|
-
peaks prd handoff verify --path .peaks/_runtime/<session-id>/prd/handoff
|
|
211
|
+
peaks prd handoff verify --path .peaks/_runtime/<session-id>/prd/handoff-<request-id>.md
|
|
210
212
|
|
|
211
213
|
peaks skill presence:clear --project <repo> # handoff complete, remove presence indicator
|
|
212
214
|
```
|
|
@@ -8,7 +8,7 @@ description: |
|
|
|
8
8
|
visibility: internal
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
<!-- peaks:loop-hygiene —
|
|
11
|
+
<!-- peaks:loop-hygiene — HAND-MAINTAINED block, NOT generated; byte-identical in every SKILL.md, enforced by tests/unit/skills/loop-hygiene-block.test.ts. Edit one, edit all 22. -->
|
|
12
12
|
|
|
13
13
|
## Loop hygiene (every turn — MANDATORY)
|
|
14
14
|
|
|
@@ -104,9 +104,9 @@ Project-level security + perf plans live at `.peaks/_runtime/<sessionId>/qa/secu
|
|
|
104
104
|
|
|
105
105
|
## QA fan-out (业务 only — v2.11.0 D1)
|
|
106
106
|
|
|
107
|
-
When peaks-qa is the **main loop** (i.e. it is the active skill and is about to run its own sub-agent dispatch, rather than being a sub-agent itself), it fans out only the **business verification** sub-agent: `qa-business`. Security and performance review are **NOT** peaks-qa's responsibility in v2.11.0 — they are owned by peaks-rd's 4-way audit fan-out (code-review + security-review + perf-baseline + karpathy-review) and the rd-side evidence files (`
|
|
107
|
+
When peaks-qa is the **main loop** (i.e. it is the active skill and is about to run its own sub-agent dispatch, rather than being a sub-agent itself), it fans out only the **business verification** sub-agent: `qa-business`. Security and performance review are **NOT** peaks-qa's responsibility in v2.11.0 — they are owned by peaks-rd's 4-way audit fan-out (code-review + security-review + perf-baseline + karpathy-review) and the rd-side evidence files (`audit/security-<rid>.md`, `audit/perf-<rid>.md`). peaks-qa reads those files by reference; it does NOT re-do them.
|
|
108
108
|
|
|
109
|
-
> **v2.15.0+ 校准:** `qa-business` 只跑业务/产品视角的 6 项验收清单(业务流程 / 需求覆盖 / 边界 case / UI 装配 / 异常态语调 / 能上线吗),**不跑技术指标**(覆盖率 / 性能 / 安全)。技术指标由 RD 4-way fan-out 自决,QA 只读 `
|
|
109
|
+
> **v2.15.0+ 校准:** `qa-business` 只跑业务/产品视角的 6 项验收清单(业务流程 / 需求覆盖 / 边界 case / UI 装配 / 异常态语调 / 能上线吗),**不跑技术指标**(覆盖率 / 性能 / 安全)。技术指标由 RD 4-way fan-out 自决,QA 只读 `audit/security-<rid>.md` + `audit/perf-<rid>.md`。详见 `.peaks/memory/peaks-loop-slice-review-and-qa-perspective.md`。
|
|
110
110
|
|
|
111
111
|
If the PRD or project warrants it, subdivide `qa-business` further into roles like `qa-business-api` / `qa-business-frontend` / `qa-business-regression`. Subdivision must stay ≤ 2 levels deep (RL-4).
|
|
112
112
|
|
|
@@ -128,7 +128,7 @@ When this skill is running in the main Claude session (not as a sub-agent), befo
|
|
|
128
128
|
- verify API behavior and frontend behavior when either surface exists;
|
|
129
129
|
- generate a validation report with commands, browser evidence, findings, and residual risks.
|
|
130
130
|
|
|
131
|
-
**Out of scope (v2.11.0 D1/D4):** peaks-qa does **not** own security review or performance review. Those are owned by peaks-rd's audit fan-out (sub-agents `security-review` and `perf-baseline`) and the rd-side evidence files. peaks-qa reads `
|
|
131
|
+
**Out of scope (v2.11.0 D1/D4):** peaks-qa does **not** own security review or performance review. Those are owned by peaks-rd's audit fan-out (sub-agents `security-review` and `perf-baseline`) and the rd-side evidence files. peaks-qa reads `audit/security-<rid>.md` and `audit/perf-<rid>.md` by reference; it does NOT produce `qa/security-findings.md` or `qa/performance-findings.md` of its own.
|
|
132
132
|
|
|
133
133
|
## Mandatory per-request artifact
|
|
134
134
|
|
|
@@ -148,7 +148,7 @@ See `references/qa-runbook.md` for the full 10-step runbook (steps #0–#9) with
|
|
|
148
148
|
|
|
149
149
|
You cannot declare a phase complete from memory. CLI enforcement: the gates below are ALSO enforced by `peaks request transition`, which fails with `code: PREREQUISITES_MISSING` if any are absent. Per-type required files: feature / refactor → test-cases + test-reports + security-findings + performance-findings; bugfix → test-cases + test-reports + security-findings (perf optional); config → security-findings only; docs / chore → none.
|
|
150
150
|
|
|
151
|
-
Gate index: A (test-cases), A2 (tests executed), A3 (security reference — v2.
|
|
151
|
+
Gate index: A (test-cases), A2 (tests executed), A3 (security reference — v2.12.0+: read audit/security-<rid>.md), A4 (performance reference — v2.12.0+: read audit/perf-<rid>.md), B (test-reports with results), C (all 3 QA files present before verdict: test-cases + test-reports + requests — security/perf evidence live under audit/), D (browser screenshots), E (acceptance coverage scan), F (QA artifact lint).
|
|
152
152
|
|
|
153
153
|
→ see `references/qa-transition-gates.md` for the full per-gate contract + `ls` / `grep` shell snippets.
|
|
154
154
|
|
|
@@ -194,7 +194,7 @@ Every QA invocation must produce a test-report artifact at `.peaks/_runtime/<ses
|
|
|
194
194
|
|
|
195
195
|
## Mandatory validation gates
|
|
196
196
|
|
|
197
|
-
QA cannot pass a change until the report contains evidence for every applicable gate. The 9 gates (0 test-case generation, 1 test-report, 2 unit tests, 3 API validation, 4 frontend browser validation, 5 browser-error feedback loop, 8 library version regressions, 9 validation report, 10 acceptance coverage) are mapped to Peaks-Loop Gates A/A2/B/C/D/E/F. **v2.11.0 D1/D4 trim:** Gates A3 (security) and A4 (performance) are no longer peaks-qa's responsibility — security review and performance baseline live under peaks-rd's audit fan-out (
|
|
197
|
+
QA cannot pass a change until the report contains evidence for every applicable gate. The 9 gates (0 test-case generation, 1 test-report, 2 unit tests, 3 API validation, 4 frontend browser validation, 5 browser-error feedback loop, 8 library version regressions, 9 validation report, 10 acceptance coverage) are mapped to Peaks-Loop Gates A/A2/B/C/D/E/F. **v2.11.0 D1/D4 trim:** Gates A3 (security) and A4 (performance) are no longer peaks-qa's responsibility — security review and performance baseline live under peaks-rd's audit fan-out (audit/security-<rid>.md + audit/perf-<rid>.md) and are cited by reference from the test report.
|
|
198
198
|
|
|
199
199
|
If Playwright MCP is unavailable, the LLM checks its own tool list for the Playwright MCP server entry; if absent, the LLM tells the user the install command (`claude mcp add playwright -- npx @playwright/mcp@latest` for Claude Code) and marks the gate blocked with the missing capability. Screenshots, logs, manual steps, or other tools must not substitute for the mandatory frontend browser gate. Do not silently downgrade frontend validation to API-only testing.
|
|
200
200
|
|
|
@@ -298,4 +298,4 @@ QA contracts to assert on the L2 surface (a minimal acceptance test set):
|
|
|
298
298
|
|
|
299
299
|
## Sub-role detached mode (Phase C, slice 2026-08-10)
|
|
300
300
|
|
|
301
|
-
QA sub-roles (`qa-business`, `qa-perf`, `qa-security`, `qa-business-api`, `qa-business-frontend`, `qa-business-regression`) accept `--mode detached --vendor <vendor>` for parallel test-case execution. Detached mode spawns a real OS process via `peaks sub-agent dispatch --mode detached`, isolated from the orchestrator session. Use detached mode when the test-case writer's expected runtime exceeds 60s OR it processes ≥ 20 source files. The default remains `in-process` (backward compat; existing 106+ dispatch tests untouched). When `--mode detached` is used, the child QA agent receives the
|
|
301
|
+
QA sub-roles (`qa-business`, `qa-perf`, `qa-security`, `qa-business-api`, `qa-business-frontend`, `qa-business-regression`) accept `--mode detached --vendor <vendor>` for parallel test-case execution. Detached mode spawns a real OS process via `peaks sub-agent dispatch --mode detached`, isolated from the orchestrator session. Use detached mode when the test-case writer's expected runtime exceeds 60s OR it processes ≥ 20 source files. The default remains `in-process` (backward compat; existing 106+ dispatch tests untouched). When `--mode detached` is used, the child QA agent receives the `<peaks-auto-compact>` marker (corrected 2026-09-13, rid `2026-09-13-defects-e`): it asks the child to persist durable state to `.peaks/_runtime/<sid>/detached/<rid>/compact/<n>.json` before its own harness compacts the session — the child cannot compact itself, and nothing is spliced back into its prompt. See `peaks-code/references/sub-agent-dispatch.md` §"Detached Mode" for the full contract.
|
|
@@ -36,8 +36,8 @@ peaks openspec validate <change-id> --project <repo> --prefer-external --json
|
|
|
36
36
|
# 5. EXECUTE tests against the actual implementation — Peaks-Loop Gate A2
|
|
37
37
|
# Run the project test command. Record output. Tests on paper are worthless.
|
|
38
38
|
# NOTE (v2.11.0 D1/D4): Security review + performance check are NOT run by peaks-qa.
|
|
39
|
-
# They are owned by
|
|
40
|
-
#
|
|
39
|
+
# They are owned by the independent audit skills and surface as audit/security-<rid>.md
|
|
40
|
+
# and audit/perf-<rid>.md under .peaks/_runtime/<sessionId>/audit/. Read them by reference;
|
|
41
41
|
# do NOT re-do them or create qa/security-findings.md / qa/performance-findings.md.
|
|
42
42
|
|
|
43
43
|
# 6. write test-report — MANDATORY, write to .peaks/_runtime/<sessionId>/qa/test-reports/<request-id>.md
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| config | (none) | `qa/test-reports/<rid>.md` |
|
|
12
12
|
| docs / chore | (none) | (none) |
|
|
13
13
|
|
|
14
|
-
Security and performance evidence surface under `
|
|
14
|
+
Security and performance evidence surface under `audit/security-<rid>.md` and `audit/perf-<rid>.md` (the independent `peaks-security-audit` / `peaks-perf-audit` skills, v2.12.0+; slice `2026-09-14-audit-artifact-rid-scoping` put the rid in the filename so two slices in one session cannot overwrite each other's evidence) and are referenced by reference from the QA test report body. The older `rd/security-review.md` / `rd/perf-baseline.md` and the ridless `audit/security.md` / `audit/perf.md` remain accepted back-compat tiers. The pre-v2.11.0 `qa/security-findings.md` / `qa/performance-findings.md` files are no longer required; existing ones are kept for auditability but ignored by the gate.
|
|
15
15
|
|
|
16
16
|
**Peaks-Loop Gate A — After test-case generation:**
|
|
17
17
|
```bash
|
|
@@ -29,18 +29,18 @@ npx vitest run --changed --reporter=verbose 2>&1 | tail -30
|
|
|
29
29
|
|
|
30
30
|
**Peaks-Loop Gate A3 — Security review referenced (v2.11.0 D1/D4: read-only reference, NOT a separate QA file):**
|
|
31
31
|
```bash
|
|
32
|
-
# peaks-qa does NOT own a qa/security-findings.md.
|
|
33
|
-
#
|
|
34
|
-
grep -E "rd/security-review
|
|
32
|
+
# peaks-qa does NOT own a qa/security-findings.md. The security audit produces
|
|
33
|
+
# audit/security-<rid>.md; QA references it by path in the test report body.
|
|
34
|
+
grep -E "audit/security|rd/security-review|security-review" .peaks/_runtime/<sessionId>/qa/test-reports/<rid>.md 2>&1
|
|
35
35
|
# Expected: at least one reference to the rd-side security review.
|
|
36
36
|
# Empty → BLOCKED: the test report must cite where security evidence lives.
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
**Peaks-Loop Gate A4 — Performance baseline referenced (v2.11.0 D1/D4: read-only reference, NOT a separate QA file):**
|
|
40
40
|
```bash
|
|
41
|
-
# peaks-qa does NOT own a qa/performance-findings.md.
|
|
42
|
-
#
|
|
43
|
-
grep -E "rd/perf-baseline
|
|
41
|
+
# peaks-qa does NOT own a qa/performance-findings.md. The perf audit produces
|
|
42
|
+
# audit/perf-<rid>.md; QA references it by path in the test report body.
|
|
43
|
+
grep -E "audit/perf|rd/perf-baseline|perf-baseline" .peaks/_runtime/<sessionId>/qa/test-reports/<rid>.md 2>&1
|
|
44
44
|
# Expected: at least one reference to the rd-side perf baseline.
|
|
45
45
|
# Empty → BLOCKED: the test report must cite where perf evidence lives.
|
|
46
46
|
```
|