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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.36 — 2026-09-10 (派发提示词瘦身 + 波次调度 + 编排器上下文审计)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **派发提示词瘦身(不牺牲规则)** — 每次派发的提示词里约 52% 是 CLI 注入的重复 boilerplate。本轮压缩 L1/生命周期/上下文探测段,并把 test-tool 块统一为一份(去掉 vitest/jest/mocha 举例,保留全部操作性规则,补上只存在于完整块里的 PB-5 例外规则)。7 个角色现在**字节完全一致**:rd/qa/qa-business/sc/prd **3897 → 3191 B(−18.1%)**。绑定规则 **21/21**、runner 规则 **5/5** 全角色断言。
8
+ - 契约指针实验**已回退**——它每次多 165 B 却不减任何内容,净亏。
9
+
10
+ 2. **文件重叠感知的波次调度** — 新 planner 按切面文件集计算波次:同一波内文件两两不重叠,重叠的推迟到后续波并**注明被哪个文件挡住**;附加式接入 `--from-dag`(`firstLevelWaves`),不改原调度语义。
11
+
12
+ 3. **编排器上下文审计 + 纪律** — 新增 `peaks code context-audit`:读会话 transcript 按 (工具, 命令) 汇总上下文消耗 Top-N。本仓库实测 **779 KB / 586 组**,且 Top-5 仅占 ~15%——**是长尾,不是单一元凶**。
13
+ - `--summary` 用于 memory reindex/list、doctor、request list:envelope **9.4–59 KB → 1.7–1.9 KB**(−79% ~ −97%),默认 `--json` 形状字节不变。
14
+ - SKILL.md 绑定规则:单次工具输出 >2 KB 禁止直接灌入编排器上下文。
15
+ - 子代理最终报告封顶 **≤40 行 / 2 KB**(5 个必填字段,细节写 artifact),已纳入 `BINDING_RULE_TOKENS`。
16
+
17
+ **验证**:build clean、tsc clean、全量 141 files / 1270 passed(1 skipped)。
18
+
19
+ ## 4.0.35 — 2026-09-10 (记忆系统 overhaul — 写入合一 / 索引重建 / 按任务调取 / 漂移健康检查 / rotate)
20
+
21
+ **Highlights**:
22
+
23
+ 1. **记忆写入单一权威** — `peaks memory ingest` 把 IDE 侧(Claude Code)记忆导入 `.peaks/memory/`,源目录只读、**绝不写 `~/.claude/`**;SKILL.md 明确:工作流内"沉淀记忆"落 `.peaks/memory/`,IDE 侧仅作会话笔记。
24
+
25
+ 2. **索引重建 + 根因修复** — `peaks memory reindex` 全量重扫并重建 `index.json`。**根因**:共享 frontmatter 解析器只认顶层 `type:`,按契约写 `metadata.type` 的文件被**静默丢弃**。现按 `metadata.type → kind → type` 解析,未识别值**输出清单**而非静默跳过。name 解析新增 `name → title → 文件名` 回退;重名冲突上报不覆盖。本仓库实测:索引 **231 → 276**。
26
+
27
+ 3. **调取按任务 + 分层预算** — preflight 此前 `fetchBlock(_taskTitle)` 的 `taskTitle` **完全未使用**,永远注入同一批 feedback+layerA。现按任务排序(per-token fuzzy,无网络/无 embedding),hot 必进、warm 按相关度入选,三重预算(条数/字节/时间)+ 可观测字段。warm 层首次可被注入。顺带修:读取器原来只扁平化 4 种 kind,丢了 decision/rule/convention/module/lesson。
28
+
29
+ 4. **kind 词表 8 → 21** — 纳入语料中实际使用的 13 个 kind(`bug`/`investigation`/`technical-pattern`/`project-rule`/`design`/`handoff`/`session-handoff`/`project-todo`/`publish-closure`/`project-closure`/`slice-closure`/`slice-pilot-findings`/`sediment`),单一常量驱动解析/索引/CLI/doctor。
30
+
31
+ 5. **`peaks memory rotate`(清除机制)** — 实现 2026-07-24 pruning policy 早已规定但从未实现的轮转:A/B 层永不入选(测试断言)、C 层满 **6 个月**且未被钉住 → 归档、D 层 → 报告待删(不删)、每个候选先过 `src/`+`skills/` 引用 grep。默认 dry-run。
32
+
33
+ 6. **doctor 漂移检查** — 新增 `l3-memory-coverage` / `l3-memory-orphans` / `l3-memory-unclassified`;此前只校验"index.json 是合法 JSON",46% 不可见也报 ok。
34
+
35
+ **验证**:build clean、tsc clean、全量 133 files / 1180 passed(1 skipped);本仓库 rotate dry-run `A:22 B:146 C:138 D:12`。
36
+
3
37
  ## 4.0.34 — 2026-09-09 (mode 模型收敛 + auto-compact 死命令 + 上下文窗口覆盖 + 确认门去 TTY)
