peaks-loop 4.0.29 → 4.0.30
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 +14 -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/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/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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
|
|
3
|
+
* lease by shelling out to `peaks worktree spawn` (avoid re-implementing
|
|
4
|
+
* the lease-write + git-worktree-add sequence in this file). The CLI
|
|
5
|
+
* does the lease write, the git worktree add, AND the error handling;
|
|
6
|
+
* we just parse the JSON envelope and surface the leaseId + path.
|
|
7
|
+
*
|
|
8
|
+
* Throws on spawn failure; the caller converts the error to a
|
|
9
|
+
* ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
|
|
10
|
+
* dispatch is already async, the lease is a few hundred ms of FS work,
|
|
11
|
+
* and we need the leaseId before we build the dispatch record.
|
|
12
|
+
*/
|
|
13
|
+
export declare function spawnWorktreeLease(args: {
|
|
14
|
+
projectRoot: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
rid: string;
|
|
17
|
+
role: string;
|
|
18
|
+
purpose: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
leaseId: string;
|
|
21
|
+
path: string;
|
|
22
|
+
branch: string;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Slice 2026-07-29-worktree-l2-extended Part 12: container
|
|
27
|
+
* isolation bridge. Shells out to `peaks container spawn` to
|
|
28
|
+
* run `docker run` + write the container lease. Returns the
|
|
29
|
+
* leaseId the dispatch record needs to persist. The shape is
|
|
30
|
+
* a subset of the spawnWorktreeLease return (just leaseId);
|
|
31
|
+
* we do not need the path/branch/expiresAt for the container
|
|
32
|
+
* path because the envelope surfaces a different set of
|
|
33
|
+
* fields (image + containerId; see container-lease.ts).
|
|
34
|
+
*/
|
|
35
|
+
export declare function spawnContainerLease(args: {
|
|
36
|
+
projectRoot: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
rid: string;
|
|
39
|
+
role: string;
|
|
40
|
+
purpose: string;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
leaseId: string;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn as childProcessSpawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
|
|
4
|
+
* lease by shelling out to `peaks worktree spawn` (avoid re-implementing
|
|
5
|
+
* the lease-write + git-worktree-add sequence in this file). The CLI
|
|
6
|
+
* does the lease write, the git worktree add, AND the error handling;
|
|
7
|
+
* we just parse the JSON envelope and surface the leaseId + path.
|
|
8
|
+
*
|
|
9
|
+
* Throws on spawn failure; the caller converts the error to a
|
|
10
|
+
* ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
|
|
11
|
+
* dispatch is already async, the lease is a few hundred ms of FS work,
|
|
12
|
+
* and we need the leaseId before we build the dispatch record.
|
|
13
|
+
*/
|
|
14
|
+
export function spawnWorktreeLease(args) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = childProcessSpawn(process.execPath, [
|
|
17
|
+
// The compiled CLI lives in dist/cli/peaks.js. We pass the entry
|
|
18
|
+
// through node so the test suite (which also runs on the same
|
|
19
|
+
// process) and the production binary share the same path. When
|
|
20
|
+
// the binary is invoked as `peaks`, the package bin stub does
|
|
21
|
+
// this for us; here we explicitly use process.execPath + the
|
|
22
|
+
// resolved entry to avoid PATH surprises.
|
|
23
|
+
process.argv[1] ?? '',
|
|
24
|
+
'worktree', 'spawn',
|
|
25
|
+
'--rid', args.rid,
|
|
26
|
+
'--role', args.role,
|
|
27
|
+
'--purpose', args.purpose,
|
|
28
|
+
'--project', args.projectRoot,
|
|
29
|
+
'--session', args.sessionId,
|
|
30
|
+
'--json'
|
|
31
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
|
|
35
|
+
child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
|
|
36
|
+
child.on('error', (err) => reject(new Error(`worktree spawn subprocess failed: ${err.message}`)));
|
|
37
|
+
child.on('close', (code) => {
|
|
38
|
+
if (code !== 0) {
|
|
39
|
+
reject(new Error(`peaks worktree spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(stdout);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
reject(new Error(`peaks worktree spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
51
|
+
reject(new Error('peaks worktree spawn envelope is not an object'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const env = parsed;
|
|
55
|
+
if (env.ok !== true || !env.data?.lease) {
|
|
56
|
+
reject(new Error(`peaks worktree spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
resolve({
|
|
60
|
+
leaseId: env.data.lease.leaseId,
|
|
61
|
+
path: env.data.lease.path,
|
|
62
|
+
branch: env.data.lease.branch,
|
|
63
|
+
expiresAt: env.data.lease.expiresAt
|
|
64
|
+
});
|
|
65
|
+
// Part 47: unref via setImmediate so the close handler
|
|
66
|
+
// finishes first and Node's stdio 'end' events drain the
|
|
67
|
+
// stdout/stderr buffers before the parent releases the
|
|
68
|
+
// child handle. Without this microtask defer, the unref
|
|
69
|
+
// races the buffered stdout close and the test receives
|
|
70
|
+
// an empty JSON envelope.
|
|
71
|
+
setImmediate(() => { child.unref(); });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Slice 2026-07-29-worktree-l2-extended Part 12: container
|
|
77
|
+
* isolation bridge. Shells out to `peaks container spawn` to
|
|
78
|
+
* run `docker run` + write the container lease. Returns the
|
|
79
|
+
* leaseId the dispatch record needs to persist. The shape is
|
|
80
|
+
* a subset of the spawnWorktreeLease return (just leaseId);
|
|
81
|
+
* we do not need the path/branch/expiresAt for the container
|
|
82
|
+
* path because the envelope surfaces a different set of
|
|
83
|
+
* fields (image + containerId; see container-lease.ts).
|
|
84
|
+
*/
|
|
85
|
+
export function spawnContainerLease(args) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const child = childProcessSpawn(process.execPath, [
|
|
88
|
+
process.argv[1] ?? '',
|
|
89
|
+
'container', 'spawn',
|
|
90
|
+
'--rid', args.rid,
|
|
91
|
+
'--role', args.role,
|
|
92
|
+
'--purpose', args.purpose,
|
|
93
|
+
'--project', args.projectRoot,
|
|
94
|
+
'--session', args.sessionId,
|
|
95
|
+
'--json'
|
|
96
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
|
|
97
|
+
let stdout = '';
|
|
98
|
+
let stderr = '';
|
|
99
|
+
child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
|
|
100
|
+
child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
|
|
101
|
+
child.on('error', (err) => reject(new Error(`container spawn subprocess failed: ${err.message}`)));
|
|
102
|
+
child.on('close', (code) => {
|
|
103
|
+
if (code !== 0) {
|
|
104
|
+
reject(new Error(`peaks container spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(stdout);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
reject(new Error(`peaks container spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
116
|
+
reject(new Error('peaks container spawn envelope is not an object'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const env = parsed;
|
|
120
|
+
if (env.ok !== true || !env.data?.lease) {
|
|
121
|
+
reject(new Error(`peaks container spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve({ leaseId: env.data.lease.leaseId });
|
|
125
|
+
});
|
|
126
|
+
// See spawnWorktreeLease above for the rationale.
|
|
127
|
+
child.unref();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type EvidenceGenerateOptions = {
|
|
2
|
+
projectRoot: string;
|
|
3
|
+
rid: string;
|
|
4
|
+
title: string;
|
|
5
|
+
files: string[];
|
|
6
|
+
lineCounts: Record<string, string>;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
};
|
|
9
|
+
export type EvidenceGenerateResult = {
|
|
10
|
+
rid: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
sessionRoot: string;
|
|
13
|
+
handoffPath: string;
|
|
14
|
+
handoffHash: string;
|
|
15
|
+
writtenFiles: string[];
|
|
16
|
+
createdDirectories: string[];
|
|
17
|
+
};
|
|
18
|
+
/** Comma/space tolerant `--files` splitter. */
|
|
19
|
+
export declare function parseFiles(raw: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Parse `--line-counts "f=n;g=m"`. Accepts both `;` and `,` separators so
|
|
22
|
+
* the reference prototype's comma form and the task's semicolon form both
|
|
23
|
+
* work. `key=value` pairs without `=` are ignored (mirrors the prototype).
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseLineCounts(raw: string): Record<string, string>;
|
|
26
|
+
export declare function generateEvidence(options: EvidenceGenerateOptions): Promise<EvidenceGenerateResult>;
|
|
@@ -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
|
+
}
|