peaks-loop 4.0.34 → 4.0.36
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 +34 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +57 -2
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +65 -3
- package/dist/cli/commands/dispatch-commands.js +19 -5
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/memory-commands.d.ts +59 -0
- package/dist/cli/commands/memory-commands.js +195 -19
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/context-schema.d.ts +1 -1
- package/dist/services/context/memory-index-reader.d.ts +26 -0
- package/dist/services/context/memory-index-reader.js +62 -30
- package/dist/services/context/memory-preflight-config.d.ts +33 -0
- package/dist/services/context/memory-preflight-config.js +32 -2
- package/dist/services/context/memory-preflight-service.d.ts +20 -1
- package/dist/services/context/memory-preflight-service.js +198 -31
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/job/job-types.d.ts +3 -3
- package/dist/services/memory/memory-ingest-service.d.ts +79 -0
- package/dist/services/memory/memory-ingest-service.js +225 -0
- package/dist/services/memory/memory-rotate-service.d.ts +88 -0
- package/dist/services/memory/memory-rotate-service.js +373 -0
- package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
- package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
- package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
- package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
- package/dist/services/memory/project-memory-service/index/search.js +14 -24
- package/dist/services/memory/project-memory-service/index.d.ts +7 -3
- package/dist/services/memory/project-memory-service/index.js +6 -2
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +80 -3
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +167 -28
- package/dist/services/memory/project-memory-service/types.d.ts +31 -1
- package/dist/services/memory/project-memory-service/types.js +76 -1
- package/dist/services/preferences/preferences-types.d.ts +14 -0
- package/dist/services/preferences/preferences-types.js +8 -0
- package/dist/services/share/run-state-contract.d.ts +1 -1
- package/package.json +5 -5
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +9 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-code/references/runbook.md +6 -0
- package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -1,20 +1,67 @@
|
|
|
1
1
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
2
2
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
3
3
|
import { loadMemoryIndex, searchMemory } from '../../services/memory/memory-search-service.js';
|
|
4
|
+
import { executeMemoryReindex, VALID_PROJECT_MEMORY_KINDS } from '../../services/memory/project-memory-service.js';
|
|
5
|
+
import { boundedNames, fitSummaryToBytes } from '../../services/context/summary-view.js';
|
|
6
|
+
import { executeMemoryIngest } from '../../services/memory/memory-ingest-service.js';
|
|
7
|
+
import { executeMemoryRotate } from '../../services/memory/memory-rotate-service.js';
|
|
4
8
|
import { pickFromList } from '../../services/fuzzy-matching/fzf-pick-service.js';
|
|
5
9
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
6
10
|
import { getErrorMessage, printResult } from '../cli-helpers.js';
|
|
7
11
|
import { join } from 'node:path';
|
|
8
|
-
const VALID_KINDS =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const VALID_KINDS = VALID_PROJECT_MEMORY_KINDS;
|
|
13
|
+
/**
|
|
14
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
15
|
+
* `memory reindex`. The full report's arrays stay on disk / in the default
|
|
16
|
+
* envelope; this replaces them with `{count, names}` views (≤ 2 KB).
|
|
17
|
+
*/
|
|
18
|
+
export function buildMemoryReindexSummary(report) {
|
|
19
|
+
const view = {
|
|
20
|
+
view: 'summary',
|
|
21
|
+
apply: report.apply,
|
|
22
|
+
projectRoot: report.projectRoot,
|
|
23
|
+
memoryDir: report.memoryDir,
|
|
24
|
+
indexPath: report.indexPath,
|
|
25
|
+
memoryMdPath: report.memoryMdPath,
|
|
26
|
+
scannedFiles: report.scannedFiles,
|
|
27
|
+
indexed: report.indexed,
|
|
28
|
+
indexedByKind: report.indexedByKind,
|
|
29
|
+
unclassified: boundedNames(report.unclassified.map((u) => `${u.name}${u.rawKind === null ? '' : ` (${u.rawKind})`}`)),
|
|
30
|
+
nameConflicts: boundedNames(report.nameConflicts.map((c) => c.name)),
|
|
31
|
+
orphanIndex: boundedNames(report.orphanIndex.map((o) => o.name)),
|
|
32
|
+
orphanDisk: boundedNames(report.orphanDisk.map((p) => p.split(/[\\/]/).pop() ?? p)),
|
|
33
|
+
memoryMd: report.memoryMd,
|
|
34
|
+
writtenFiles: boundedNames(report.writtenFiles.map((p) => p.split(/[\\/]/).pop() ?? p)),
|
|
35
|
+
};
|
|
36
|
+
return fitSummaryToBytes(view);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Slice B: bounded view of `memory list`. `count` is the true total; `names`
|
|
40
|
+
* carries `name (kind)` labels for the first N entries.
|
|
41
|
+
*/
|
|
42
|
+
export function buildMemoryListSummary(data) {
|
|
43
|
+
const label = (e) => `${e.name} (${e.kind})`;
|
|
44
|
+
const view = {
|
|
45
|
+
view: 'summary',
|
|
46
|
+
indexPath: data.snapshot.indexPath,
|
|
47
|
+
version: data.snapshot.version,
|
|
48
|
+
updatedAt: data.snapshot.updatedAt,
|
|
49
|
+
total: data.entries.length,
|
|
50
|
+
kindFilter: data.kindFilter ?? null,
|
|
51
|
+
entries: boundedNames(data.entries.map(label)),
|
|
52
|
+
};
|
|
53
|
+
if (data.pickedOutputPath !== null) {
|
|
54
|
+
view.picked = boundedNames(data.pickedEntries.map(label));
|
|
55
|
+
view.pickedOutputPath = data.pickedOutputPath;
|
|
56
|
+
view.fzfVersion = data.fzfVersion;
|
|
57
|
+
}
|
|
58
|
+
return fitSummaryToBytes(view);
|
|
59
|
+
}
|
|
60
|
+
function resolveMemoryProjectRoot(project) {
|
|
61
|
+
return project !== undefined
|
|
62
|
+
? resolveCanonicalProjectRoot(project)
|
|
63
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
64
|
+
}
|
|
18
65
|
export async function runMemoryList(io, options) {
|
|
19
66
|
const projectRoot = options.project !== undefined
|
|
20
67
|
? resolveCanonicalProjectRoot(options.project)
|
|
@@ -64,15 +111,28 @@ export async function runMemoryList(io, options) {
|
|
|
64
111
|
if (entries.length === 0) {
|
|
65
112
|
nextActions.push('No entries match; run `peaks memory extract` to build the index from memory/*.md files.');
|
|
66
113
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
114
|
+
// Slice B: `--summary` swaps the full entry array for a bounded
|
|
115
|
+
// `{count, names}` view. The default (no flag) envelope is byte-identical
|
|
116
|
+
// to before — the flag is strictly opt-in.
|
|
117
|
+
const data = options.summary === true
|
|
118
|
+
? buildMemoryListSummary({
|
|
119
|
+
snapshot,
|
|
120
|
+
entries,
|
|
121
|
+
kindFilter,
|
|
122
|
+
pickedEntries,
|
|
123
|
+
pickedOutputPath,
|
|
124
|
+
fzfVersion,
|
|
125
|
+
})
|
|
126
|
+
: {
|
|
127
|
+
indexPath: snapshot.indexPath,
|
|
128
|
+
version: snapshot.version,
|
|
129
|
+
updatedAt: snapshot.updatedAt,
|
|
130
|
+
total: entries.length,
|
|
131
|
+
kindFilter: kindFilter ?? null,
|
|
132
|
+
entries,
|
|
133
|
+
...(options.pick === true ? { picked: pickedEntries, pickedOutputPath, fzfVersion } : {})
|
|
134
|
+
};
|
|
135
|
+
printResult(io, ok('memory.list', data, warnings, nextActions), options.json);
|
|
76
136
|
}
|
|
77
137
|
catch (error) {
|
|
78
138
|
const message = getErrorMessage(error);
|
|
@@ -124,3 +184,119 @@ export async function runMemorySearch(io, options) {
|
|
|
124
184
|
process.exitCode = 1;
|
|
125
185
|
}
|
|
126
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* `peaks memory reindex` — rebuild `.peaks/memory/index.json` from disk and
|
|
189
|
+
* regenerate `MEMORY.md`. Dry-run by default; `--apply` writes.
|
|
190
|
+
*/
|
|
191
|
+
export async function runMemoryReindex(io, options) {
|
|
192
|
+
const projectRoot = resolveMemoryProjectRoot(options.project);
|
|
193
|
+
if (options.dryRun === true && options.apply === true) {
|
|
194
|
+
printResult(io, fail('memory.reindex', 'INVALID_MEMORY_REINDEX_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview the drift report, or pass --apply to rebuild']), options.json);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const report = executeMemoryReindex({ projectRoot, apply: options.apply === true });
|
|
200
|
+
const nextActions = [];
|
|
201
|
+
if (options.apply !== true) {
|
|
202
|
+
nextActions.push('Preview only — re-run with --apply to rebuild index.json and regenerate MEMORY.md.');
|
|
203
|
+
}
|
|
204
|
+
if (report.unclassified.length > 0) {
|
|
205
|
+
nextActions.push(`${report.unclassified.length} file(s) have no resolvable kind; add \`metadata.type\` (or \`kind:\`) to index them.`);
|
|
206
|
+
}
|
|
207
|
+
if (report.orphanIndex.length > 0) {
|
|
208
|
+
nextActions.push(`${report.orphanIndex.length} previous index entry(ies) point at missing files; they are dropped from the rebuilt index.`);
|
|
209
|
+
}
|
|
210
|
+
if (report.nameConflicts.length > 0) {
|
|
211
|
+
nextActions.push(`${report.nameConflicts.length} name collision(s) across files; both entries are kept — rename one file to disambiguate.`);
|
|
212
|
+
}
|
|
213
|
+
// Slice B: `--summary` keeps the scalar drift counts + names-of-first-N;
|
|
214
|
+
// the full report (with every unclassified/orphan path) stays available
|
|
215
|
+
// by omitting the flag. Default shape is unchanged.
|
|
216
|
+
const data = options.summary === true
|
|
217
|
+
? buildMemoryReindexSummary(report)
|
|
218
|
+
: report;
|
|
219
|
+
printResult(io, ok('memory.reindex', data, [], nextActions), options.json);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const message = getErrorMessage(error);
|
|
223
|
+
const code = error.code ?? 'MEMORY_REINDEX_FAILED';
|
|
224
|
+
printResult(io, fail('memory.reindex', code, message, { projectRoot }, ['Check that the project has a readable .peaks/memory directory']), options.json);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* `peaks memory rotate` — tier-driven retention for `.peaks/memory/`.
|
|
230
|
+
* Implements the sediment pruning policy (tier 1: archive, never delete).
|
|
231
|
+
* Dry-run by default; `--apply` moves tier-C candidates into `archived/`.
|
|
232
|
+
*/
|
|
233
|
+
export async function runMemoryRotate(io, options) {
|
|
234
|
+
const projectRoot = resolveMemoryProjectRoot(options.project);
|
|
235
|
+
if (options.dryRun === true && options.apply === true) {
|
|
236
|
+
printResult(io, fail('memory.rotate', 'INVALID_MEMORY_ROTATE_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview the rotation plan, or pass --apply to archive tier-C candidates']), options.json);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const report = executeMemoryRotate({ projectRoot, apply: options.apply === true });
|
|
242
|
+
const nextActions = [];
|
|
243
|
+
if (report.refused) {
|
|
244
|
+
nextActions.push(`Refused to apply: ${report.refusalReasons.join('; ')}`);
|
|
245
|
+
}
|
|
246
|
+
else if (options.apply !== true) {
|
|
247
|
+
nextActions.push('Preview only — re-run with --apply to move the tier-C candidates into archived/.');
|
|
248
|
+
}
|
|
249
|
+
if (report.excluded.length > 0) {
|
|
250
|
+
nextActions.push(`${report.excluded.length} candidate(s) excluded by a safety gate (see \`excluded\`).`);
|
|
251
|
+
}
|
|
252
|
+
const deleteCandidates = report.candidates.filter((candidate) => candidate.action === 'delete-candidate');
|
|
253
|
+
if (deleteCandidates.length > 0) {
|
|
254
|
+
nextActions.push(`${deleteCandidates.length} tier-D file(s) are delete-candidates only; peaks never deletes them — remove by hand if you are sure.`);
|
|
255
|
+
}
|
|
256
|
+
printResult(io, ok('memory.rotate', report, report.warnings, nextActions), options.json);
|
|
257
|
+
if (report.refused)
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const message = getErrorMessage(error);
|
|
262
|
+
const code = error.code ?? 'MEMORY_ROTATE_FAILED';
|
|
263
|
+
printResult(io, fail('memory.rotate', code, message, { projectRoot }, ['Check that the project has a readable .peaks/memory directory']), options.json);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `peaks memory ingest` — import memories written by the IDE-side agent into
|
|
269
|
+
* `.peaks/memory/`. The IDE-side source is read-only; dry-run by default.
|
|
270
|
+
*/
|
|
271
|
+
export async function runMemoryIngest(io, options) {
|
|
272
|
+
const projectRoot = resolveMemoryProjectRoot(options.project);
|
|
273
|
+
if (options.dryRun === true && options.apply === true) {
|
|
274
|
+
printResult(io, fail('memory.ingest', 'INVALID_MEMORY_INGEST_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview imports, or pass --apply to write them into .peaks/memory']), options.json);
|
|
275
|
+
process.exitCode = 1;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const report = executeMemoryIngest({
|
|
280
|
+
projectRoot,
|
|
281
|
+
...(options.sourceDir !== undefined ? { sourceDir: options.sourceDir } : {}),
|
|
282
|
+
apply: options.apply === true
|
|
283
|
+
});
|
|
284
|
+
const nextActions = [];
|
|
285
|
+
if (options.apply !== true && report.imported.length > 0) {
|
|
286
|
+
nextActions.push('Preview only — re-run with --apply to write these memories into .peaks/memory.');
|
|
287
|
+
}
|
|
288
|
+
if (report.conflicts.length > 0) {
|
|
289
|
+
nextActions.push(`${report.conflicts.length} conflict(s) left both copies in place; resolve them by hand.`);
|
|
290
|
+
}
|
|
291
|
+
if (report.needsClassification.length > 0) {
|
|
292
|
+
nextActions.push(`${report.needsClassification.length} file(s) could not be classified; add \`metadata.type\` to the source before re-running.`);
|
|
293
|
+
}
|
|
294
|
+
printResult(io, ok('memory.ingest', report, report.warnings, nextActions), options.json);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const message = getErrorMessage(error);
|
|
298
|
+
const code = error.code ?? 'MEMORY_INGEST_FAILED';
|
|
299
|
+
printResult(io, fail('memory.ingest', code, message, { projectRoot }, ['Check the --source-dir path and that .peaks/memory is writable']), options.json);
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { type RequestArtifactSummary } from '../../services/artifacts/request-artifact-service.js';
|
|
2
3
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
6
|
+
* `request list`. `count` is the true total; `names` carries
|
|
7
|
+
* `role/requestId (state)` labels for the first N entries. The full `items`
|
|
8
|
+
* array (with paths + timestamps) is one flag away — omit `--summary`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildRequestListSummary(items: readonly RequestArtifactSummary[]): Record<string, unknown>;
|
|
3
11
|
export declare function registerRequestCommands(program: Command, io: ProgramIO): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError } from 'commander';
|
|
2
2
|
import { createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES } from '../../services/artifacts/request-artifact-service.js';
|
|
3
|
+
import { boundedNames, fitSummaryToBytes } from '../../services/context/summary-view.js';
|
|
3
4
|
import { applyPerArtifactFormat, inferArtifactName, parseRequestType, parseRole, parseStateForRole, resolveDefaultFormat, VALID_ROLES, } from './request-format-helpers.js';
|
|
4
5
|
import { ConfirmationRequiredError } from '../../services/mode/mode-enforcement.js';
|
|
5
6
|
import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '../../services/mode/bypass-tracker.js';
|
|
@@ -9,6 +10,20 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
9
10
|
import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
|
|
10
11
|
import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
|
|
11
12
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
13
|
+
/**
|
|
14
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
15
|
+
* `request list`. `count` is the true total; `names` carries
|
|
16
|
+
* `role/requestId (state)` labels for the first N entries. The full `items`
|
|
17
|
+
* array (with paths + timestamps) is one flag away — omit `--summary`.
|
|
18
|
+
*/
|
|
19
|
+
export function buildRequestListSummary(items) {
|
|
20
|
+
const view = {
|
|
21
|
+
view: 'summary',
|
|
22
|
+
count: items.length,
|
|
23
|
+
items: boundedNames(items.map((i) => `${i.role}/${i.requestId} (${i.state})`)),
|
|
24
|
+
};
|
|
25
|
+
return fitSummaryToBytes(view);
|
|
26
|
+
}
|
|
12
27
|
export function registerRequestCommands(program, io) {
|
|
13
28
|
const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
|
|
14
29
|
addJsonOption(request
|
|
@@ -87,7 +102,8 @@ export function registerRequestCommands(program, io) {
|
|
|
87
102
|
.description('List per-request artifacts under a project workspace')
|
|
88
103
|
.requiredOption('--project <path>', 'target project root')
|
|
89
104
|
.option('--session-id <session>', 'limit to a specific session id')
|
|
90
|
-
.option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
105
|
+
.option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
106
|
+
.option('--summary', 'emit counts + names-of-first-N only (≤ 2 KB) instead of the full item array; the default envelope is unchanged')).action(async (options) => {
|
|
91
107
|
try {
|
|
92
108
|
const listOptions = { projectRoot: options.project };
|
|
93
109
|
if (options.sessionId !== undefined) {
|
|
@@ -97,7 +113,12 @@ export function registerRequestCommands(program, io) {
|
|
|
97
113
|
listOptions.role = options.role;
|
|
98
114
|
}
|
|
99
115
|
const items = await listRequestArtifacts(listOptions);
|
|
100
|
-
|
|
116
|
+
// Slice B: `--summary` is opt-in; the default `{count, items}` shape is
|
|
117
|
+
// byte-identical to before.
|
|
118
|
+
const data = options.summary === true
|
|
119
|
+
? buildRequestListSummary(items)
|
|
120
|
+
: { count: items.length, items };
|
|
121
|
+
printResult(io, ok('request.list', data), options.json);
|
|
101
122
|
}
|
|
102
123
|
catch (error) {
|
|
103
124
|
printResult(io, fail('request.list', 'REQUEST_LIST_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check project path before retrying']), options.json);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerDispatchCommand } from './dispatch-commands.js';
|
|
2
2
|
import { registerHeartbeatCommand } from './heartbeat-commands.js';
|
|
3
3
|
import { registerShareCommand, registerSharedReadCommand, registerAwaitCommand, registerFinalizeCommand } from './share-commands.js';
|
|
4
|
+
import { registerWavePlanCommand } from './wave-plan-commands.js';
|
|
4
5
|
// Re-export `validateRole` for backward compat — the integration test
|
|
5
6
|
// suite and any external callers still import it from this entry file.
|
|
6
7
|
// The canonical implementation now lives in `sub-agent-shared.ts`.
|
|
@@ -17,4 +18,5 @@ export function registerSubAgentCommands(program, io) {
|
|
|
17
18
|
registerSharedReadCommand(subAgent, io);
|
|
18
19
|
registerAwaitCommand(subAgent, io);
|
|
19
20
|
registerFinalizeCommand(subAgent, io); // D21
|
|
21
|
+
registerWavePlanCommand(subAgent, io); // §3 file-overlap wave planner
|
|
20
22
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks sub-agent wave-plan` — slice 2026-09-10-dispatch-token-and-swarm §3.
|
|
3
|
+
*
|
|
4
|
+
* Nested under the existing `sub-agent` verb (no new top-level verb, per the
|
|
5
|
+
* project-level rule that users never learn a new CLI surface; the LLM runs
|
|
6
|
+
* this on their behalf).
|
|
7
|
+
*
|
|
8
|
+
* Input: slice descriptors `{ slices: [{ id, files: [...] }] }` from a JSON
|
|
9
|
+
* file (`--slices <file>`) or inline (`--slices-json '<json>'`).
|
|
10
|
+
*
|
|
11
|
+
* Output: a machine-readable wave plan where every wave's slices have
|
|
12
|
+
* pairwise-disjoint file sets, plus the per-slice deferral reason naming the
|
|
13
|
+
* colliding file. The orchestrator uses this to fan out a level in parallel
|
|
14
|
+
* WITHOUT serializing on a shared file — the deferred slices simply run in
|
|
15
|
+
* the next wave.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
19
|
+
export interface WavePlanOptions {
|
|
20
|
+
slices?: string;
|
|
21
|
+
slicesJson?: string;
|
|
22
|
+
json?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function registerWavePlanCommand(parent: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { getErrorMessage, ok, fail } from 'peaks-loop-shared/result';
|
|
3
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
4
|
+
import { planFileOverlapWaves } from '../../services/dispatch/file-overlap-wave-planner.js';
|
|
5
|
+
export function registerWavePlanCommand(parent, io) {
|
|
6
|
+
addJsonOption(parent
|
|
7
|
+
.command('wave-plan')
|
|
8
|
+
.description('§3 file-overlap-aware scheduling: read slice descriptors ' +
|
|
9
|
+
'({slices:[{id,files:[]}]}) and emit a wave plan where every wave is ' +
|
|
10
|
+
'pairwise file-disjoint. Overlapping slices are deferred to later ' +
|
|
11
|
+
'waves with the colliding file named. Machine-readable envelope; the ' +
|
|
12
|
+
'LLM runs this, users never type it.')
|
|
13
|
+
.option('--slices <file>', 'path to a JSON file: { "slices": [{ "id": "s1", "files": ["src/a.ts"] }] }')
|
|
14
|
+
.option('--slices-json <json>', 'inline JSON with the same shape as --slices')).action((options) => {
|
|
15
|
+
const asJson = options.json === true;
|
|
16
|
+
const source = typeof options.slicesJson === 'string' && options.slicesJson.length > 0
|
|
17
|
+
? { text: options.slicesJson }
|
|
18
|
+
: typeof options.slices === 'string' && options.slices.length > 0
|
|
19
|
+
? readSlicesFile(options.slices)
|
|
20
|
+
: null;
|
|
21
|
+
if (source === null) {
|
|
22
|
+
printResult(io, fail('sub-agent.wave-plan', 'MISSING_INPUT', 'pass --slices <file> or --slices-json <json>', { ok: false, waves: [] }, ['Provide slice descriptors as { "slices": [{ "id": "s1", "files": ["src/a.ts"] }] }.']), asJson);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (source.error !== undefined) {
|
|
27
|
+
printResult(io, fail('sub-agent.wave-plan', 'INVALID_INPUT', source.error, { ok: false, waves: [] }, ['Check the JSON shape: { "slices": [{ "id": string, "files": string[] }] }.']), asJson);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(source.text);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
printResult(io, fail('sub-agent.wave-plan', 'INVALID_JSON', `input is not valid JSON: ${getErrorMessage(err)}`, { ok: false, waves: [] }, ['Fix the JSON syntax and re-run.']), asJson);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const descriptors = coerceDescriptors(parsed);
|
|
41
|
+
if (descriptors === null) {
|
|
42
|
+
printResult(io, fail('sub-agent.wave-plan', 'INVALID_SHAPE', 'expected { "slices": [{ "id": string, "files": string[] }] }', { ok: false, waves: [] }, ['Each entry needs a non-empty string id and an array of file paths.']), asJson);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const plan = planFileOverlapWaves(descriptors);
|
|
47
|
+
const warnings = plan.duplicateIds.length > 0
|
|
48
|
+
? [`DUPLICATE_SLICE_IDS: ${plan.duplicateIds.join(', ')} (first descriptor wins; the rest were not scheduled)`]
|
|
49
|
+
: [];
|
|
50
|
+
printResult(io, ok('sub-agent.wave-plan', {
|
|
51
|
+
envelopeVersion: '2.1.0',
|
|
52
|
+
ok: true,
|
|
53
|
+
sliceCount: plan.sliceCount,
|
|
54
|
+
waveCount: plan.waves.length,
|
|
55
|
+
waves: plan.waves,
|
|
56
|
+
duplicateIds: plan.duplicateIds,
|
|
57
|
+
maxParallelism: plan.waves.reduce((max, w) => Math.max(max, w.slices.length), 0)
|
|
58
|
+
}, warnings, [
|
|
59
|
+
plan.waves.length <= 1
|
|
60
|
+
? 'All slices are file-disjoint: dispatch them in a single wave.'
|
|
61
|
+
: `Dispatch wave 0 first, then each later wave after its predecessors finish; the deferred[] entries name the blocking file.`
|
|
62
|
+
]), asJson);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function readSlicesFile(path) {
|
|
66
|
+
try {
|
|
67
|
+
return { text: readFileSync(path, 'utf8') };
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return { text: '', error: `cannot read --slices file ${path}: ${getErrorMessage(err)}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function coerceDescriptors(parsed) {
|
|
74
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
75
|
+
return null;
|
|
76
|
+
const raw = parsed.slices;
|
|
77
|
+
if (!Array.isArray(raw))
|
|
78
|
+
return null;
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const item of raw) {
|
|
81
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item))
|
|
82
|
+
return null;
|
|
83
|
+
const id = item.id;
|
|
84
|
+
const files = item.files;
|
|
85
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
if (files !== undefined && (!Array.isArray(files) || files.some((f) => typeof f !== 'string'))) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
out.push({ id, files: files ?? [] });
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
@@ -68,6 +68,22 @@ export interface DispatchPromptInput {
|
|
|
68
68
|
* and before the memory/task content.
|
|
69
69
|
*/
|
|
70
70
|
freshContextBlock?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §4: session capsule
|
|
73
|
+
* published by the orchestrator through `peaks sub-agent share`.
|
|
74
|
+
*
|
|
75
|
+
* - `undefined` / `null` → no capsule pointer, no precedence line
|
|
76
|
+
* (byte-identical legacy prompt).
|
|
77
|
+
* - `{ batchId, key, bytes }` → a `shared-read` pointer plus the
|
|
78
|
+
* mandatory precedence line: the capsule is ADVISORY BACKGROUND only
|
|
79
|
+
* and the task spec wins on conflict. Nothing the sub-agent must act
|
|
80
|
+
* on may live only in the capsule.
|
|
81
|
+
*/
|
|
82
|
+
capsule?: {
|
|
83
|
+
readonly batchId: string;
|
|
84
|
+
readonly key: string;
|
|
85
|
+
readonly bytes: number;
|
|
86
|
+
} | null;
|
|
71
87
|
}
|
|
72
88
|
/**
|
|
73
89
|
* Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
|
|
@@ -91,7 +107,7 @@ export interface DispatchPromptInput {
|
|
|
91
107
|
* fallback path is `peaks worktree auth grant --rid <id> --reason <text>
|
|
92
108
|
* --ttl <5m>` (already shipped). Update the prose once `spawn` lands.
|
|
93
109
|
*/
|
|
94
|
-
export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusal (L1 worktree governance \u2014 locked 2026-07-29)\n\nYou MUST NOT follow the superpowers chain for worktree decisions:\n\n- superpowers:brainstorming \u2192 superpowers:writing-plans \u2192\n superpowers:subagent-driven-development \u2192 superpowers:using-git-worktrees\n\
|
|
110
|
+
export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusal (L1 worktree governance \u2014 locked 2026-07-29)\n\nYou MUST NOT follow the superpowers chain for worktree decisions:\n\n- superpowers:brainstorming \u2192 superpowers:writing-plans \u2192\n superpowers:subagent-driven-development \u2192 superpowers:using-git-worktrees\n\nThat chain ends with `git worktree add` (superpowers:using-git-worktrees SKILL.md line 96), which bypasses peaks-loop's L2 hook gate and L3 IDE `permissions.deny`. L3 denial is NOT sufficient \u2014 by then the chain has already taught raw `git worktree add`.\n\nFor worktree operations, use ONLY:\n\n- `peaks worktree spawn --rid <rid> --ttl <duration> --purpose <text>` (after rid-L2-extended ships)\n- until then: `peaks worktree auth grant --rid <id> --reason <text> --ttl <5m>`\n\nSuperpowers skills remain REFERENCE material for brainstorming / planning techniques, but MUST NOT be used as a workflow. peaks-rd's own PRD/RD artifacts (`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md`) are authoritative.\n\nIf the upstream superpowers chain suggests raw `git worktree add`:\n\n1. STOP\n2. Re-author the plan as a peaks-rd artifact\n3. Continue with `peaks worktree spawn` (or the auth-grant fallback)\n";
|
|
95
111
|
/**
|
|
96
112
|
* Slice 2026-08-01-subagent-merge-and-e2e (Task 8): the dispatch
|
|
97
113
|
* system prompt gains three lifecycle rules. The sub-agent must:
|
|
@@ -117,17 +133,38 @@ export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusa
|
|
|
117
133
|
* start, maximizing Anthropic prompt-cache prefix reuse (stable-first
|
|
118
134
|
* ordering).
|
|
119
135
|
*/
|
|
120
|
-
export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit
|
|
136
|
+
export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit; the parent session best-effort-kills it before merge-back.\n- Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10); your E2E work is duplicate effort.\n- Do NOT call `git merge`, `git pull`, `git rebase`, or `peaks worktree release`. The parent session owns the merge-back step.\n";
|
|
137
|
+
/**
|
|
138
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice C): cap the sub-agent's
|
|
139
|
+
* FINAL report.
|
|
140
|
+
*
|
|
141
|
+
* Why (measured, session 2026-09-07-session-245530): 20 sub-agent final
|
|
142
|
+
* reports cost ≈ 60 KB ≈ 15K tokens of the ORCHESTRATOR's window in one
|
|
143
|
+
* session — the reports, not the dispatch boilerplate, were the second-largest
|
|
144
|
+
* consumer. The sub-agent already writes a full artifact to disk; the report
|
|
145
|
+
* only needs to be the index into it.
|
|
146
|
+
*
|
|
147
|
+
* QUALITY GUARD (binding): the cap removes no information. Everything the
|
|
148
|
+
* parent needs to ACT on stays in the report; everything longer lives in the
|
|
149
|
+
* artifact the parent can `Read`. The five mandatory fields below are exactly
|
|
150
|
+
* the ones the orchestrator must have to decide the next gate.
|
|
151
|
+
*/
|
|
152
|
+
export declare const REPORT_CAP_BLOCK = "## Final report cap (mandatory)\n\nYour FINAL report to the parent MUST be \u2264 40 lines and \u2264 2 KB. Write any longer detail into the artifact file you already own \u2014 the parent can `Read` that file for the full detail, so nothing is lost. The report itself MUST still carry: changed files (one line each), the exact commands you ran, pass/fail counts, tsc status, and any blocker. Do NOT paste file contents, full tool output, or logs into the report.\n";
|
|
121
153
|
/**
|
|
122
|
-
* Compose the system-prompt body
|
|
123
|
-
*
|
|
154
|
+
* Compose the system-prompt body for a sub-agent dispatch.
|
|
155
|
+
*
|
|
156
|
+
* 2026-09-10-dispatch-block-d (Option D): the composer owns the Test Tool
|
|
157
|
+
* Detection injection — ONE unified block for every role, prepended first.
|
|
158
|
+
* Callers MUST NOT prepend `formatTestToolDetection()` themselves or the
|
|
159
|
+
* block is injected twice.
|
|
124
160
|
*
|
|
125
161
|
* Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
|
|
126
|
-
* controller brief): when the memory block is unavailable, the
|
|
127
|
-
* `formatTestToolDetection()\n\n
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
162
|
+
* controller brief): when the memory block is unavailable, the composed body is
|
|
163
|
+
* exactly `formatTestToolDetection() + "\n\n" + L1 + "\n" + LIFECYCLE +
|
|
164
|
+
* "\n" + REPORT_CAP + "\n" + contextBlock + taskBody`, so the unavailable
|
|
165
|
+
* branch MUST return `taskBody` unwrapped (NOT a `# title\n\n` wrap).
|
|
166
|
+
* (REPORT_CAP joined the stable prefix in slice
|
|
167
|
+
* 2026-09-10-context-audit-and-discipline, Slice C.)
|
|
131
168
|
* The contract holds for callers that do not pass `codegraphBlock` (all
|
|
132
169
|
* non-RD roles). Slice 2026-09-03-codegraph-preread deliberately inserts a
|
|
133
170
|
* codegraph structure block (or its fail-soft unavailable note) for RD
|
|
@@ -151,3 +188,23 @@ export declare function buildDispatchSystemPrompt(input: DispatchPromptInput): s
|
|
|
151
188
|
* byte-stable and trivially testable.
|
|
152
189
|
*/
|
|
153
190
|
export declare const CODEGRAPH_UNAVAILABLE_BLOCK = "## Codegraph structure\n\ncodegraph unavailable \u2014 proceeding on project-scan only.\n";
|
|
191
|
+
/** Binding phrases every dispatch prompt must contain, for every role. */
|
|
192
|
+
export declare const BINDING_RULE_TOKENS: readonly string[];
|
|
193
|
+
/**
|
|
194
|
+
* The runner-direct-path tokens: the refusal example, the two direct paths
|
|
195
|
+
* the block names (`peaks test --json` to introspect; PB-5, the repo-defined
|
|
196
|
+
* `test` / `test:*` scripts that are NOT gated), and the two pieces of
|
|
197
|
+
* quality guidance that must survive any compression — never assume a
|
|
198
|
+
* runner without asking the user as a last resort, and prefer
|
|
199
|
+
* `peaks test <file>` because it resolves the local binary Windows-aware.
|
|
200
|
+
*
|
|
201
|
+
* 2026-09-10-dispatch-block-d (Option D): there is no role split any more,
|
|
202
|
+
* so this set is asserted IDENTICALLY for every role. The runner EXAMPLES
|
|
203
|
+
* were removed as part of the unification — they were never rules.
|
|
204
|
+
*/
|
|
205
|
+
export declare const TEST_RUNNER_RULE_TOKENS: readonly string[];
|
|
206
|
+
/**
|
|
207
|
+
* Return the subset of `tokens` that `text` does NOT contain. Pure; used by
|
|
208
|
+
* the rule-presence guard and usable by any future prompt self-check.
|
|
209
|
+
*/
|
|
210
|
+
export declare function missingRuleTokens(text: string, tokens: readonly string[]): readonly string[];
|