4
38
 
5
39
  **Highlights**:
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Slice rid-024 — runtime probes: post-compact-detect / auto-compact /
3
- * context-now / gate-step-08 / emit-handoff.
3
+ * context-now / context-audit / gate-step-08 / emit-handoff.
4
4
  *
5
5
  * Extracted from code-commands.ts (rid-024 split).
6
- * Owns: 5 sub-commands that read or mutate runtime state.
6
+ * Owns: 6 sub-commands that read or mutate runtime state.
7
7
  * Owns the `readActiveSid` helper (only used by these runtime probes).
8
+ *
9
+ * Slice 2026-09-10-context-audit-and-discipline added `context-audit`
10
+ * (what fills the window, grouped by tool + short input key).
8
11
  */
9
12
  import type { Command } from 'commander';
10
13
  import { type ProgramIO } from '../cli-helpers.js';
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * Slice rid-024 — runtime probes: post-compact-detect / auto-compact /
3
- * context-now / gate-step-08 / emit-handoff.
3
+ * context-now / context-audit / gate-step-08 / emit-handoff.
4
4
  *
5
5
  * Extracted from code-commands.ts (rid-024 split).
6
- * Owns: 5 sub-commands that read or mutate runtime state.
6
+ * Owns: 6 sub-commands that read or mutate runtime state.
7
7
  * Owns the `readActiveSid` helper (only used by these runtime probes).
8
+ *
9
+ * Slice 2026-09-10-context-audit-and-discipline added `context-audit`
10
+ * (what fills the window, grouped by tool + short input key).
8
11
  */
9
12
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
10
13
  import { fail, ok } from 'peaks-loop-shared/result';
11
14
  import { detectPostCompactResume, formatPostCompactResumeLogLine } from '../../services/code/post-compact-detector.js';
12
15
  import { runAutoCompact } from '../../services/code/auto-compact-orchestrator.js';
16
+ import { auditContext } from '../../services/context/context-audit.js';
13
17
  import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
14
18
  import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from '../../services/code/emit-handoff.js';
15
19
  import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code/job-shape-decision.js';
@@ -254,6 +258,57 @@ export function registerCodeRuntimeCommands(code, io) {
254
258
  process.exitCode = 1;
255
259
  }
256
260
  });
