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.
Files changed (72) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
  3. package/dist/cli/commands/code-runtime-commands.js +57 -2
  4. package/dist/cli/commands/core/doctor-command.d.ts +8 -0
  5. package/dist/cli/commands/core/doctor-command.js +44 -2
  6. package/dist/cli/commands/core/memory-command.js +65 -3
  7. package/dist/cli/commands/dispatch-commands.js +19 -5
  8. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  9. package/dist/cli/commands/memory-commands.d.ts +59 -0
  10. package/dist/cli/commands/memory-commands.js +195 -19
  11. package/dist/cli/commands/request-commands.d.ts +8 -0
  12. package/dist/cli/commands/request-commands.js +23 -2
  13. package/dist/cli/commands/sub-agent-commands.js +2 -0
  14. package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
  15. package/dist/cli/commands/wave-plan-commands.js +93 -0
  16. package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
  17. package/dist/services/context/build-dispatch-system-prompt.js +132 -17
  18. package/dist/services/context/context-audit.d.ts +100 -0
  19. package/dist/services/context/context-audit.js +322 -0
  20. package/dist/services/context/context-schema.d.ts +1 -1
  21. package/dist/services/context/memory-index-reader.d.ts +26 -0
  22. package/dist/services/context/memory-index-reader.js +62 -30
  23. package/dist/services/context/memory-preflight-config.d.ts +33 -0
  24. package/dist/services/context/memory-preflight-config.js +32 -2
  25. package/dist/services/context/memory-preflight-service.d.ts +20 -1
  26. package/dist/services/context/memory-preflight-service.js +198 -31
  27. package/dist/services/context/summary-view.d.ts +54 -0
  28. package/dist/services/context/summary-view.js +114 -0
  29. package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
  30. package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
  31. package/dist/services/dispatch/session-capsule.d.ts +23 -0
  32. package/dist/services/dispatch/session-capsule.js +56 -0
  33. package/dist/services/dispatch/slice-dag.d.ts +9 -0
  34. package/dist/services/dispatch/slice-dag.js +9 -1
  35. package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
  36. package/dist/services/dispatch/test-tool-detection.js +14 -13
  37. package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
  38. package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
  39. package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
  40. package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
  41. package/dist/services/ide/ide-types.d.ts +15 -0
  42. package/dist/services/job/job-types.d.ts +3 -3
  43. package/dist/services/memory/memory-ingest-service.d.ts +79 -0
  44. package/dist/services/memory/memory-ingest-service.js +225 -0
  45. package/dist/services/memory/memory-rotate-service.d.ts +88 -0
  46. package/dist/services/memory/memory-rotate-service.js +373 -0
  47. package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
  48. package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
  49. package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
  50. package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
  51. package/dist/services/memory/project-memory-service/index/search.js +14 -24
  52. package/dist/services/memory/project-memory-service/index.d.ts +7 -3
  53. package/dist/services/memory/project-memory-service/index.js +6 -2
  54. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +80 -3
  55. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +167 -28
  56. package/dist/services/memory/project-memory-service/types.d.ts +31 -1
  57. package/dist/services/memory/project-memory-service/types.js +76 -1
  58. package/dist/services/preferences/preferences-types.d.ts +14 -0
  59. package/dist/services/preferences/preferences-types.js +8 -0
  60. package/dist/services/share/run-state-contract.d.ts +1 -1
  61. package/package.json +5 -5
  62. package/skills/bee/peaks-qa/SKILL.md +2 -0
  63. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
  64. package/skills/bee/peaks-rd/SKILL.md +2 -0
  65. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
  66. package/skills/bee/peaks-txt/SKILL.md +2 -0
  67. package/skills/bee/peaks-ui/SKILL.md +2 -0
  68. package/skills/peaks-code/SKILL.md +9 -1
  69. package/skills/peaks-code/references/context-governance.md +29 -0
  70. package/skills/peaks-code/references/runbook.md +6 -0
  71. package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
  72. 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\nBefore running any test, read `package.json#scripts.test` to identify the project's test framework. Use the project-local runner \u2014 do NOT invoke `npx <runner>`:\n\n- **vitest** \u2192 `./node_modules/.bin/vitest run <file>` (or `pnpm test -- <file>`)\n- **jest** \u2192 `./node_modules/.bin/jest <file>` (or `pnpm test -- <file>`)\n- **mocha** \u2192 `./node_modules/.bin/mocha <file>` (or `pnpm test -- <file>`)\n\n## Test Scope (mandatory)\n\nThe dispatched test command MUST be **scoped** to a single file or pattern. An unscoped `./node_modules/.bin/vitest run` (no path filter) is **refused** because the 483-file suite is one keystroke from a 36-minute wall clock:\n\n- **scoped** \u2192 `./node_modules/.bin/vitest run tests/unit/foo.test.ts` (or any explicit file/pattern)\n- **intentional full run** \u2192 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.\n- **refused** \u2192 bare `./node_modules/.bin/vitest run` (no argument after `run`) without the opt-in token.\n\n`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).\n\nIf unsure which framework the consumer project uses, run `peaks test --json` first to introspect the resolved framework + argv. Only as a last resort, ask the user before assuming a runner. The CLI command `peaks test <file>` already resolves the local binary for you (Windows-aware).";
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
- Before running any test, read \`package.json#scripts.test\` to identify the project's test framework. Use the project-local runner — do NOT invoke \`npx <runner>\`:
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
- The dispatched test command MUST be **scoped** to a single file or pattern. An unscoped \`./node_modules/.bin/vitest run\` (no path filter) is **refused** because the 483-file suite is one keystroke from a 36-minute wall clock:
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
- If unsure which framework the consumer project uses, run \`peaks test --json\` first to introspect the resolved framework + argv. Only as a last resort, ask the user before assuming a runner. The CLI command \`peaks test <file>\` already resolves the local binary for you (Windows-aware).`;
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
- * Check: `.peaks/memory/index.json` is well-formed JSON
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;