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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--summary` — bounded, additive views of large CLI envelopes.
|
|
3
|
+
*
|
|
4
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B, part 1).
|
|
5
|
+
*
|
|
6
|
+
* Rationale (measured, session 2026-09-07-session-245530): dumping a full
|
|
7
|
+
* `peaks memory reindex --json` array four times cost ≈ 160 KB ≈ 40K tokens
|
|
8
|
+
* of orchestrator context — roughly 4% of a 1M window for ONE command
|
|
9
|
+
* repeated. The default envelopes stay exactly as they are (back-compat);
|
|
10
|
+
* `--summary` is an OPT-IN view that keeps counts + names-of-first-N and
|
|
11
|
+
* drops the per-entry bodies, which remain on disk and are re-readable with
|
|
12
|
+
* the full command. No information is destroyed — only the in-context copy
|
|
13
|
+
* shrinks.
|
|
14
|
+
*
|
|
15
|
+
* Byte bound: every summary object is passed through `fitSummaryToBytes`,
|
|
16
|
+
* which shrinks string arrays (longest first) until the JSON serialization is
|
|
17
|
+
* ≤ `SUMMARY_DATA_MAX_BYTES` (2 KB minus a small envelope reserve, so the
|
|
18
|
+
* PRINTED envelope stays ≤ `SUMMARY_MAX_BYTES`). Scalars are never touched,
|
|
19
|
+
* so counts and paths stay exact.
|
|
20
|
+
*/
|
|
21
|
+
/** Hard ceiling for the PRINTED `--summary` envelope, in UTF-8 bytes. */
|
|
22
|
+
export const SUMMARY_MAX_BYTES = 2048;
|
|
23
|
+
/**
|
|
24
|
+
* Reserve for the envelope wrapper (`ok`/`command`/`warnings`/`nextActions`/
|
|
25
|
+
* error fields) that the CLI adds around `data`. The builders cap `data` at
|
|
26
|
+
* `SUMMARY_DATA_MAX_BYTES` so the whole printed envelope stays ≤ 2 KB.
|
|
27
|
+
*/
|
|
28
|
+
export const SUMMARY_ENVELOPE_RESERVE_BYTES = 512;
|
|
29
|
+
/** Cap applied to the summary `data` object itself (pretty-printed size). */
|
|
30
|
+
export const SUMMARY_DATA_MAX_BYTES = SUMMARY_MAX_BYTES - SUMMARY_ENVELOPE_RESERVE_BYTES;
|
|
31
|
+
/** Per-name character cap — a name is a label, not a document. */
|
|
32
|
+
export const SUMMARY_NAME_MAX_CHARS = 120;
|
|
33
|
+
/** How many names each command asks for before the byte fitter trims. */
|
|
34
|
+
export const SUMMARY_INITIAL_NAMES = 40;
|
|
35
|
+
function clipName(name, maxChars) {
|
|
36
|
+
const collapsed = name.replace(/\s+/g, ' ').trim();
|
|
37
|
+
return collapsed.length <= maxChars ? collapsed : `${collapsed.slice(0, maxChars - 1)}…`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a `{count, names}` view. `count` is always the full length; `names`
|
|
41
|
+
* carries the first `SUMMARY_INITIAL_NAMES` (clipped) entries — the byte
|
|
42
|
+
* fitter may trim further.
|
|
43
|
+
*/
|
|
44
|
+
export function boundedNames(names) {
|
|
45
|
+
return {
|
|
46
|
+
count: names.length,
|
|
47
|
+
names: names.slice(0, SUMMARY_INITIAL_NAMES).map((n) => clipName(n, SUMMARY_NAME_MAX_CHARS)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Size of the value AS PRINTED — `printResult` serializes with `null, 2`, so
|
|
52
|
+
* the bound must be measured on the pretty form, not the compact one.
|
|
53
|
+
*/
|
|
54
|
+
function byteLength(value) {
|
|
55
|
+
try {
|
|
56
|
+
return Buffer.byteLength(JSON.stringify(value, null, 2) ?? '', 'utf8');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return Number.POSITIVE_INFINITY;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Collect every array (with its key path) nested in `node`. */
|
|
63
|
+
function collectArrays(node, path, out) {
|
|
64
|
+
if (Array.isArray(node)) {
|
|
65
|
+
out.push({ path, array: node });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (typeof node !== 'object' || node === null)
|
|
69
|
+
return;
|
|
70
|
+
for (const [key, value] of Object.entries(node)) {
|
|
71
|
+
collectArrays(value, path === '' ? key : `${path}.${key}`, out);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Shrink `data` (in place on a clone) until its JSON form fits `maxBytes`.
|
|
76
|
+
*
|
|
77
|
+
* Algorithm: repeatedly find the LONGEST nested array and drop its last
|
|
78
|
+
* element. Scalars are never modified, so `count` fields stay truthful; the
|
|
79
|
+
* `names` arrays simply show fewer names. Returns the input unchanged when it
|
|
80
|
+
* already fits or when there is no array left to trim.
|
|
81
|
+
*/
|
|
82
|
+
export function fitSummaryToBytes(data, maxBytes = SUMMARY_DATA_MAX_BYTES) {
|
|
83
|
+
let out;
|
|
84
|
+
try {
|
|
85
|
+
out = structuredClone(data);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
try {
|
|
89
|
+
out = JSON.parse(JSON.stringify(data));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return data; // not serializable — leave the caller's object alone
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Each iteration removes one element, so the bound is the total element
|
|
96
|
+
// count — a cheap upper limit that can never spin forever.
|
|
97
|
+
for (let guard = 0; guard < 100_000; guard++) {
|
|
98
|
+
if (byteLength(out) <= maxBytes)
|
|
99
|
+
return out;
|
|
100
|
+
const arrays = [];
|
|
101
|
+
collectArrays(out, '', arrays);
|
|
102
|
+
let longest = null;
|
|
103
|
+
for (const candidate of arrays) {
|
|
104
|
+
if (candidate.array.length === 0)
|
|
105
|
+
continue;
|
|
106
|
+
if (longest === null || candidate.array.length > longest.array.length)
|
|
107
|
+
longest = candidate;
|
|
108
|
+
}
|
|
109
|
+
if (longest === null)
|
|
110
|
+
return out; // nothing left to shrink
|
|
111
|
+
longest.array.pop();
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §3 — file-overlap-aware
|
|
3
|
+
* parallel scheduling.
|
|
4
|
+
*
|
|
5
|
+
* Problem: fan-out is mandatory, but the orchestrator serializes whenever
|
|
6
|
+
* two slices touch the same file (observed 2026-09-07: two slices both
|
|
7
|
+
* edited `src/cli/commands/code-runtime-commands.ts`, forcing a wait).
|
|
8
|
+
* Topological DAG levels do not know about files, so a "parallel" level can
|
|
9
|
+
* still contain a write/write conflict.
|
|
10
|
+
*
|
|
11
|
+
* Solution: a pure planner over slice descriptors `{ id, files[] }` that
|
|
12
|
+
* returns WAVES. Every slice in a wave has a file set pairwise disjoint
|
|
13
|
+
* from every other slice in that wave. A slice whose files collide with an
|
|
14
|
+
* earlier wave is deferred to a later wave, and the plan records WHICH file
|
|
15
|
+
* collided with WHICH already-scheduled slice — so the caller can explain
|
|
16
|
+
* the serialization instead of silently waiting.
|
|
17
|
+
*
|
|
18
|
+
* Pure: no I/O, no clock, deterministic for a given input.
|
|
19
|
+
*
|
|
20
|
+
* Relationship to `planDispatchWaves` (dag-orchestrator.ts): that planner
|
|
21
|
+
* chunks a topological level by `maxConcurrency` only. This planner is
|
|
22
|
+
* orthogonal — it refines a level (or any slice set) by file overlap. The
|
|
23
|
+
* `--from-dag` path uses this one additively (see `firstLevelWaves` in the
|
|
24
|
+
* dispatch envelope); the topological scheduler itself is unchanged.
|
|
25
|
+
*/
|
|
26
|
+
/** One unit of work and the files it is expected to touch. */
|
|
27
|
+
export interface SliceFileDescriptor {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly files: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
/** Why a slice did not land in the first wave it was considered for. */
|
|
32
|
+
export interface WaveDeferral {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
/** Wave the slice was placed in. */
|
|
35
|
+
readonly waveIndex: number;
|
|
36
|
+
/** The file that collided. */
|
|
37
|
+
readonly collidingFile: string;
|
|
38
|
+
/** The slice already scheduled in an earlier wave that holds that file. */
|
|
39
|
+
readonly collidedWith: string;
|
|
40
|
+
/** Human-readable one-liner (stable wording). */
|
|
41
|
+
readonly reason: string;
|
|
42
|
+
}
|
|
43
|
+
export interface FileOverlapWave {
|
|
44
|
+
readonly waveIndex: number;
|
|
45
|
+
/** Slice ids in input order. */
|
|
46
|
+
readonly slices: readonly string[];
|
|
47
|
+
/** Union of the wave's files, sorted. */
|
|
48
|
+
readonly files: readonly string[];
|
|
49
|
+
/** Deferrals resolved INTO this wave. */
|
|
50
|
+
readonly deferred: readonly WaveDeferral[];
|
|
51
|
+
}
|
|
52
|
+
export interface FileOverlapWavePlan {
|
|
53
|
+
readonly waves: readonly FileOverlapWave[];
|
|
54
|
+
/** Slice ids that appeared more than once; only the first is scheduled. */
|
|
55
|
+
readonly duplicateIds: readonly string[];
|
|
56
|
+
/** Number of distinct slices scheduled. */
|
|
57
|
+
readonly sliceCount: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Plan waves such that no two slices in the same wave share a file.
|
|
61
|
+
*
|
|
62
|
+
* Greedy, input-order stable: each slice goes into the earliest existing
|
|
63
|
+
* wave whose files are disjoint from its own; otherwise a new wave is
|
|
64
|
+
* opened. A slice with no files never collides.
|
|
65
|
+
*
|
|
66
|
+
* Duplicate ids: the FIRST descriptor wins; later ones are reported in
|
|
67
|
+
* `duplicateIds` and not scheduled (scheduling the same slice twice would
|
|
68
|
+
* double-dispatch it).
|
|
69
|
+
*/
|
|
70
|
+
export declare function planFileOverlapWaves(slices: readonly SliceFileDescriptor[]): FileOverlapWavePlan;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §3 — file-overlap-aware
|
|
3
|
+
* parallel scheduling.
|
|
4
|
+
*
|
|
5
|
+
* Problem: fan-out is mandatory, but the orchestrator serializes whenever
|
|
6
|
+
* two slices touch the same file (observed 2026-09-07: two slices both
|
|
7
|
+
* edited `src/cli/commands/code-runtime-commands.ts`, forcing a wait).
|
|
8
|
+
* Topological DAG levels do not know about files, so a "parallel" level can
|
|
9
|
+
* still contain a write/write conflict.
|
|
10
|
+
*
|
|
11
|
+
* Solution: a pure planner over slice descriptors `{ id, files[] }` that
|
|
12
|
+
* returns WAVES. Every slice in a wave has a file set pairwise disjoint
|
|
13
|
+
* from every other slice in that wave. A slice whose files collide with an
|
|
14
|
+
* earlier wave is deferred to a later wave, and the plan records WHICH file
|
|
15
|
+
* collided with WHICH already-scheduled slice — so the caller can explain
|
|
16
|
+
* the serialization instead of silently waiting.
|
|
17
|
+
*
|
|
18
|
+
* Pure: no I/O, no clock, deterministic for a given input.
|
|
19
|
+
*
|
|
20
|
+
* Relationship to `planDispatchWaves` (dag-orchestrator.ts): that planner
|
|
21
|
+
* chunks a topological level by `maxConcurrency` only. This planner is
|
|
22
|
+
* orthogonal — it refines a level (or any slice set) by file overlap. The
|
|
23
|
+
* `--from-dag` path uses this one additively (see `firstLevelWaves` in the
|
|
24
|
+
* dispatch envelope); the topological scheduler itself is unchanged.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Plan waves such that no two slices in the same wave share a file.
|
|
28
|
+
*
|
|
29
|
+
* Greedy, input-order stable: each slice goes into the earliest existing
|
|
30
|
+
* wave whose files are disjoint from its own; otherwise a new wave is
|
|
31
|
+
* opened. A slice with no files never collides.
|
|
32
|
+
*
|
|
33
|
+
* Duplicate ids: the FIRST descriptor wins; later ones are reported in
|
|
34
|
+
* `duplicateIds` and not scheduled (scheduling the same slice twice would
|
|
35
|
+
* double-dispatch it).
|
|
36
|
+
*/
|
|
37
|
+
export function planFileOverlapWaves(slices) {
|
|
38
|
+
const duplicateIds = [];
|
|
39
|
+
const seenIds = new Set();
|
|
40
|
+
const ordered = [];
|
|
41
|
+
for (const slice of slices) {
|
|
42
|
+
if (typeof slice?.id !== 'string' || slice.id.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
if (seenIds.has(slice.id)) {
|
|
45
|
+
duplicateIds.push(slice.id);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
seenIds.add(slice.id);
|
|
49
|
+
ordered.push({ id: slice.id, files: normalizeFiles(slice.files) });
|
|
50
|
+
}
|
|
51
|
+
const waves = [];
|
|
52
|
+
for (const slice of ordered) {
|
|
53
|
+
// First wave whose file set is disjoint from this slice's.
|
|
54
|
+
let placedAt = -1;
|
|
55
|
+
let collision = null;
|
|
56
|
+
for (let w = 0; w < waves.length; w += 1) {
|
|
57
|
+
const wave = waves[w];
|
|
58
|
+
if (wave === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
let hit = null;
|
|
61
|
+
for (const file of slice.files) {
|
|
62
|
+
const owner = wave.owner.get(file);
|
|
63
|
+
if (owner !== undefined) {
|
|
64
|
+
hit = { file, withSlice: owner };
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (hit === null) {
|
|
69
|
+
placedAt = w;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
// Remember the FIRST collision (earliest wave) for the reason string.
|
|
73
|
+
if (collision === null) {
|
|
74
|
+
collision = { file: hit.file, withSlice: hit.withSlice, waveIndex: w };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (placedAt === -1) {
|
|
78
|
+
placedAt = waves.length;
|
|
79
|
+
waves.push({ slices: [], files: new Set(), owner: new Map(), deferred: [] });
|
|
80
|
+
}
|
|
81
|
+
const target = waves[placedAt];
|
|
82
|
+
if (target === undefined)
|
|
83
|
+
continue; // unreachable; satisfies strict TS
|
|
84
|
+
target.slices.push(slice.id);
|
|
85
|
+
for (const file of slice.files) {
|
|
86
|
+
target.files.add(file);
|
|
87
|
+
target.owner.set(file, slice.id);
|
|
88
|
+
}
|
|
89
|
+
if (collision !== null && placedAt > 0) {
|
|
90
|
+
target.deferred.push({
|
|
91
|
+
id: slice.id,
|
|
92
|
+
waveIndex: placedAt,
|
|
93
|
+
collidingFile: collision.file,
|
|
94
|
+
collidedWith: collision.withSlice,
|
|
95
|
+
reason: `deferred to wave ${placedAt}: file "${collision.file}" already scheduled in wave ${collision.waveIndex} by slice "${collision.withSlice}"`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
waves: waves.map((w, index) => ({
|
|
101
|
+
waveIndex: index,
|
|
102
|
+
slices: w.slices,
|
|
103
|
+
files: [...w.files].sort(),
|
|
104
|
+
deferred: w.deferred
|
|
105
|
+
})),
|
|
106
|
+
duplicateIds,
|
|
107
|
+
sliceCount: ordered.length
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function normalizeFiles(files) {
|
|
111
|
+
if (!Array.isArray(files))
|
|
112
|
+
return [];
|
|
113
|
+
const set = new Set();
|
|
114
|
+
for (const f of files) {
|
|
115
|
+
if (typeof f === 'string' && f.length > 0)
|
|
116
|
+
set.add(f);
|
|
117
|
+
}
|
|
118
|
+
return [...set].sort();
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Reserved batch id for the session capsule (matches the channel path pattern). */
|
|
2
|
+
export declare const SESSION_CAPSULE_BATCH_ID = "session-capsule";
|
|
3
|
+
/** Reserved key inside that channel. */
|
|
4
|
+
export declare const SESSION_CAPSULE_KEY = "orchestrator.capsule";
|
|
5
|
+
export interface SessionCapsuleRef {
|
|
6
|
+
readonly batchId: string;
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** Byte size of the published value (for the pointer line). */
|
|
9
|
+
readonly bytes: number;
|
|
10
|
+
/** ISO8601 timestamp of the last write. */
|
|
11
|
+
readonly updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read the session capsule, if one was published. Returns `null` when the
|
|
15
|
+
* channel is absent, empty, or unreadable — the dispatch prompt then
|
|
16
|
+
* renders neither the pointer nor the precedence line (byte-identical to
|
|
17
|
+
* the pre-slice shape).
|
|
18
|
+
*/
|
|
19
|
+
export declare function readSessionCapsule(opts: {
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
sid: string;
|
|
22
|
+
rid: string;
|
|
23
|
+
}): SessionCapsuleRef | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §4 — session capsule reader.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator publishes already-known background ONCE per session
|
|
5
|
+
* through the existing G8.4 channel:
|
|
6
|
+
*
|
|
7
|
+
* peaks sub-agent share \
|
|
8
|
+
* --batch session-capsule \
|
|
9
|
+
* --key orchestrator.capsule \
|
|
10
|
+
* --value '{"rootCauses":[...],"decisions":[...],"fileMap":{...}}'
|
|
11
|
+
*
|
|
12
|
+
* Dispatch prompts then carry a one-line `shared-read` pointer instead of
|
|
13
|
+
* re-explaining that background in every task spec.
|
|
14
|
+
*
|
|
15
|
+
* QUALITY GUARD: the capsule is ADVISORY BACKGROUND ONLY. Nothing a
|
|
16
|
+
* sub-agent must ACT on may live only in the capsule — the task spec is
|
|
17
|
+
* authoritative and wins on conflict. The precedence sentence is rendered
|
|
18
|
+
* by `renderCapsulePointer` in build-dispatch-system-prompt.ts and is not
|
|
19
|
+
* optional when the pointer is emitted.
|
|
20
|
+
*
|
|
21
|
+
* This module only READS. Publishing is the orchestrator's job through the
|
|
22
|
+
* already-shipped `peaks sub-agent share` primitive — no new write path.
|
|
23
|
+
*/
|
|
24
|
+
import { readSharedChannel } from 'peaks-loop-shared-channel';
|
|
25
|
+
/** Reserved batch id for the session capsule (matches the channel path pattern). */
|
|
26
|
+
export const SESSION_CAPSULE_BATCH_ID = 'session-capsule';
|
|
27
|
+
/** Reserved key inside that channel. */
|
|
28
|
+
export const SESSION_CAPSULE_KEY = 'orchestrator.capsule';
|
|
29
|
+
/**
|
|
30
|
+
* Read the session capsule, if one was published. Returns `null` when the
|
|
31
|
+
* channel is absent, empty, or unreadable — the dispatch prompt then
|
|
32
|
+
* renders neither the pointer nor the precedence line (byte-identical to
|
|
33
|
+
* the pre-slice shape).
|
|
34
|
+
*/
|
|
35
|
+
export function readSessionCapsule(opts) {
|
|
36
|
+
try {
|
|
37
|
+
const channel = readSharedChannel({
|
|
38
|
+
projectRoot: opts.projectRoot,
|
|
39
|
+
sid: opts.sid,
|
|
40
|
+
rid: opts.rid,
|
|
41
|
+
batchId: SESSION_CAPSULE_BATCH_ID
|
|
42
|
+
});
|
|
43
|
+
const entry = channel.entries[SESSION_CAPSULE_KEY];
|
|
44
|
+
if (entry === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
batchId: SESSION_CAPSULE_BATCH_ID,
|
|
48
|
+
key: SESSION_CAPSULE_KEY,
|
|
49
|
+
bytes: entry.valueSize,
|
|
50
|
+
updatedAt: entry.at
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null; // fail-soft: a missing capsule never blocks a dispatch
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -31,6 +31,15 @@ export interface SliceNode {
|
|
|
31
31
|
* (complex = user-attended, simple/trivial = overnight). Optional.
|
|
32
32
|
*/
|
|
33
33
|
readonly complexity?: SliceComplexity;
|
|
34
|
+
/**
|
|
35
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §3: files this slice is
|
|
36
|
+
* expected to touch. When EVERY node of a topological level declares
|
|
37
|
+
* `files`, `--from-dag` emits a file-overlap wave plan (`firstLevelWaves`)
|
|
38
|
+
* so the LLM can fan the level out without serializing on a shared file.
|
|
39
|
+
* Optional and additive: absent → DAG hash and dispatch behavior are
|
|
40
|
+
* byte-identical to before this slice.
|
|
41
|
+
*/
|
|
42
|
+
readonly files?: readonly string[];
|
|
34
43
|
}
|
|
35
44
|
export interface DependsOn {
|
|
36
45
|
readonly from: string;
|
|
@@ -103,6 +103,11 @@ export function validateDag(dag) {
|
|
|
103
103
|
if (n.complexity !== undefined && !isSliceComplexity(n.complexity)) {
|
|
104
104
|
throw new InvalidSliceDagError(`node ${n.id} complexity must be one of ${SLICE_COMPLEXITIES.join('|')} when present`);
|
|
105
105
|
}
|
|
106
|
+
// Slice 2026-09-10 §3: optional file list. Only shape-checked when
|
|
107
|
+
// present so pre-existing DAGs stay valid.
|
|
108
|
+
if (n.files !== undefined && (!Array.isArray(n.files) || n.files.some((f) => typeof f !== 'string' || f.length === 0))) {
|
|
109
|
+
throw new InvalidSliceDagError(`node ${n.id} files must be an array of non-empty strings when present`);
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
// v2.15.0 follow-up — G12 defensive rule: foundation slice can only
|
|
108
113
|
// depend on another foundation slice. Business depending on foundation
|
|
@@ -219,7 +224,10 @@ export function serializeDag(dag) {
|
|
|
219
224
|
// (only when present, preserving hash stability for old DAGs).
|
|
220
225
|
...(n.foundation !== undefined ? { foundation: n.foundation } : {}),
|
|
221
226
|
...(n.upstreamSync !== undefined ? { upstreamSync: n.upstreamSync } : {}),
|
|
222
|
-
...(n.complexity !== undefined ? { complexity: n.complexity } : {})
|
|
227
|
+
...(n.complexity !== undefined ? { complexity: n.complexity } : {}),
|
|
228
|
+
// Slice 2026-09-10 §3: only present when declared, so the hash of a
|
|
229
|
+
// file-less DAG is unchanged.
|
|
230
|
+
...(n.files !== undefined ? { files: [...n.files] } : {})
|
|
223
231
|
}));
|
|
224
232
|
const edges = [...dag.edges]
|
|
225
233
|
.sort((a, b) => {
|
|
@@ -26,8 +26,19 @@
|
|
|
26
26
|
* "remove the redundancy" of a test-tool-detection instruction because
|
|
27
27
|
* the dispatch CLI always prepends it. This is a guarantee, not a
|
|
28
28
|
* suggestion.
|
|
29
|
+
*
|
|
30
|
+
* 2026-09-10-dispatch-block-d (Option D): ONE block for EVERY role. The
|
|
31
|
+
* runner-table EXAMPLES were dropped — they were never rules, and
|
|
32
|
+
* `package.json#scripts.test` is the source of truth at run time — while
|
|
33
|
+
* the one missing real rule is stated inline: PB-5, i.e. repo-defined
|
|
34
|
+
* `test` / `test:*` scripts are the human/LLM direct path and are NOT
|
|
35
|
+
* gated by the scope rule. The soft fallback ("only as a last resort, ask
|
|
36
|
+
* the user before assuming a runner") and the Windows-aware note on
|
|
37
|
+
* `peaks test <file>` are RETAINED — they are quality guidance, not
|
|
38
|
+
* examples. No role split remains, so every role receives a byte-identical
|
|
39
|
+
* block.
|
|
29
40
|
*/
|
|
30
|
-
export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandatory)\n\
|
|
41
|
+
export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandatory)\n\nRead `package.json#scripts.test` for the project's framework and use the project-local runner \u2014 do NOT invoke `npx <runner>`. If unsure, run `peaks test --json` first. Only as a last resort, ask the user before assuming a runner. `peaks test <file>` already resolves the local binary for you (Windows-aware).\n\n## Test Scope (mandatory)\n\nAny test command MUST be **scoped** to a single file or pattern; a bare `./node_modules/.bin/vitest run` is **refused** unless you prefix the explicit opt-in token `PEAKS_FULL_TEST=1` (CI / release verification only, never routine slice verification).\n\nRepo-defined `test` / `test:*` scripts are the human/LLM direct path and are NOT gated by this scope rule (PB-5).";
|
|
31
42
|
/**
|
|
32
43
|
* Pure helper that returns the block. Exists as a function (not just an
|
|
33
44
|
* exported constant) so future variants can take a runtime parameter
|
|
@@ -26,26 +26,27 @@
|
|
|
26
26
|
* "remove the redundancy" of a test-tool-detection instruction because
|
|
27
27
|
* the dispatch CLI always prepends it. This is a guarantee, not a
|
|
28
28
|
* suggestion.
|
|
29
|
+
*
|
|
30
|
+
* 2026-09-10-dispatch-block-d (Option D): ONE block for EVERY role. The
|
|
31
|
+
* runner-table EXAMPLES were dropped — they were never rules, and
|
|
32
|
+
* `package.json#scripts.test` is the source of truth at run time — while
|
|
33
|
+
* the one missing real rule is stated inline: PB-5, i.e. repo-defined
|
|
34
|
+
* `test` / `test:*` scripts are the human/LLM direct path and are NOT
|
|
35
|
+
* gated by the scope rule. The soft fallback ("only as a last resort, ask
|
|
36
|
+
* the user before assuming a runner") and the Windows-aware note on
|
|
37
|
+
* `peaks test <file>` are RETAINED — they are quality guidance, not
|
|
38
|
+
* examples. No role split remains, so every role receives a byte-identical
|
|
39
|
+
* block.
|
|
29
40
|
*/
|
|
30
41
|
export const TEST_TOOL_DETECTION_BLOCK = `## Test Tool Detection (mandatory)
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **vitest** → \`./node_modules/.bin/vitest run <file>\` (or \`pnpm test -- <file>\`)
|
|
35
|
-
- **jest** → \`./node_modules/.bin/jest <file>\` (or \`pnpm test -- <file>\`)
|
|
36
|
-
- **mocha** → \`./node_modules/.bin/mocha <file>\` (or \`pnpm test -- <file>\`)
|
|
43
|
+
Read \`package.json#scripts.test\` for the project's framework and use the project-local runner — do NOT invoke \`npx <runner>\`. If unsure, run \`peaks test --json\` first. Only as a last resort, ask the user before assuming a runner. \`peaks test <file>\` already resolves the local binary for you (Windows-aware).
|
|
37
44
|
|
|
38
45
|
## Test Scope (mandatory)
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **scoped** → \`./node_modules/.bin/vitest run tests/unit/foo.test.ts\` (or any explicit file/pattern)
|
|
43
|
-
- **intentional full run** → prefix with the explicit opt-in token \`PEAKS_FULL_TEST=1\` to override the scope gate. Use only for CI / release verification, never for routine verification during a slice.
|
|
44
|
-
- **refused** → bare \`./node_modules/.bin/vitest run\` (no argument after \`run\`) without the opt-in token.
|
|
45
|
-
|
|
46
|
-
\`pnpm test\` / \`pnpm test:unit\` / \`pnpm test:cli\` / \`pnpm test:integration\` and any repo-defined \`test*\` script remain the **human / LLM direct path** and are not gated by this rule (PB-5).
|
|
47
|
+
Any test command MUST be **scoped** to a single file or pattern; a bare \`./node_modules/.bin/vitest run\` is **refused** unless you prefix the explicit opt-in token \`PEAKS_FULL_TEST=1\` (CI / release verification only, never routine slice verification).
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
Repo-defined \`test\` / \`test:*\` scripts are the human/LLM direct path and are NOT gated by this scope rule (PB-5).`;
|
|
49
50
|
/**
|
|
50
51
|
* Pure helper that returns the block. Exists as a function (not just an
|
|
51
52
|
* exported constant) so future variants can take a runtime parameter
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`L3:l3-memory-health`).
|
|
2
|
+
* Checks for `.peaks/memory/` health (`L3:l3-memory-health` and siblings).
|
|
4
3
|
*
|
|
5
4
|
* Slice 2026-06-13-repair-pre-existing-test-failures: the
|
|
6
5
|
* production MemoryIndex schema (see
|
|
@@ -11,6 +10,24 @@
|
|
|
11
10
|
*
|
|
12
11
|
* When no `.peaks/memory/index.json` exists yet, the check passes
|
|
13
12
|
* (fresh project — no memories have been extracted).
|
|
13
|
+
*
|
|
14
|
+
* Slice 2026-09-09-memory-system-overhaul (D) extends the check with the
|
|
15
|
+
* drift findings the original version could not see. It used to report
|
|
16
|
+
* `ok: true` for "index.json is well-formed JSON; 100 hot + 131 warm" and
|
|
17
|
+
* never looked at coverage, orphans, or unclassified files. It now emits,
|
|
18
|
+
* in addition to the unchanged well-formed-JSON assertion:
|
|
19
|
+
*
|
|
20
|
+
* - `L3:l3-memory-coverage` — disk files vs indexed entries (warning
|
|
21
|
+
* when the gap exceeds a small threshold)
|
|
22
|
+
* - `L3:l3-memory-orphans` — index entries whose `sourcePath` is gone
|
|
23
|
+
* (error) + disk files absent from the
|
|
24
|
+
* index (warning)
|
|
25
|
+
* - `L3:l3-memory-unclassified` — files with no resolvable kind (warning,
|
|
26
|
+
* count + first N names)
|
|
27
|
+
*
|
|
28
|
+
* All three are read-only and fail-soft: an inspection error degrades to a
|
|
29
|
+
* single warning instead of throwing, and none of them change the id or the
|
|
30
|
+
* `ok` semantics of the original `L3:l3-memory-health` assertion.
|
|
14
31
|
*/
|
|
15
32
|
import type { DoctorCheckPlugin } from '../types.js';
|
|
16
33
|
export declare const check: DoctorCheckPlugin;
|