261
+ // Slice 2026-09-10-context-audit-and-discipline (Slice A): visibility into
262
+ // WHAT fills the orchestrator window. `context-now` returns a ratio; this
263
+ // returns the grouped byte breakdown of tool results from the live
264
+ // transcript. Read-only + fail-soft: an unavailable transcript reports
265
+ // `available:false` with a reason and NEVER sets a non-zero exit code.
266
+ addJsonOption(code
267
+ .command('context-audit')
268
+ .description('Slice 2026-09-10 Slice A: report what fills the current session\'s ' +
269
+ 'context window, grouped by tool + short input key (command line / ' +
270
+ 'path tail / pattern), sorted by bytes. Locates the CURRENT session\'s ' +
271
+ 'IDE transcript through the active adapter\'s ' +
272
+ '`compact.resolveTranscriptPath` (vendor-neutral); emits total bytes, ' +
273
+ 'entry count, and the top-N groups ' +
274
+ '(`{tool, key, bytes, pctOfTotal, count}`). Read-only and fail-soft — ' +
275
+ 'a missing/oversized/corrupt transcript returns `available:false` ' +
276
+ 'with a reason and never blocks. Never dumps tool result content.')
277
+ .requiredOption('--project <path>', 'target project root')
278
+ .option('--session-id <sid>', 'override session id (default: read from active presence)')
279
+ .option('--top <n>', 'number of top entries to emit (default 15, max 100)', (value) => Number(value))
280
+ .option('--transcript <path>', 'override the transcript jsonl path (test seam)')).action((opts) => {
281
+ try {
282
+ // `resolveOuterSessionId` checks the env signal FIRST, so the peaks
283
+ // session id is only a fallback lookup key — mirror context-now's
284
+ // 'unknown' default so an unbound presence still resolves via env.
285
+ const sessionId = opts.sessionId ?? readActiveSid(opts.project) ?? 'unknown';
286
+ const outerSessionId = resolveOuterSessionId(opts.project, sessionId);
287
+ const result = auditContext({
288
+ outerSessionId: outerSessionId ?? null,
289
+ ...(opts.top !== undefined ? { topN: opts.top } : {}),
290
+ ...(opts.transcript !== undefined ? { transcriptPath: opts.transcript } : {}),
291
+ });
292
+ // Fail-soft contract: unavailability is DATA, not an error. The exit
293
+ // code stays 0 so a `context-audit` call can never block a workflow.
294
+ const nextActions = result.available
295
+ ? [`${result.entryCount} tool result(s) across ${result.groupCount} group(s); showing top ${result.entries.length}.`]
296
+ : [`context-audit unavailable: ${result.reason ?? 'unknown'} — continue without it (read-only probe).`];
297
+ printResult(io, ok('code.context-audit', { ...result }, [], nextActions), opts.json);
298
+ }
299
+ catch (err) {
300
+ printResult(io, ok('code.context-audit', {
301
+ available: false,
302
+ reason: `audit-failed: ${getErrorMessage(err)}`,
303
+ transcriptPath: null,
304
+ totalBytes: 0,
305
+ entryCount: 0,
306
+ groupCount: 0,
307
+ topN: 0,
308
+ entries: []
309
+ }, [], ['context-audit is a read-only probe; continue without it.']), opts.json);
310
+ }
311
+ });
257
312
  // v3.1.2 Step 0.8 — Mechanical PreToolUse gate.
258
313
  // Wire-installed by `peaks workspace init` (extends the existing hook
259
314
  // installer). Exit code is the load-bearing contract:
@@ -1,5 +1,13 @@
1
1
  import type { Command } from 'commander';
2
2
  import { type ProgramIO } from '../../cli-helpers.js';
3
+ /**
4
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
5
+ * the doctor envelope. The `checks` array (one object per check, each with a
6
+ * full message) and the stale-binding instance list collapse to counts +
7
+ * names-of-first-N; the summary counters and log section are scalars and are
8
+ * kept verbatim. ≤ 2 KB by construction.
9
+ */
10
+ export declare function buildDoctorSummary(data: Record<string, unknown>): Record<string, unknown>;
3
11
  export type BindingSource = 'canonical' | 'legacy';
4
12
  export type DoctorLogsSection = {
5
13
  logDir: string;
@@ -4,6 +4,7 @@ import { findProjectRoot } from '../../../services/config/config-safety.js';
4
4
  import { loadSkillRegistry } from '../../../services/skills/skill-registry.js';
5
5
  import { planStatusLineInstall } from '../../../services/skills/statusline-settings-service.js';
6
6
  import { addJsonOption, printResult } from '../../cli-helpers.js';
7
+ import { boundedNames, fitSummaryToBytes } from '../../../services/context/summary-view.js';
7
8
  import { fail, ok } from 'peaks-loop-shared/result';
8
9
  // slice-3b Option C: the doctor subpackage owns the check pipeline but
9
10
  // does NOT import cross-domain utils from the main package (avoids
@@ -38,6 +39,43 @@ function statusLineAlreadyInstalledForScope(scope, projectRoot) {
38
39
  return false;
39
40
  }
40
41
  }
