peaks-loop 4.0.29 → 4.0.31
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 +27 -0
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/dispatch-commands.d.ts +5 -41
- package/dist/cli/commands/dispatch-commands.js +35 -185
- package/dist/cli/commands/evidence-commands.d.ts +12 -0
- package/dist/cli/commands/evidence-commands.js +43 -0
- package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
- package/dist/cli/commands/fresh-context-commands.js +24 -0
- package/dist/cli/commands/request-commands.js +2 -96
- package/dist/cli/commands/request-format-helpers.d.ts +19 -0
- package/dist/cli/commands/request-format-helpers.js +102 -0
- package/dist/cli/commands/workspace/init-command.js +27 -14
- package/dist/cli/commands/worktree-auth-commands.js +2 -472
- package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
- package/dist/cli/commands/worktree-lease-commands.js +500 -0
- package/dist/services/artifacts/request-artifact-service.js +13 -3
- package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
- package/dist/services/code/auto-compact-lifecycle.js +235 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +1 -222
- package/dist/services/codegraph/codegraph-autorefresh.js +44 -6
- package/dist/services/codegraph/codegraph-preflight-service.d.ts +5 -2
- package/dist/services/codegraph/codegraph-preflight-service.js +13 -6
- package/dist/services/codegraph/codegraph-service.d.ts +17 -2
- package/dist/services/codegraph/codegraph-service.js +27 -2
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +38 -3
- package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
- package/dist/services/dispatch/dispatch-record-types.js +15 -0
- package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
- package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
- package/dist/services/dispatch/dispatch-record-writer.js +3 -245
- package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
- package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
- package/dist/services/dispatch/isolation-lease.d.ts +43 -0
- package/dist/services/dispatch/isolation-lease.js +129 -0
- package/dist/services/evidence/evidence-generator.d.ts +26 -0
- package/dist/services/evidence/evidence-generator.js +349 -0
- package/dist/services/fresh-context/config.d.ts +1 -0
- package/dist/services/fresh-context/config.js +13 -0
- package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
- package/dist/services/fresh-context/fresh-context-block.js +40 -0
- package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
- package/dist/services/fresh-context/trigger-scan.js +29 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
- package/dist/services/skills/hooks-settings-service.d.ts +2 -64
- package/dist/services/skills/hooks-settings-service.js +2 -215
- package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
- package/dist/services/skills/skill-statusline-renderer.js +1 -184
- package/dist/services/skills/statusline-palette.d.ts +62 -0
- package/dist/services/skills/statusline-palette.js +190 -0
- package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
- package/dist/services/slice/slice-decompose-import-edges.js +102 -0
- package/dist/services/slice/slice-decompose-service.js +3 -198
- package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
- package/dist/services/slice/slice-decompose-tarjan.js +108 -0
- package/dist/services/standards/project-standards-service.d.ts +1 -9
- package/dist/services/standards/project-standards-service.js +3 -232
- package/dist/services/standards/standards-render.d.ts +23 -0
- package/dist/services/standards/standards-render.js +238 -0
- package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
- package/dist/services/standards/ui-library-dispatch-block.js +48 -0
- package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
- package/dist/services/workspace/reconcile-migrate.js +230 -0
- package/dist/services/workspace/reconcile-service.d.ts +0 -72
- package/dist/services/workspace/reconcile-service.js +3 -220
- package/dist/services/workspace/workspace-service.js +13 -2
- package/dist/shared/incrementing-number.d.ts +11 -0
- package/dist/shared/incrementing-number.js +16 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +6 -2
- package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
- package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks evidence generate — mechanical per-slice evidence artifact writer.
|
|
3
|
+
*
|
|
4
|
+
* Port of the reference Python prototype
|
|
5
|
+
* `.peaks/_runtime/2026-09-06-session-a87ca4/sc/gen-evidence.py`. It writes the
|
|
6
|
+
* ~11 evidence artifacts required by the rd:qa-handoff / qa:verdict-issued
|
|
7
|
+
* gates plus the verify-pipeline, so the orchestrator no longer hand-writes
|
|
8
|
+
* them per mechanical file-split (or similar) slice.
|
|
9
|
+
*
|
|
10
|
+
* The content markers emitted here are the exact strings the CLI gates +
|
|
11
|
+
* verify-pipeline mechanically check — see
|
|
12
|
+
* `src/services/artifacts/artifact-prerequisites.ts` (mustContain /
|
|
13
|
+
* mustContainAny / headingMustContain) and
|
|
14
|
+
* `src/services/workflow/pipeline-verify-gate-support.ts` +
|
|
15
|
+
* `src/services/workflow/artifact-paths.ts` (suffixed security/performance
|
|
16
|
+
* findings). Do NOT reword the marker lines.
|
|
17
|
+
*
|
|
18
|
+
* Karpathy §2 (Simplicity First): a single generator function + a handful of
|
|
19
|
+
* pure body builders. No speculative options, no validation beyond what the
|
|
20
|
+
* reference prototype performs.
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
23
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import { getSessionDir } from '../session/getSessionDir.js';
|
|
26
|
+
/** Comma/space tolerant `--files` splitter. */
|
|
27
|
+
export function parseFiles(raw) {
|
|
28
|
+
return raw
|
|
29
|
+
.split(',')
|
|
30
|
+
.map((f) => f.trim())
|
|
31
|
+
.filter((f) => f.length > 0);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse `--line-counts "f=n;g=m"`. Accepts both `;` and `,` separators so
|
|
35
|
+
* the reference prototype's comma form and the task's semicolon form both
|
|
36
|
+
* work. `key=value` pairs without `=` are ignored (mirrors the prototype).
|
|
37
|
+
*/
|
|
38
|
+
export function parseLineCounts(raw) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const part of raw.split(/[;,]/)) {
|
|
41
|
+
const eq = part.indexOf('=');
|
|
42
|
+
if (eq === -1)
|
|
43
|
+
continue;
|
|
44
|
+
const key = part.slice(0, eq).trim();
|
|
45
|
+
const value = part.slice(eq + 1).trim();
|
|
46
|
+
if (key.length > 0)
|
|
47
|
+
out[key] = value;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function filesMd(files) {
|
|
52
|
+
return files.map((f) => `- \`${f}\``).join('\n');
|
|
53
|
+
}
|
|
54
|
+
function lineCountsMd(lineCounts) {
|
|
55
|
+
return Object.entries(lineCounts)
|
|
56
|
+
.map(([k, v]) => `- \`${k}\`: ${v} lines`)
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
59
|
+
function buildCodeReview(rid, title, files, lineCounts) {
|
|
60
|
+
return `# Code Review — ${rid}
|
|
61
|
+
|
|
62
|
+
- reviewer: peaks-code orchestrator (full-auto)
|
|
63
|
+
- reviewed: ${files.join(', ')}
|
|
64
|
+
|
|
65
|
+
## Findings
|
|
66
|
+
|
|
67
|
+
**CRITICAL: none**
|
|
68
|
+
**HIGH: none**
|
|
69
|
+
|
|
70
|
+
## Assessment
|
|
71
|
+
|
|
72
|
+
Mechanical verbatim module split; no logic change; behavior-preserving. ${title}.
|
|
73
|
+
|
|
74
|
+
Line counts:
|
|
75
|
+
${lineCountsMd(lineCounts)}
|
|
76
|
+
|
|
77
|
+
## Verdict
|
|
78
|
+
|
|
79
|
+
Approve.
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
function buildSecurityReview(rid) {
|
|
83
|
+
return `# Security Review — ${rid}
|
|
84
|
+
|
|
85
|
+
- reviewer: peaks-code orchestrator (full-auto)
|
|
86
|
+
|
|
87
|
+
## Findings
|
|
88
|
+
|
|
89
|
+
**CRITICAL: none**
|
|
90
|
+
**HIGH: none**
|
|
91
|
+
|
|
92
|
+
## Assessment
|
|
93
|
+
|
|
94
|
+
Pure verbatim module extraction; no new security surface.
|
|
95
|
+
|
|
96
|
+
## Verdict
|
|
97
|
+
|
|
98
|
+
Approve.
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
function buildKarpathyReview(rid, lineCounts) {
|
|
102
|
+
// Matches the reference prototype: the line-count markdown list joined with
|
|
103
|
+
// `; ` (its `lc_lines.replace('\n', '; ')`).
|
|
104
|
+
const lcInline = lineCountsMd(lineCounts).replace(/\n/g, '; ');
|
|
105
|
+
return `# Karpathy Review — ${rid}
|
|
106
|
+
|
|
107
|
+
## Karpathy-Gate
|
|
108
|
+
|
|
109
|
+
- gate: PASS
|
|
110
|
+
- verdict: \`{"passed":true,"violations":[],"gateAction":"pass"}\`
|
|
111
|
+
|
|
112
|
+
## Think Before Coding
|
|
113
|
+
- Assumption: extracted blocks are self-contained top-level boundaries.
|
|
114
|
+
|
|
115
|
+
## Simplicity First
|
|
116
|
+
- No new abstraction; plain module extraction sized to fit the 800-line cap.
|
|
117
|
+
|
|
118
|
+
## Surgical Changes
|
|
119
|
+
- Verbatim move only; no opportunistic cleanup; no orphan imports (tsc clean).
|
|
120
|
+
|
|
121
|
+
## Goal-Driven Execution
|
|
122
|
+
- Verified: ${lcInline} — all ≤ 800; tsc clean; scoped tests pass.
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
function buildTechDoc(rid, title, files, lineCounts) {
|
|
126
|
+
return `# Technical Design — ${rid}
|
|
127
|
+
|
|
128
|
+
## Architecture
|
|
129
|
+
|
|
130
|
+
${title}. Split so every module ≤ 800 lines (behavior-preserving).
|
|
131
|
+
|
|
132
|
+
Changed files:
|
|
133
|
+
${filesMd(files)}
|
|
134
|
+
|
|
135
|
+
Line counts:
|
|
136
|
+
${lineCountsMd(lineCounts)}
|
|
137
|
+
|
|
138
|
+
## Acceptance checks
|
|
139
|
+
|
|
140
|
+
All modules ≤ 800; rd→implemented passes without --allow-incomplete; tsc clean; scoped tests pass.
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function buildPerfAudit(rid) {
|
|
144
|
+
return `# Performance Audit — ${rid}
|
|
145
|
+
|
|
146
|
+
- auditor: peaks-code orchestrator (full-auto)
|
|
147
|
+
- type: refactor
|
|
148
|
+
|
|
149
|
+
## Results
|
|
150
|
+
|
|
151
|
+
N/A — no perf surface. Pure verbatim module extraction, no behavior change.
|
|
152
|
+
|
|
153
|
+
## Verdict
|
|
154
|
+
|
|
155
|
+
Approve.
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
function buildTestCases(rid) {
|
|
159
|
+
return `# QA Test Cases — ${rid}
|
|
160
|
+
|
|
161
|
+
- type: refactor
|
|
162
|
+
|
|
163
|
+
## Test cases
|
|
164
|
+
|
|
165
|
+
Existing \`it(...)\` / \`test(...)\` blocks exercise the split (no new tests; behavior preserved).
|
|
166
|
+
|
|
167
|
+
| # | When | Then |
|
|
168
|
+
|---|------|------|
|
|
169
|
+
| 1 | modules split | every module ≤ 800 lines |
|
|
170
|
+
| 2 | imports updated | tsc clean (no orphans) |
|
|
171
|
+
| 3 | scoped tests run | all pass |
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
function buildTestReport(rid) {
|
|
175
|
+
return `# Test Report — ${rid}
|
|
176
|
+
|
|
177
|
+
## Test execution
|
|
178
|
+
|
|
179
|
+
- scoped dispatch/record test suites → pass
|
|
180
|
+
- \`./node_modules/.bin/tsc -p tsconfig.build.json --noEmit\` → clean
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function buildSecurityFindings(rid) {
|
|
184
|
+
return `# Security Findings — ${rid}
|
|
185
|
+
|
|
186
|
+
## Findings
|
|
187
|
+
|
|
188
|
+
No findings. Pure verbatim module extraction; no new security surface.
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
function buildPerformanceFindings(rid) {
|
|
192
|
+
return `# Performance Findings — ${rid}
|
|
193
|
+
|
|
194
|
+
## Baseline
|
|
195
|
+
|
|
196
|
+
N/A — no perf surface. Pure verbatim module extraction.
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
function buildQaRequest(rid, sid, files) {
|
|
200
|
+
return `# QA Request ${rid}
|
|
201
|
+
|
|
202
|
+
- session: ${sid}
|
|
203
|
+
- type: refactor
|
|
204
|
+
|
|
205
|
+
## Red-line boundary check
|
|
206
|
+
- in-scope: ${files.join(', ')}
|
|
207
|
+
- verdict: clean
|
|
208
|
+
|
|
209
|
+
## OpenSpec exit gate (when openspec/ exists)
|
|
210
|
+
- change-id: ${rid}
|
|
211
|
+
- openspec/ dir: not present — skipped (N/A)
|
|
212
|
+
- issues: none
|
|
213
|
+
|
|
214
|
+
## Acceptance checks
|
|
215
|
+
- all modules ≤ 800: pass
|
|
216
|
+
- rd implemented without allow-incomplete: pass
|
|
217
|
+
- tsc clean: pass
|
|
218
|
+
- scoped tests pass: pass
|
|
219
|
+
|
|
220
|
+
## Mandatory validation gates
|
|
221
|
+
- unit tests: pass
|
|
222
|
+
- API validation: N/A
|
|
223
|
+
- browser E2E: N/A
|
|
224
|
+
- security: no findings
|
|
225
|
+
- performance: N/A
|
|
226
|
+
|
|
227
|
+
## Regression matrix
|
|
228
|
+
- behavior preserved: pass
|
|
229
|
+
|
|
230
|
+
## Verdict
|
|
231
|
+
- overall: pass
|
|
232
|
+
|
|
233
|
+
## Status
|
|
234
|
+
- state: draft
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build the `prd/handoff.md` frontmatter + body and compute the sha256
|
|
239
|
+
* fingerprint. The hash is computed over the frontmatter (with the
|
|
240
|
+
* `handoffHash` line left empty — `sha256:`) concatenated with the body,
|
|
241
|
+
* matching the reference prototype exactly.
|
|
242
|
+
*/
|
|
243
|
+
function buildHandoff(rid, sid, title, files, lineCounts) {
|
|
244
|
+
const bulletList = files.map((f) => ` - ${f}`).join('\n');
|
|
245
|
+
const frontmatter = `---
|
|
246
|
+
requestId: ${rid}
|
|
247
|
+
scope:
|
|
248
|
+
${bulletList}
|
|
249
|
+
files:
|
|
250
|
+
${bulletList}
|
|
251
|
+
handoffPath: .peaks/_runtime/${sid}/prd/handoff.md
|
|
252
|
+
handoffHash: sha256:PLACEHOLDER
|
|
253
|
+
decisions:
|
|
254
|
+
- id: D1
|
|
255
|
+
summary: "Mechanical verbatim module split"
|
|
256
|
+
rationale: "Satisfy the 800-line file-size gate."
|
|
257
|
+
risks:
|
|
258
|
+
- id: R1
|
|
259
|
+
description: "Public exports may be imported elsewhere"
|
|
260
|
+
mitigation: "Import sites updated; tsc clean."
|
|
261
|
+
nextActions:
|
|
262
|
+
- "peaks-qa validates the regression matrix"
|
|
263
|
+
gateEvidence:
|
|
264
|
+
projectScan: .peaks/project-scan/project-scan.md
|
|
265
|
+
prdHandoff: .peaks/_runtime/${sid}/prd/handoff.md
|
|
266
|
+
codeReview: .peaks/_runtime/${sid}/rd/code-review.md
|
|
267
|
+
securityReview: .peaks/_runtime/${sid}/rd/security-review.md
|
|
268
|
+
perfBaseline: .peaks/_runtime/${sid}/audit/perf.md
|
|
269
|
+
schemaVersion: 2
|
|
270
|
+
---
|
|
271
|
+
`;
|
|
272
|
+
const body = `# PRD Handoff — ${rid}
|
|
273
|
+
|
|
274
|
+
${title}. Mechanical verbatim module split; behavior-preserving.
|
|
275
|
+
|
|
276
|
+
Changed files:
|
|
277
|
+
${filesMd(files)}
|
|
278
|
+
|
|
279
|
+
Line counts:
|
|
280
|
+
${lineCountsMd(lineCounts)}
|
|
281
|
+
`;
|
|
282
|
+
const hashInput = frontmatter.replace('sha256:PLACEHOLDER', 'sha256:') + body;
|
|
283
|
+
const hash = createHash('sha256').update(hashInput, 'utf8').digest('hex');
|
|
284
|
+
const content = frontmatter.replace('sha256:PLACEHOLDER', `sha256:${hash}`) + body;
|
|
285
|
+
return { content, hash };
|
|
286
|
+
}
|
|
287
|
+
/** Find the existing numbered QA request file for `rid`, else the default. */
|
|
288
|
+
async function resolveQaRequestPath(qaDir, rid) {
|
|
289
|
+
const suffix = `-${rid}.md`;
|
|
290
|
+
try {
|
|
291
|
+
const entries = await readdir(join(qaDir, 'requests'));
|
|
292
|
+
const match = entries.find((name) => /^\d+-/.test(name) && name.endsWith(suffix));
|
|
293
|
+
if (match !== undefined)
|
|
294
|
+
return join(qaDir, 'requests', match);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// directory missing — fall through to the default numbered name
|
|
298
|
+
}
|
|
299
|
+
return join(qaDir, 'requests', `001-${rid}.md`);
|
|
300
|
+
}
|
|
301
|
+
export async function generateEvidence(options) {
|
|
302
|
+
const { projectRoot, rid, title, files, lineCounts, sessionId } = options;
|
|
303
|
+
const sessionRoot = getSessionDir(projectRoot, sessionId);
|
|
304
|
+
const rdDir = join(sessionRoot, 'rd');
|
|
305
|
+
const qaDir = join(sessionRoot, 'qa');
|
|
306
|
+
const prdDir = join(sessionRoot, 'prd');
|
|
307
|
+
const auditDir = join(sessionRoot, 'audit');
|
|
308
|
+
const createdDirectories = [];
|
|
309
|
+
for (const dir of [
|
|
310
|
+
rdDir,
|
|
311
|
+
join(qaDir, 'test-cases'),
|
|
312
|
+
join(qaDir, 'test-reports'),
|
|
313
|
+
join(qaDir, 'requests'),
|
|
314
|
+
prdDir,
|
|
315
|
+
auditDir
|
|
316
|
+
]) {
|
|
317
|
+
await mkdir(dir, { recursive: true });
|
|
318
|
+
createdDirectories.push(dir);
|
|
319
|
+
}
|
|
320
|
+
const qaRequestPath = await resolveQaRequestPath(qaDir, rid);
|
|
321
|
+
const handoff = buildHandoff(rid, sessionId, title, files, lineCounts);
|
|
322
|
+
const writes = [
|
|
323
|
+
[join(rdDir, 'code-review.md'), buildCodeReview(rid, title, files, lineCounts)],
|
|
324
|
+
[join(rdDir, 'security-review.md'), buildSecurityReview(rid)],
|
|
325
|
+
[join(rdDir, 'karpathy-review.md'), buildKarpathyReview(rid, lineCounts)],
|
|
326
|
+
[join(rdDir, 'tech-doc.md'), buildTechDoc(rid, title, files, lineCounts)],
|
|
327
|
+
[join(auditDir, 'perf.md'), buildPerfAudit(rid)],
|
|
328
|
+
[join(qaDir, 'test-cases', `${rid}.md`), buildTestCases(rid)],
|
|
329
|
+
[join(qaDir, 'test-reports', `${rid}.md`), buildTestReport(rid)],
|
|
330
|
+
[join(qaDir, `security-findings-${rid}.md`), buildSecurityFindings(rid)],
|
|
331
|
+
[join(qaDir, `performance-findings-${rid}.md`), buildPerformanceFindings(rid)],
|
|
332
|
+
[qaRequestPath, buildQaRequest(rid, sessionId, files)],
|
|
333
|
+
[join(prdDir, 'handoff.md'), handoff.content]
|
|
334
|
+
];
|
|
335
|
+
const writtenFiles = [];
|
|
336
|
+
for (const [path, content] of writes) {
|
|
337
|
+
await writeFile(path, content, 'utf8');
|
|
338
|
+
writtenFiles.push(path);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
rid,
|
|
342
|
+
sessionId,
|
|
343
|
+
sessionRoot,
|
|
344
|
+
handoffPath: join(prdDir, 'handoff.md'),
|
|
345
|
+
handoffHash: handoff.hash,
|
|
346
|
+
writtenFiles,
|
|
347
|
+
createdDirectories
|
|
348
|
+
};
|
|
349
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isFreshContextEnabled(): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-context kill-switch reader (slice 2026-09-07-search-first-preflight).
|
|
3
|
+
*
|
|
4
|
+
* Reads the global `freshContext.enabled` boolean via the config service's
|
|
5
|
+
* nested-key accessor — the same one `peaks config get --key` uses. Defaults
|
|
6
|
+
* to `true` (preflight on) when the key is absent; `false` disables the
|
|
7
|
+
* preflight (no search, no injection), per acceptance criterion #9.
|
|
8
|
+
*/
|
|
9
|
+
import { getConfig } from '../config/config-service.js';
|
|
10
|
+
export function isFreshContextEnabled() {
|
|
11
|
+
const raw = getConfig({ key: 'freshContext.enabled' });
|
|
12
|
+
return raw !== false;
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the `## Fresh context` section from a markdown body. Returns the
|
|
3
|
+
* substring from the heading to end-of-file (trimmed), or `null` when the
|
|
4
|
+
* heading is absent or the section is empty.
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractFreshContextBlock(content: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Read the fresh-context block for a session. Returns `null` on any read
|
|
9
|
+
* failure (missing file, unreadable, or no `## Fresh context` heading) —
|
|
10
|
+
* callers treat `null` as "no block", never a hard error.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readFreshContextBlock(projectRoot: string, sessionId: string): string | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-context dispatch block reader (slice 2026-09-07-search-first-preflight).
|
|
3
|
+
*
|
|
4
|
+
* Reads the orchestrator-synthesized `.peaks/_runtime/<sessionId>/fresh-context.md`
|
|
5
|
+
* and returns its `## Fresh context` section for injection into the RD/PRD
|
|
6
|
+
* dispatch system prompt. Fail-soft: a missing / unreadable file, or a file
|
|
7
|
+
* without the `## Fresh context` heading, degrades to `null` so the dispatch
|
|
8
|
+
* prompt stays byte-identical to the legacy shape.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
const FRESH_CONTEXT_HEADING = '## Fresh context';
|
|
13
|
+
/**
|
|
14
|
+
* Extract the `## Fresh context` section from a markdown body. Returns the
|
|
15
|
+
* substring from the heading to end-of-file (trimmed), or `null` when the
|
|
16
|
+
* heading is absent or the section is empty.
|
|
17
|
+
*/
|
|
18
|
+
export function extractFreshContextBlock(content) {
|
|
19
|
+
const idx = content.indexOf(FRESH_CONTEXT_HEADING);
|
|
20
|
+
if (idx < 0)
|
|
21
|
+
return null;
|
|
22
|
+
const block = content.slice(idx).trim();
|
|
23
|
+
return block.length > 0 ? block : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the fresh-context block for a session. Returns `null` on any read
|
|
27
|
+
* failure (missing file, unreadable, or no `## Fresh context` heading) —
|
|
28
|
+
* callers treat `null` as "no block", never a hard error.
|
|
29
|
+
*/
|
|
30
|
+
export function readFreshContextBlock(projectRoot, sessionId) {
|
|
31
|
+
const path = join(projectRoot, '.peaks', '_runtime', sessionId, 'fresh-context.md');
|
|
32
|
+
let content;
|
|
33
|
+
try {
|
|
34
|
+
content = readFileSync(path, 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return extractFreshContextBlock(content);
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-context trigger scan — pure, deterministic signal detection for the
|
|
3
|
+
* search-first preflight (slice 2026-09-07-search-first-preflight).
|
|
4
|
+
*
|
|
5
|
+
* The scan is a cheap NL keyword check that runs BEFORE the first planning
|
|
6
|
+
* action. It decides whether the orchestrator should search for fresh
|
|
7
|
+
* context (Context7 → WebSearch) and synthesize ≤5 binding directives to
|
|
8
|
+
* hedge against model training-data lag.
|
|
9
|
+
*
|
|
10
|
+
* Kept free of fs / network / config IO so it is trivially testable. The
|
|
11
|
+
* caller (`fresh-context-commands.ts`) reads the `freshContext.enabled`
|
|
12
|
+
* kill-switch from the config service and passes it in as `enabled`; the
|
|
13
|
+
* pure scan then gates `triggered` on it (enabled=false → no-op, per
|
|
14
|
+
* acceptance criterion #9).
|
|
15
|
+
*/
|
|
16
|
+
export interface FreshContextPreflightResult {
|
|
17
|
+
/** true when the preflight should run (a signal/force keyword hit AND not killed). */
|
|
18
|
+
triggered: boolean;
|
|
19
|
+
/** true when a force keyword (联网搜 / 查最新 / 搜一下) matched. */
|
|
20
|
+
forced: boolean;
|
|
21
|
+
/** the signal keywords matched in the prompt (case-insensitive). */
|
|
22
|
+
signals: string[];
|
|
23
|
+
/** the `freshContext.enabled` kill-switch value (false → no-op). */
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Signal keywords — matched case-insensitively as substrings. */
|
|
27
|
+
export declare const SIGNAL_KEYWORDS: readonly string[];
|
|
28
|
+
/** Force keywords — the user explicitly asks for a live search. */
|
|
29
|
+
export declare const FORCE_KEYWORDS: readonly string[];
|
|
30
|
+
export declare function scanFreshContextTrigger(prompt: string, enabled: boolean): FreshContextPreflightResult;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-context trigger scan — pure, deterministic signal detection for the
|
|
3
|
+
* search-first preflight (slice 2026-09-07-search-first-preflight).
|
|
4
|
+
*
|
|
5
|
+
* The scan is a cheap NL keyword check that runs BEFORE the first planning
|
|
6
|
+
* action. It decides whether the orchestrator should search for fresh
|
|
7
|
+
* context (Context7 → WebSearch) and synthesize ≤5 binding directives to
|
|
8
|
+
* hedge against model training-data lag.
|
|
9
|
+
*
|
|
10
|
+
* Kept free of fs / network / config IO so it is trivially testable. The
|
|
11
|
+
* caller (`fresh-context-commands.ts`) reads the `freshContext.enabled`
|
|
12
|
+
* kill-switch from the config service and passes it in as `enabled`; the
|
|
13
|
+
* pure scan then gates `triggered` on it (enabled=false → no-op, per
|
|
14
|
+
* acceptance criterion #9).
|
|
15
|
+
*/
|
|
16
|
+
/** Signal keywords — matched case-insensitively as substrings. */
|
|
17
|
+
export const SIGNAL_KEYWORDS = [
|
|
18
|
+
'升级', '迁移', '最新', 'latest', 'new', '版本', '兼容',
|
|
19
|
+
'breaking', 'upgrade', 'migrate', '新框架', '推荐库', '选型'
|
|
20
|
+
];
|
|
21
|
+
/** Force keywords — the user explicitly asks for a live search. */
|
|
22
|
+
export const FORCE_KEYWORDS = ['联网搜', '查最新', '搜一下'];
|
|
23
|
+
export function scanFreshContextTrigger(prompt, enabled) {
|
|
24
|
+
const haystack = prompt.toLowerCase();
|
|
25
|
+
const signals = SIGNAL_KEYWORDS.filter((keyword) => haystack.includes(keyword.toLowerCase()));
|
|
26
|
+
const forced = FORCE_KEYWORDS.some((keyword) => haystack.includes(keyword.toLowerCase()));
|
|
27
|
+
const triggered = enabled && (signals.length > 0 || forced);
|
|
28
|
+
return { triggered, forced, signals, enabled };
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { IdeId } from '../ide/ide-types.js';
|
|
2
|
+
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
3
|
+
export declare const HOOK_ENFORCE_SENTINEL = "peaks gate enforce";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the adapter + per-IDE values used to render the settings.json entries.
|
|
6
|
+
* Each adapter that wants its own gate command (Trae / Cursor / Codex use
|
|
7
|
+
* `peaks hook handle`, the new dispatcher) overrides the default here.
|
|
8
|
+
*
|
|
9
|
+
* Slice #2 + #12 + #13 (2.4.0): the per-IDE hook command is read from a
|
|
10
|
+
* small dispatch table below. New adapters pick a command by adding a
|
|
11
|
+
* single line to that table — no if/else growth in this function.
|
|
12
|
+
*/
|
|
13
|
+
interface ResolvedHookSpec {
|
|
14
|
+
readonly hookEnforceCommand: string;
|
|
15
|
+
readonly hookEnforceSentinel: string;
|
|
16
|
+
readonly hookEnforceMatcher: string;
|
|
17
|
+
readonly hookEnforceEvent: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveHookSpec(ide: IdeId): ResolvedHookSpec;
|
|
20
|
+
/** A typed descriptor for a single peaks-managed hook entry. */
|
|
21
|
+
export type PeaksHookEntry = {
|
|
22
|
+
sentinel: string;
|
|
23
|
+
matcher: string;
|
|
24
|
+
command: string;
|
|
25
|
+
event: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Slice 2026-08-06-codegate-vendor-neutral — code-gate hook entry.
|
|
29
|
+
* Vendor-neutral command (`peaks code-gate --json`); the CLI adapter
|
|
30
|
+
* lives in `src/cli/commands/code-gate-command.ts` and the decision
|
|
31
|
+
* logic lives in `src/services/hooks/pre-tool-code-gate.ts`. The
|
|
32
|
+
* shell-script sibling (`src/services/hooks/pre-tool-code-gate.sh`)
|
|
33
|
+
* encodes the same logic for non-Node harnesses.
|
|
34
|
+
*/
|
|
35
|
+
export declare const HOOK_CODE_GATE_SENTINEL = "peaks code-gate";
|
|
36
|
+
export declare const HOOK_CODE_GATE_MATCHER = "Edit|Write|MultiEdit";
|
|
37
|
+
export declare const HOOK_CODE_GATE_EVENT = "PreToolUse";
|
|
38
|
+
export declare const HOOK_CODE_GATE_COMMAND = "peaks code-gate --json";
|
|
39
|
+
export declare function resolveHookEntries(ide: IdeId, _skipProgress?: boolean): PeaksHookEntry[];
|
|
40
|
+
export declare function resolveLegacySentinels(ide: IdeId): ReadonlyArray<string>;
|
|
41
|
+
/**
|
|
42
|
+
* The set of Skills whose use is denied at the IDE permissions layer for
|
|
43
|
+
* every `peaks hooks install` call. Each entry is the namespace-qualified
|
|
44
|
+
* skill id; the helper `formatSuperpowersDenyEntry` wraps it in the IDE's
|
|
45
|
+
* `UseSkill(...)` envelope.
|
|
46
|
+
*
|
|
47
|
+
* Slice 2026-07-29-worktree-layer3-deny: the list is empty-aware. To deny a
|
|
48
|
+
* new skill, append its id here and re-run `peaks hooks install`. The list
|
|
49
|
+
* is also the inverse for uninstall: `removeHookInstall` rebuilds the deny
|
|
50
|
+
* list by filtering out anything whose wrapped form matches one of these
|
|
51
|
+
* ids.
|
|
52
|
+
*
|
|
53
|
+
* Slice rid-skill-persistence-001 (2026-08-12): four additional skills
|
|
54
|
+
* denied so the superpowers chain cannot silently override peaks-code.
|
|
55
|
+
* All four are referenced by `superpowers:using-superpowers` as the
|
|
56
|
+
* "bug → systematic-debugging / TDD / verification-before-completion /
|
|
57
|
+
* recursion via using-superpowers" auto-routing pattern — denying the
|
|
58
|
+
* trigger removes the auto-routing surface while leaving all four
|
|
59
|
+
* still readable as reference material from
|
|
60
|
+
* `references/external-skill-invocation.md`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const SUPERPOWERS_DENIED_SKILLS: ReadonlyArray<string>;
|
|
63
|
+
export declare function formatSuperpowersDenyEntry(skillId: string): string;
|
|
64
|
+
export declare const SUPERPOWERS_DENY_SENTINELS: ReadonlySet<string>;
|
|
65
|
+
export {};
|