42
+ /**
43
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
44
+ * the doctor envelope. The `checks` array (one object per check, each with a
45
+ * full message) and the stale-binding instance list collapse to counts +
46
+ * names-of-first-N; the summary counters and log section are scalars and are
47
+ * kept verbatim. ≤ 2 KB by construction.
48
+ */
49
+ export function buildDoctorSummary(data) {
50
+ const checks = Array.isArray(data.checks) ? data.checks : [];
51
+ const label = (c) => {
52
+ const id = typeof c.id === 'string' ? c.id : 'unknown';
53
+ if (c.ok === true)
54
+ return `ok ${id}`;
55
+ return `${c.severity === 'warning' ? 'warn' : 'FAIL'} ${id}`;
56
+ };
57
+ const failed = checks.filter((c) => c.ok === false);
58
+ const stale = typeof data.staleBinding === 'object' && data.staleBinding !== null
59
+ ? data.staleBinding
60
+ : null;
61
+ const view = {
62
+ view: 'summary',
63
+ summary: data.summary,
64
+ checks: boundedNames(checks.map(label)),
65
+ failed: boundedNames(failed.map((c) => `${typeof c.id === 'string' ? c.id : 'unknown'}: ${typeof c.message === 'string' ? c.message : ''}`)),
66
+ };
67
+ if (stale !== null) {
68
+ view.staleBinding = {
69
+ ttlMs: stale.ttlMs,
70
+ staleCount: stale.staleCount,
71
+ droppedCount: stale.droppedCount,
72
+ droppedSids: boundedNames(Array.isArray(stale.droppedSids) ? stale.droppedSids.map((s) => String(s)) : []),
73
+ };
74
+ }
75
+ if (data.logs !== undefined)
76
+ view.logs = data.logs;
77
+ return fitSummaryToBytes(view);
78
+ }
41
79
  function doctorStatusLineInstalledProbe() {
42
80
  const projectRoot = findProjectRoot(process.cwd());
43
81
  // Check both scopes: a user may have installed the statusLine globally, which
@@ -136,7 +174,8 @@ export function registerDoctorCommand(program, io) {
136
174
  // scan) addressable for non-current projects. The doctor was
137
175
  // hardcoded to findProjectRoot(process.cwd()) which is the
138
176
  // wrong default for users inspecting a sibling project.
139
- .option('--project <path>', 'target project root (defaults to git root or cwd)')).action(async (options) => {
177
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
178
+ .option('--summary', 'JSON envelope only: emit check counts + names-of-first-N (≤ 2 KB) instead of the full checks/stale-binding arrays; the default envelope is unchanged')).action(async (options) => {
140
179
  // v2.18.2 cycle 2 (Q2 arbitration): --rebuild-binding and
141
180
  // --cleanup-stale BOTH mutate the binding file. Running them
142
181
  // together is ambiguous (rebuild rewrites callerIds; cleanup
@@ -209,9 +248,12 @@ export function registerDoctorCommand(program, io) {
209
248
  droppedCount: droppedStale.length,
210
249
  droppedSids: droppedStale
211
250
  };
212
- const data = logsSection === null
251
+ const fullData = logsSection === null
213
252
  ? { ...report, staleBinding: staleBindingSection }
214
253
  : { ...report, logs: logsSection, staleBinding: staleBindingSection };
254
+ // Slice B: `--summary` is opt-in and affects the JSON envelope only (the
255
+ // human-readable path below already prints one line per check).
256
+ const data = options.summary === true ? buildDoctorSummary(fullData) : fullData;
215
257
  // Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
216
258
  // repair cycle: `report.summary.ok` already factors in the
217
259
  // severity-aware aggregation in `buildReport` (warnings do NOT
@@ -1,6 +1,8 @@
1
- import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult } from '../../../services/memory/project-memory-service.js';
1
+ import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult, VALID_PROJECT_MEMORY_KINDS } from '../../../services/memory/project-memory-service.js';
2
2
  import { fail, ok } from 'peaks-loop-shared/result';
3
3
  import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
4
+ /** Derived from the canonical kind vocabulary — never hand-maintain a list here. */
5
+ const KIND_HELP = VALID_PROJECT_MEMORY_KINDS.join(', ');
4
6
  export function registerMemoryCommand(program, io) {
5
7
  const memory = program.command('memory').description('Manage project-local Peaks memory');
6
8
  addJsonOption(memory
@@ -48,9 +50,10 @@ export function registerMemoryCommand(program, io) {
48
50
  addJsonOption(memory
49
51
  .command('list')
50
52
  .description('List all memory entries from .peaks/memory/index.json. Pass --pick to spawn fzf for interactive multi-select; the picked subset is written to .peaks/memory/picked.json.')
51
- .option('--kind <kind>', 'filter by memory kind (one of: project, rule, decision, reference, feedback, convention, module, lesson)')
53
+ .option('--kind <kind>', `filter by memory kind (one of: ${KIND_HELP})`)
52
54
  .option('--pick', 'spawn fzf for interactive multi-select (requires fzf >= 0.38); writes picked.json')
53
55
  .option('--fzf-bin <path>', 'override fzf binary path (default: fzf on PATH)', 'fzf')
56
+ .option('--summary', 'emit counts + names-of-first-N only (≤ 2 KB) instead of the full entry array; the default envelope is unchanged')
54
57
  .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((options) => {
55
58
  void import('../memory-commands.js').then(({ runMemoryList }) => {
56
59
  void runMemoryList(io, {
@@ -58,6 +61,7 @@ export function registerMemoryCommand(program, io) {
58
61
  ...(options.pick === true ? { pick: true } : {}),
59
62
  ...(options.fzfBin ? { fzfBin: options.fzfBin } : {}),
60
63
  ...(options.project !== undefined ? { project: options.project } : {}),
64
+ ...(options.summary === true ? { summary: true } : {}),
61
65
  ...(options.json !== undefined ? { json: options.json } : {}),
62
66
  });
63
67
  }).catch((error) => {
@@ -72,10 +76,68 @@ export function registerMemoryCommand(program, io) {
72
76
  process.exitCode = 1;
73
77
  });
74
78
  });
79
+ addJsonOption(memory
80
+ .command('reindex')
81
+ .description('Rebuild .peaks/memory/index.json from every memory file on disk and regenerate MEMORY.md; reports unclassified files and orphans both ways. Dry-run by default; pass --apply to write.')
82
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
83
+ .option('--dry-run', 'report drift without writing (default)')
84
+ .option('--apply', 'rebuild index.json and regenerate MEMORY.md')
85
+ .option('--summary', 'emit drift counts + names-of-first-N only (≤ 2 KB) instead of the full arrays; the default envelope is unchanged')).action((options) => {
86
+ void import('../memory-commands.js').then(({ runMemoryReindex }) => {
87
+ void runMemoryReindex(io, {
88
+ ...(options.project !== undefined ? { project: options.project } : {}),
89
+ ...(options.dryRun === true ? { dryRun: true } : {}),
90
+ ...(options.apply === true ? { apply: true } : {}),
91
+ ...(options.summary === true ? { summary: true } : {}),
92
+ ...(options.json !== undefined ? { json: options.json } : {}),
93
+ });
94
+ }).catch((error) => {
95
+ printResult(io, fail('memory.reindex', 'MEMORY_REINDEX_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
96
+ process.exitCode = 1;
97
+ });
98
+ });
99
+ addJsonOption(memory
100
+ .command('ingest')
101
+ .description('Import memories written by the IDE-side agent (~/.claude/projects/<hash>/memory/*.md) into the peaks-owned .peaks/memory store. The IDE-side dir is read-only. Dry-run by default; pass --apply to write.')
102
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
103
+ .option('--source-dir <path>', 'override the IDE-side memory directory')
104
+ .option('--dry-run', 'preview imports without writing (default)')
105
+ .option('--apply', 'write normalized memories into .peaks/memory')).action((options) => {
106
+ void import('../memory-commands.js').then(({ runMemoryIngest }) => {
107
+ void runMemoryIngest(io, {
108
+ ...(options.project !== undefined ? { project: options.project } : {}),
109
+ ...(options.sourceDir !== undefined ? { sourceDir: options.sourceDir } : {}),
110
+ ...(options.dryRun === true ? { dryRun: true } : {}),
111
+ ...(options.apply === true ? { apply: true } : {}),
112
+ ...(options.json !== undefined ? { json: options.json } : {}),
113
+ });
114
+ }).catch((error) => {
115
+ printResult(io, fail('memory.ingest', 'MEMORY_INGEST_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
116
+ process.exitCode = 1;
117
+ });
118
+ });
119
+ addJsonOption(memory
120
+ .command('rotate')
121
+ .description('Tier-driven retention for .peaks/memory/ (sediment pruning policy, tier 1: archive only, never delete). Tier assignment: explicit `metadata.tier: A|B|C|D` wins; else files under archived/ are D, files pinned in MEMORY.md are B, kinds rule/convention/project-rule are A, kinds decision/reference/feedback/module/bug/investigation/technical-pattern are B, everything else is C. Tier C older than 6 months (frontmatter updatedAt/updated/modified, else file mtime) and not pinned is archived; tier D is reported as a delete-candidate only. Tier A/B are never selected, every candidate must pass a reference grep against src/ + skills/, and --apply refuses an empty plan or a failed gate. Dry-run by default.')
122
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
123
+ .option('--dry-run', 'report the rotation plan without moving anything (default)')
124
+ .option('--apply', 'move tier-C candidates into .peaks/memory/archived/')).action((options) => {
125
+ void import('../memory-commands.js').then(({ runMemoryRotate }) => {
126
+ void runMemoryRotate(io, {
127
+ ...(options.project !== undefined ? { project: options.project } : {}),
128
+ ...(options.dryRun === true ? { dryRun: true } : {}),
129
+ ...(options.apply === true ? { apply: true } : {}),
130
+ ...(options.json !== undefined ? { json: options.json } : {}),
131
+ });
132
+ }).catch((error) => {
133
+ printResult(io, fail('memory.rotate', 'MEMORY_ROTATE_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
134
+ process.exitCode = 1;
135
+ });
136
+ });
75
137
  addJsonOption(memory
76
138
  .command('search <query>')
77
139
  .description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6.')
78
- .option('--kind <kind>', 'filter by memory kind (one of: project, rule, decision, reference, feedback, convention, module, lesson)')
140
+ .option('--kind <kind>', `filter by memory kind (one of: ${KIND_HELP})`)
79
141
  .option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
80
142
  .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
81
143
  // Lazy import avoids a top-of-file import cycle (memory-commands.ts
@@ -35,9 +35,10 @@ import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-type
35
35
  import { writeLogEntry } from '../../services/log/logger.js';
36
36
  import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
37
37
  import { runDispatchFromDag } from './dispatch-from-dag.js';
38
- import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
39
- import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
38
+ import { TEST_TOOL_DETECTION_BLOCK } from '../../services/dispatch/test-tool-detection.js';
39
+ import { MemoryPreflightService, deriveMemoryQuery } from '../../services/context/memory-preflight-service.js';
40
40
  import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
41
+ import { readSessionCapsule } from '../../services/dispatch/session-capsule.js';
41
42
  import { computeUiLibraryDispatchBlock } from '../../services/standards/ui-library-dispatch-block.js';
42
43
  import { readFreshContextBlock } from '../../services/fresh-context/fresh-context-block.js';
43
44
  import { createDispatchProvenanceToken, DISPATCH_PROVENANCE_ENV, writeDispatchProvenance, } from '../../services/worktree/dispatch-provenance.js';
@@ -373,7 +374,9 @@ export function registerDispatchCommand(parent, io) {
373
374
  // memory preflight block (or silently skip when unavailable) via the
374
375
  // pure-function builder.
375
376
  const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
376
- const memoryBlock = await preflightService.fetchBlock(role);
377
+ // Slice 2026-09-09-memory-retrieval: rank the injected memory by the
378
+ // task at hand (role + first line of the brief), not the bare role.
379
+ const memoryBlock = await preflightService.fetchBlock(deriveMemoryQuery(role, options.prompt));
377
380
  // Slice 2026-09-03-codegraph-preread (Option A): pre-dispatch
378
381
  // codegraph preflight for RD planning. BEFORE the RD sub-agent's
379
382
  // prompt is composed, ensure the codegraph index exists (init +
@@ -441,10 +444,17 @@ export function registerDispatchCommand(parent, io) {
441
444
  // "no probe available" hint instead of failing the
442
445
  // dispatch.
443
446
  }
447
+ // Slice 2026-09-10-dispatch-token-and-swarm §4: advisory background
448
+ // capsule published by the orchestrator via `peaks sub-agent share`.
449
+ // Absent capsule → no pointer and no precedence line (byte-identical
450
+ // legacy prompt).
451
+ const capsuleRef = readSessionCapsule({ projectRoot, sid, rid });
444
452
  const memoryAugmentedBody = buildDispatchSystemPrompt({
445
453
  taskTitle: role,
446
454
  taskBody: options.prompt,
447
455
  memoryBlock,
456
+ // 2026-09-10-dispatch-block-d (Option D): the composer owns the ONE
457
+ // unified Test Tool Detection injection for every role.
448
458
  contextProbe,
449
459
  // exactOptionalPropertyTypes: only set codegraphBlock when the rd
450
460
  // preflight actually produced a value (null = attempted-unavailable,
@@ -457,7 +467,11 @@ export function registerDispatchCommand(parent, io) {
457
467
  // Same optionality contract for the fresh-context block: set only
458
468
  // when the rd/prd read ran (null = no block on disk → no block,
459
469
  // undefined = non-rd/prd role → legacy prompt unchanged).
460
- ...(freshContextBlock !== undefined ? { freshContextBlock } : {})
470
+ ...(freshContextBlock !== undefined ? { freshContextBlock } : {}),
471
+ // §4: advisory session capsule pointer + precedence line.
472
+ ...(capsuleRef !== null
473
+ ? { capsule: { batchId: capsuleRef.batchId, key: capsuleRef.key, bytes: capsuleRef.bytes } }
474
+ : {})
461
475
  });
462
476
  // Part 2.C: when --isolation worktree, prepend an isolation envelope
463
477
  // block so the sub-agent sees the lease id + worktree path. The block
@@ -496,7 +510,7 @@ export function registerDispatchCommand(parent, io) {
496
510
  `confirm the file exists. Anti-fake-green rule (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure §Lesson 1): ` +
497
511
  `if the file does not exist, your verdict MUST be \`status: "blocked"\` with reason "must_ls_files_failed". Do NOT silently skip this step.\n`;
498
512
  }
499
- const effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
513
+ const effectivePrompt = `${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
500
514
  const warnings = [...decision.warnings];
501
515
  let toolCall;
502
516
  try {
@@ -32,6 +32,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
32
32
  import { detectInstalledIde } from '../../services/ide/ide-detector.js';
33
33
  import { getAdapter } from '../../services/ide/ide-registry.js';
34
34
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
35
+ import { planFileOverlapWaves } from '../../services/dispatch/file-overlap-wave-planner.js';
35
36
  export async function runDispatchFromDag(role, options, asJson, io) {
36
37
  if (!options.fromDag)
37
38
  return;
@@ -135,6 +136,19 @@ export async function runDispatchFromDag(role, options, asJson, io) {
135
136
  // end-to-end, but they are NOT surfaced in the CLI envelope (the LLM
136
137
  // sees them only after re-invoking with fresh level-1 contracts).
137
138
  const firstLevelIds = new Set(levelArr[0] ?? []);
139
+ // Slice 2026-09-10-dispatch-token-and-swarm §3: when EVERY first-level
140
+ // node declares `files`, refine the level into file-overlap waves so the
141
+ // LLM fans out without serializing on a shared file. Emitted additively
142
+ // in the envelope; the topological dispatch flow itself is unchanged.
143
+ // Nodes without `files` → `firstLevelWaves: null` (legacy envelope).
144
+ const firstLevelDescriptors = [...firstLevelIds].map((id) => ({
145
+ id,
146
+ files: dag.nodes.find((n) => n.id === id)?.files ?? []
147
+ }));
148
+ const firstLevelWaves = firstLevelDescriptors.length > 0
149
+ && firstLevelDescriptors.every((d) => d.files.length > 0)
150
+ ? planFileOverlapWaves(firstLevelDescriptors).waves
151
+ : null;
138
152
  const emittedToolCalls = [];
139
153
  const emittedSliceIds = [];
140
154
  // CLI runner: emit the per-slice `buildToolCall` envelope, then return
@@ -240,6 +254,9 @@ export async function runDispatchFromDag(role, options, asJson, io) {
240
254
  dispatchCount: emittedSliceIds.length,
241
255
  levelsTotal: levelArr.length,
242
256
  firstLevel: emittedSliceIds,
257
+ // §3: file-overlap wave plan for the first level (null when any
258
+ // first-level node omits `files`). Additive field.
259
+ firstLevelWaves,
243
260
  toolCalls: emittedToolCalls,
244
261
  existingContractCount: existingContracts.length,
245
262
  expectedCompletionSeconds: 60,
@@ -1,3 +1,5 @@
1
+ import { type MemoryIndexEntry, type MemoryIndexSnapshot, type ProjectMemoryKind } from '../../services/memory/memory-search-service.js';
2
+ import { type MemoryReindexReport } from '../../services/memory/project-memory-service.js';
1
3
  import { type ProgramIO } from '../cli-helpers.js';
2
4
  export interface MemorySearchCommandOptions {
3
5
  query: string;
@@ -12,6 +14,47 @@ export interface MemoryListCommandOptions {
12
14
  fzfBin?: string;
13
15
  project?: string;
14
16
  json?: boolean;
17
+ /** Slice B: emit counts + names-of-first-N instead of the full entry array. */
18
+ summary?: boolean;
19
+ }
20
+ export interface MemoryReindexCommandOptions {
21
+ project?: string;
22
+ dryRun?: boolean;
23
+ apply?: boolean;
24
+ json?: boolean;
25
+ /** Slice B: emit counts + names-of-first-N instead of the full arrays. */
26
+ summary?: boolean;
27
+ }
28
+ /**
29
+ * Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
30
+ * `memory reindex`. The full report's arrays stay on disk / in the default
31
+ * envelope; this replaces them with `{count, names}` views (≤ 2 KB).
32
+ */
33
+ export declare function buildMemoryReindexSummary(report: MemoryReindexReport): Record<string, unknown>;
34
+ /**
35
+ * Slice B: bounded view of `memory list`. `count` is the true total; `names`
36
+ * carries `name (kind)` labels for the first N entries.
37
+ */
38
+ export declare function buildMemoryListSummary(data: {
39
+ snapshot: MemoryIndexSnapshot;
40
+ entries: readonly MemoryIndexEntry[];
41
+ kindFilter: ProjectMemoryKind | undefined;
42
+ pickedEntries: readonly MemoryIndexEntry[];
43
+ pickedOutputPath: string | null;
44
+ fzfVersion: string | null;
45
+ }): Record<string, unknown>;
46
+ export interface MemoryIngestCommandOptions {
47
+ project?: string;
48
+ sourceDir?: string;
49
+ dryRun?: boolean;
50
+ apply?: boolean;
51
+ json?: boolean;
52
+ }
53
+ export interface MemoryRotateCommandOptions {
54
+ project?: string;
55
+ dryRun?: boolean;
56
+ apply?: boolean;
57
+ json?: boolean;
15
58
  }
16
59
  export declare function runMemoryList(io: ProgramIO, options: MemoryListCommandOptions): Promise<void>;
17
60
  /**
@@ -19,3 +62,19 @@ export declare function runMemoryList(io: ProgramIO, options: MemoryListCommandO
19
62
  * exercise the full envelope without spawning a subprocess.
20
63
  */
21
64
  export declare function runMemorySearch(io: ProgramIO, options: MemorySearchCommandOptions): Promise<void>;
65
+ /**
66
+ * `peaks memory reindex` — rebuild `.peaks/memory/index.json` from disk and
67
+ * regenerate `MEMORY.md`. Dry-run by default; `--apply` writes.
68
+ */
69
+ export declare function runMemoryReindex(io: ProgramIO, options: MemoryReindexCommandOptions): Promise<void>;
70
+ /**
71
+ * `peaks memory rotate` — tier-driven retention for `.peaks/memory/`.
72
+ * Implements the sediment pruning policy (tier 1: archive, never delete).
73
+ * Dry-run by default; `--apply` moves tier-C candidates into `archived/`.
74
+ */
75
+ export declare function runMemoryRotate(io: ProgramIO, options: MemoryRotateCommandOptions): Promise<void>;
76
+ /**
77
+ * `peaks memory ingest` — import memories written by the IDE-side agent into
78
+ * `.peaks/memory/`. The IDE-side source is read-only; dry-run by default.
79
+ */
80
+ export declare function runMemoryIngest(io: ProgramIO, options: MemoryIngestCommandOptions): Promise<void>;