peaks-loop 4.0.33 → 4.0.35

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 (88) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli/commands/code-commands.js +2 -0
  3. package/dist/cli/commands/code-mode-gate-commands.js +3 -3
  4. package/dist/cli/commands/code-mode-status-command.d.ts +17 -0
  5. package/dist/cli/commands/code-mode-status-command.js +51 -0
  6. package/dist/cli/commands/code-run-command.js +8 -1
  7. package/dist/cli/commands/code-runtime-commands.js +13 -5
  8. package/dist/cli/commands/core/memory-command.js +61 -3
  9. package/dist/cli/commands/core/skill-command.js +1 -1
  10. package/dist/cli/commands/dispatch-commands.js +4 -2
  11. package/dist/cli/commands/memory-commands.d.ts +35 -0
  12. package/dist/cli/commands/memory-commands.js +119 -10
  13. package/dist/cli/commands/request-commands.js +3 -7
  14. package/dist/cli/commands/session-24h-mode.js +9 -1
  15. package/dist/services/24h-mode/auto-engage.d.ts +39 -0
  16. package/dist/services/24h-mode/auto-engage.js +46 -0
  17. package/dist/services/24h-mode/decider.d.ts +11 -5
  18. package/dist/services/24h-mode/decider.js +13 -13
  19. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
  20. package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
  21. package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
  22. package/dist/services/code/auto-compact-lifecycle.js +1 -1
  23. package/dist/services/code/auto-compact-modes.d.ts +1 -1
  24. package/dist/services/code/auto-compact-modes.js +1 -1
  25. package/dist/services/code/auto-compact-orchestrator.js +18 -22
  26. package/dist/services/code/mode-gate.d.ts +6 -2
  27. package/dist/services/code/mode-gate.js +9 -5
  28. package/dist/services/code/orchestrator-can-do.js +2 -2
  29. package/dist/services/config/config-service.d.ts +17 -0
  30. package/dist/services/config/config-service.js +34 -0
  31. package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
  32. package/dist/services/context/auto-compact-dispatcher.js +4 -4
  33. package/dist/services/context/auto-compact-reader.js +8 -1
  34. package/dist/services/context/auto-compact-types.d.ts +13 -1
  35. package/dist/services/context/build-dispatch-system-prompt.js +2 -2
  36. package/dist/services/context/memory-index-reader.d.ts +26 -0
  37. package/dist/services/context/memory-index-reader.js +62 -30
  38. package/dist/services/context/memory-preflight-config.d.ts +33 -0
  39. package/dist/services/context/memory-preflight-config.js +32 -2
  40. package/dist/services/context/memory-preflight-service.d.ts +20 -1
  41. package/dist/services/context/memory-preflight-service.js +198 -31
  42. package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
  43. package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
  44. package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
  45. package/dist/services/hooks/auto-compact-hook-install.js +2 -2
  46. package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
  47. package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
  48. package/dist/services/ide/ide-types.d.ts +10 -1
  49. package/dist/services/job/job-types.d.ts +3 -3
  50. package/dist/services/memory/memory-ingest-service.d.ts +79 -0
  51. package/dist/services/memory/memory-ingest-service.js +225 -0
  52. package/dist/services/memory/memory-rotate-service.d.ts +88 -0
  53. package/dist/services/memory/memory-rotate-service.js +373 -0
  54. package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
  55. package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
  56. package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
  57. package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
  58. package/dist/services/memory/project-memory-service/index/search.js +14 -24
  59. package/dist/services/memory/project-memory-service/index.d.ts +7 -3
  60. package/dist/services/memory/project-memory-service/index.js +6 -2
  61. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +75 -3
  62. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
  63. package/dist/services/memory/project-memory-service/types.d.ts +31 -1
  64. package/dist/services/memory/project-memory-service/types.js +76 -1
  65. package/dist/services/mode/mode-enforcement.d.ts +4 -1
  66. package/dist/services/mode/mode-enforcement.js +26 -28
  67. package/dist/services/mode/mode-status-service.d.ts +81 -0
  68. package/dist/services/mode/mode-status-service.js +108 -0
  69. package/dist/services/preferences/preferences-types.d.ts +14 -0
  70. package/dist/services/preferences/preferences-types.js +8 -0
  71. package/dist/services/share/run-state-contract.d.ts +1 -1
  72. package/dist/services/skills/presence-lease-service.d.ts +21 -0
  73. package/dist/services/skills/presence-lease-service.js +26 -0
  74. package/dist/services/skills/skill-presence-service.d.ts +10 -1
  75. package/dist/services/skills/skill-presence-service.js +32 -4
  76. package/dist/services/skills/skill-statusline-service.js +6 -3
  77. package/package.json +5 -5
  78. package/skills/peaks-code/SKILL.md +10 -10
  79. package/skills/peaks-code/references/mode-selection.md +7 -3
  80. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  81. package/skills/peaks-code/references/runbook.md +16 -5
  82. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  83. package/skills/peaks-code/references/standards-preflight.md +1 -1
  84. package/skills/peaks-code/references/startup-sequence.md +1 -1
  85. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  86. package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
  87. package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
  88. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.35 — 2026-09-10 (记忆系统 overhaul — 写入合一 / 索引重建 / 按任务调取 / 漂移健康检查 / rotate)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **记忆写入单一权威** — `peaks memory ingest` 把 IDE 侧(Claude Code)记忆导入 `.peaks/memory/`,源目录只读、**绝不写 `~/.claude/`**;SKILL.md 明确:工作流内"沉淀记忆"落 `.peaks/memory/`,IDE 侧仅作会话笔记。
8
+
9
+ 2. **索引重建 + 根因修复** — `peaks memory reindex` 全量重扫并重建 `index.json`。**根因**:共享 frontmatter 解析器只认顶层 `type:`,按契约写 `metadata.type` 的文件被**静默丢弃**。现按 `metadata.type → kind → type` 解析,未识别值**输出清单**而非静默跳过。name 解析新增 `name → title → 文件名` 回退;重名冲突上报不覆盖。本仓库实测:索引 **231 → 276**。
10
+
11
+ 3. **调取按任务 + 分层预算** — preflight 此前 `fetchBlock(_taskTitle)` 的 `taskTitle` **完全未使用**,永远注入同一批 feedback+layerA。现按任务排序(per-token fuzzy,无网络/无 embedding),hot 必进、warm 按相关度入选,三重预算(条数/字节/时间)+ 可观测字段。warm 层首次可被注入。顺带修:读取器原来只扁平化 4 种 kind,丢了 decision/rule/convention/module/lesson。
12
+
13
+ 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。
14
+
15
+ 5. **`peaks memory rotate`(清除机制)** — 实现 2026-07-24 pruning policy 早已规定但从未实现的轮转:A/B 层永不入选(测试断言)、C 层满 **6 个月**且未被钉住 → 归档、D 层 → 报告待删(不删)、每个候选先过 `src/`+`skills/` 引用 grep。默认 dry-run。
16
+
17
+ 6. **doctor 漂移检查** — 新增 `l3-memory-coverage` / `l3-memory-orphans` / `l3-memory-unclassified`;此前只校验"index.json 是合法 JSON",46% 不可见也报 ok。
18
+
19
+ **验证**:build clean、tsc clean、全量 133 files / 1180 passed(1 skipped);本仓库 rotate dry-run `A:22 B:146 C:138 D:12`。
20
+
21
+ ## 4.0.34 — 2026-09-09 (mode 模型收敛 + auto-compact 死命令 + 上下文窗口覆盖 + 确认门去 TTY)
22
+
23
+ **Highlights**:
24
+
25
+ 1. **mode 模型收敛(用户设计)** — 此前 `SkillPresenceMode` 与 24h 状态机是两个互不相识的自主性开关。
26
+
27
+ - `SkillPresenceMode = full-auto | assisted | strict | 24h`(四选一,并列)。
28
+ - `swarm` 从"模式"降级为**所有 mode 的默认执行策略**(与"fan-out 强制、不可退回串行"一致)。
29
+ - 24h 的 6 状态机保留为 **24h mode 的内部状态**;24h 是**唯一可自动开启**的 mode(T1–T5),其余 mode 仍需用户选。
30
+ - 新增 `peaks code mode status`:一条命令输出 presence mode / 24h 状态 / job mode / auto-compact profile。
31
+ - **顺带修好一个严重 bug**:`getSkillPresence()` 一直在丢掉 `mode` 字段,导致所有读 `presence.mode` 的守卫(mode-enforcement、assisted/strict 边界、post-compact-detector)**全是死代码**——用户选的 mode 从未真正生效。
32
+
33
+ 2. **auto-compact 契约指向不存在的命令(根因)** — `peaks compact auto --execute` 在 `skills/` + `src/` 出现 **49 次**(含 CLI 自己返回的 `next`),实际执行是 `COMMAND_NOT_FOUND`。真命令是 `peaks code auto-compact`。LLM 按契约执行 → 撞死命令 → 只能回头让用户手动 compact。全部引用已改指真命令,并加守卫测试防回归。
34
+
35
+ 3. **上下文窗口不再对未知模型默认 20w** — 新增 `PEAKS_CONTEXT_WINDOW_TOKENS`(env)与 `context.windowTokens`(config)覆盖,优先级 env > config > 启发式 > 默认;`peaks code context-now` 新增 `capacitySource` 字段,一眼看出窗口值来自哪。
36
+
37
+ 4. **assisted/strict 确认门不再阻塞在 TTY** — `requireUserConfirmation` 此前会开 `readline` 的 `(y/N)` 提示;LLM 会话里 stdin 非 TTY → 直接挂死,且违反 Human-NL-Choice-Only。现改为立即抛出带 `transitionKey` + `mode` + 可执行 nextActions 的错误,由 LLM 用 `AskUserQuestion` 承接、用户同意后重跑 `--confirm`。bypass 语义(`--confirm` / `--force-confirm` / `PEAKS_AUTO_CONFIRM`)完全不变。
38
+
39
+ **验证**:build clean、tsc clean、全量 126 files / 1102 passed(1 skipped);实测 `capacitySource` 默认 `model-heuristic`、`PEAKS_CONTEXT_WINDOW_TOKENS=400000` → `env-override`。
40
+
3
41
  ## 4.0.33 — 2026-09-09 (ECC 去插件化 + 动态获取修复 + 移除 understand-anything)
4
42
 
5
43
  **Highlights**:
@@ -23,6 +23,7 @@
23
23
  * registerCodeCommands (now a thin orchestrator).
24
24
  */
25
25
  import { registerCodeRunCommand } from './code-run-command.js';
26
+ import { registerCodeModeStatusCommand } from './code-mode-status-command.js';
26
27
  import { registerCodeModeGateCommands } from './code-mode-gate-commands.js';
27
28
  import { registerCodeJobShapeCommands } from './code-job-shape-commands.js';
28
29
  import { registerCodeRuntimeCommands } from './code-runtime-commands.js';
@@ -113,6 +114,7 @@ export function registerCodeCommands(program, io) {
113
114
  .command('code', { hidden: true })
114
115
  .description('peaks-code LLM-side workflow planner (slice 2 fast mode)');
115
116
  registerCodeModeGateCommands(code, io);
117
+ registerCodeModeStatusCommand(code, io);
116
118
  registerCodeJobShapeCommands(code, io);
117
119
  registerCodeRuntimeCommands(code, io);
118
120
  registerCodeOrchestratorCanDoCommand(code, io);
@@ -35,7 +35,7 @@ export function registerCodeModeGateCommands(code, io) {
35
35
  addJsonOption(code
36
36
  .command('should-pause')
37
37
  .description('v2.11.0 D5: ask the mode-gate whether the LLM should pause for an AskUserQuestion at a given step. ' +
38
- 'full-auto / swarm auto-proceed (recommended = chosen); assisted / strict pause. ' +
38
+ 'full-auto / 24h auto-proceed (recommended = chosen); assisted / strict pause. ' +
39
39
  'The 3 hard-floor categories always pause regardless of mode. ' +
40
40
  'v2.15.0 slice 002 AC-2: when --step step-1-mode-select AND the recorded skill presence is stale ' +
41
41
  '(outer-session-mismatch / no-presence), the gate returns shouldPause: true with reason "stale-presence" ' +
@@ -50,7 +50,7 @@ export function registerCodeModeGateCommands(code, io) {
50
50
  // pause on `step-1-mode-select` (mode-selection-itself) will
51
51
  // pause regardless, and the LLM-side caller can present
52
52
  // AskUserQuestion without first knowing the mode.
53
- .option('--mode <mode>', 'one of: full-auto, assisted, swarm, strict. Defaults to full-auto when omitted (Step 1 chicken-and-egg fix).')
53
+ .option('--mode <mode>', 'one of: full-auto, assisted, strict, 24h. Defaults to full-auto when omitted (Step 1 chicken-and-egg fix).')
54
54
  .option('--hard-floor <category>', 'optional hard-floor override (irreversible-external-side-effect | authentication-credential | multi-day-investment | commit-boundary-side-effect)')
55
55
  .option('--recommended <option>', 'recommended option label to log when auto-proceeding', 'recommended-option')
56
56
  .option('--project <path>', 'v2.15.0 slice 002 AC-2: project root for presence:check-stale. Default: cwd. Pass only when step=step-1-mode-select.')
@@ -64,7 +64,7 @@ export function registerCodeModeGateCommands(code, io) {
64
64
  // step-1-mode-select will still pause regardless.
65
65
  const mode = opts.mode ?? 'full-auto';
66
66
  if (!isCodeMode(mode)) {
67
- printResult(io, fail('code.should-pause', 'INVALID_MODE', `mode must be one of full-auto, assisted, swarm, strict (got "${mode}")`, { provided: mode }, ['Pass --mode full-auto | assisted | swarm | strict']), opts.json);
67
+ printResult(io, fail('code.should-pause', 'INVALID_MODE', `mode must be one of full-auto, assisted, strict, 24h (got "${mode}")`, { provided: mode }, ['Pass --mode full-auto | assisted | strict | 24h']), opts.json);
68
68
  process.exitCode = 1;
69
69
  return;
70
70
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Slice 2026-09-09-mode-consolidation (Slice D) — `peaks code mode status`.
3
+ *
4
+ * One read for the whole stacked autonomy picture:
5
+ * - presence MODE (`full-auto | assisted | strict | 24h`),
6
+ * - the 24h internal state machine (only when the mode is `24h`),
7
+ * - Job mode,
8
+ * - the resolved auto-compact profile (`standard | partial`).
9
+ *
10
+ * Read-only and fail-soft: the service degrades every unreadable
11
+ * artifact to `null` / `false`; this command only wraps the call in an
12
+ * envelope. Nested under the existing `peaks code` verb (no new
13
+ * top-level verb — peaks-loop is enhancement, not a new CLI surface).
14
+ */
15
+ import type { Command } from 'commander';
16
+ import { type ProgramIO } from '../cli-helpers.js';
17
+ export declare function registerCodeModeStatusCommand(code: Command, io: ProgramIO): void;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Slice 2026-09-09-mode-consolidation (Slice D) — `peaks code mode status`.
3
+ *
4
+ * One read for the whole stacked autonomy picture:
5
+ * - presence MODE (`full-auto | assisted | strict | 24h`),
6
+ * - the 24h internal state machine (only when the mode is `24h`),
7
+ * - Job mode,
8
+ * - the resolved auto-compact profile (`standard | partial`).
9
+ *
10
+ * Read-only and fail-soft: the service degrades every unreadable
11
+ * artifact to `null` / `false`; this command only wraps the call in an
12
+ * envelope. Nested under the existing `peaks code` verb (no new
13
+ * top-level verb — peaks-loop is enhancement, not a new CLI surface).
14
+ */
15
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
16
+ import { fail, ok } from 'peaks-loop-shared/result';
17
+ import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
18
+ import { resolveModeStatus } from '../../services/mode/mode-status-service.js';
19
+ export function registerCodeModeStatusCommand(code, io) {
20
+ // Explicit two-step nesting (NOT `.command('mode status')` — that form
21
+ // creates a positional `<status>` argument, not a subcommand).
22
+ const mode = code
23
+ .command('mode')
24
+ .description('Mode introspection (read-only). Sub-actions: status.');
25
+ addJsonOption(mode
26
+ .command('status')
27
+ .description('v4.0.34 (slice 2026-09-09-mode-consolidation): one read for the whole stacked ' +
28
+ 'autonomy picture — presence mode, the 24h internal state (when mode=24h), Job mode, ' +
29
+ 'and the resolved auto-compact profile. Read-only + fail-soft.')
30
+ .option('--project <path>', 'project root (defaults to current directory)')
31
+ .option('--session-id <sessionId>', 'explicit session id (defaults to the canonical binding)')).action((options) => {
32
+ try {
33
+ const projectRoot = resolveCanonicalProjectRoot(options.project ?? process.cwd());
34
+ const status = resolveModeStatus({
35
+ projectRoot,
36
+ sessionId: options.sessionId ?? null
37
+ });
38
+ printResult(io, ok('code.mode.status', { projectRoot, ...status }, [], [
39
+ status.mode === null
40
+ ? 'No presence mode recorded — Step 1 mode selection has not run for this session.'
41
+ : `mode=${status.mode} (${status.modeSource}) → auto-compact profile=${status.autoCompactProfile}`
42
+ ]), options.json);
43
+ }
44
+ catch (error) {
45
+ printResult(io, fail('code.mode.status', 'MODE_STATUS_FAILED', getErrorMessage(error), null, [
46
+ 'Verify the project path and try again'
47
+ ]), options.json);
48
+ process.exitCode = 1;
49
+ }
50
+ });
51
+ }
@@ -1,5 +1,6 @@
1
1
  import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
2
2
  import { read24hState, write24hState } from '../../services/24h-mode/index.js';
3
+ import { applyAutoEngagePresenceMode } from '../../services/24h-mode/auto-engage.js';
3
4
  import { getErrorMessage } from '../cli-helpers.js';
4
5
  function emit(io, payload, json) {
5
6
  io.stdout((json === true ? JSON.stringify(payload) : JSON.stringify(payload, null, 2)) + '\n');
@@ -54,9 +55,15 @@ export function registerCodeRunCommand(code, io) {
54
55
  enteredFrom: current.state,
55
56
  exitCondition: null
56
57
  });
58
+ // Slice 2026-09-09-mode-consolidation (Slice B): auto-engage is the
59
+ // ONLY path allowed to set a mode without a user pick, and `24h` is
60
+ // the only mode it may set. Stamp it onto the in-flight presence
61
+ // lease so the mode gate / statusline / `peaks code mode status`
62
+ // see the same autonomy level the 24h state machine is running.
63
+ const presenceMode = applyAutoEngagePresenceMode({ projectRoot, sessionId: sid });
57
64
  emit(io, {
58
65
  ok: true,
59
- data: { changeId: changeId ?? null, mode: '24H_ACTIVE', state: nextState, autoEngaged: true, trigger: options.trigger ?? options.tier, sessionId: sid, path: next.path },
66
+ data: { changeId: changeId ?? null, mode: '24H_ACTIVE', state: nextState, autoEngaged: true, trigger: options.trigger ?? options.tier, sessionId: sid, path: next.path, presenceMode },
60
67
  next: 'Continue the existing 24H_ACTIVE flow; no brainstorming gate is required for T3/T4.'
61
68
  }, options.json);
62
69
  }
@@ -140,7 +140,11 @@ export function registerCodeRuntimeCommands(code, io) {
140
140
  'hermes / openclaw register their own env-var via IdeAdapter.compact. ' +
141
141
  'v3.1.2: when --enforce-job-mode is set OR job-shape.json says isJob=true, ' +
142
142
  '≥0.85 emits action=auto-compact-now (MANDATORY, not advisory) and ' +
143
- '≥0.95 emits action=red-line (forced hook fires next turn).')
143
+ '≥0.95 emits action=red-line (forced hook fires next turn). ' +
144
+ 'Context-window override: set env PEAKS_CONTEXT_WINDOW_TOKENS=<positive int> ' +
145
+ 'or `peaks config set --key context.windowTokens --value <positive int>`; ' +
146
+ 'the JSON envelope reports the winning layer as capacitySource ' +
147
+ '(env-override | config | model-heuristic | default).')
144
148
  .requiredOption('--project <path>', 'target project root')
145
149
  .option('--session-id <sid>', 'override session id (default: read from active presence)')
146
150
  .option('--enforce-job-mode', 'v3.1.2: treat ≥0.85 as MANDATORY auto-compact (not advisory). Auto-enabled when job-shape.json says isJob=true.')
@@ -190,12 +194,12 @@ export function registerCodeRuntimeCommands(code, io) {
190
194
  let next = null;
191
195
  if (probe.ratio >= 0.95) {
192
196
  action = isJobMode ? 'red-line' : 'red-line';
193
- next = 'peaks compact auto --execute';
197
+ next = 'peaks code auto-compact';
194
198
  }
195
199
  else if (probe.ratio >= 0.85) {
196
200
  if (isJobMode) {
197
201
  action = 'auto-compact-now';
198
- next = 'peaks compact auto --execute';
202
+ next = 'peaks code auto-compact';
199
203
  }
200
204
  else {
201
205
  action = 'soft-warn';
@@ -224,6 +228,10 @@ export function registerCodeRuntimeCommands(code, io) {
224
228
  rawBytes: probe.rawBytes ?? null,
225
229
  rawTokens: probe.rawTokens ?? null,
226
230
  capacityTokens: probe.capacityTokens ?? null,
231
+ // Slice 2026-09-09-context-window-override: which layer produced
232
+ // capacityTokens (env-override | config | model-heuristic |
233
+ // default) — null for byte/percent sources, which have no window.
234
+ capacitySource: probe.capacitySource ?? null,
227
235
  bytesPrompt: promptSizeBytes ?? null,
228
236
  capturedAt: probe.capturedAt
229
237
  }, [], [
@@ -233,8 +241,8 @@ export function registerCodeRuntimeCommands(code, io) {
233
241
  ? `Job-mode MANDATORY auto-compact. Code MUST call \`${next}\` WITHOUT confirmation.`
234
242
  : action === 'soft-warn'
235
243
  ? isJobMode
236
- ? `Job mode soft-warn (50–85%). Continue working; the next \`peaks compact auto\` will re-check.`
237
- : `Soft warn (50–85%). Continue working; the next \`peaks compact auto\` will re-check.`
244
+ ? `Job mode soft-warn (50–85%). Continue working; the next \`peaks code auto-compact\` will re-check.`
245
+ : `Soft warn (50–85%). Continue working; the next \`peaks code auto-compact\` will re-check.`
238
246
  : `Below 50%. No action required.`,
239
247
  jobModeNotice
240
248
  ]), true);
@@ -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,7 +50,7 @@ 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')
54
56
  .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((options) => {
@@ -72,10 +74,66 @@ export function registerMemoryCommand(program, io) {
72
74
  process.exitCode = 1;
73
75
  });
74
76
  });
77
+ addJsonOption(memory
78
+ .command('reindex')
79
+ .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.')
80
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
81
+ .option('--dry-run', 'report drift without writing (default)')
82
+ .option('--apply', 'rebuild index.json and regenerate MEMORY.md')).action((options) => {
83
+ void import('../memory-commands.js').then(({ runMemoryReindex }) => {
84
+ void runMemoryReindex(io, {
85
+ ...(options.project !== undefined ? { project: options.project } : {}),
86
+ ...(options.dryRun === true ? { dryRun: true } : {}),
87
+ ...(options.apply === true ? { apply: true } : {}),
88
+ ...(options.json !== undefined ? { json: options.json } : {}),
89
+ });
90
+ }).catch((error) => {
91
+ printResult(io, fail('memory.reindex', 'MEMORY_REINDEX_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
92
+ process.exitCode = 1;
93
+ });
94
+ });
95
+ addJsonOption(memory
96
+ .command('ingest')
97
+ .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.')
98
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
99
+ .option('--source-dir <path>', 'override the IDE-side memory directory')
100
+ .option('--dry-run', 'preview imports without writing (default)')
101
+ .option('--apply', 'write normalized memories into .peaks/memory')).action((options) => {
102
+ void import('../memory-commands.js').then(({ runMemoryIngest }) => {
103
+ void runMemoryIngest(io, {
104
+ ...(options.project !== undefined ? { project: options.project } : {}),
105
+ ...(options.sourceDir !== undefined ? { sourceDir: options.sourceDir } : {}),
106
+ ...(options.dryRun === true ? { dryRun: true } : {}),
107
+ ...(options.apply === true ? { apply: true } : {}),
108
+ ...(options.json !== undefined ? { json: options.json } : {}),
109
+ });
110
+ }).catch((error) => {
111
+ printResult(io, fail('memory.ingest', 'MEMORY_INGEST_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
112
+ process.exitCode = 1;
113
+ });
114
+ });
115
+ addJsonOption(memory
116
+ .command('rotate')
117
+ .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.')
118
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
119
+ .option('--dry-run', 'report the rotation plan without moving anything (default)')
120
+ .option('--apply', 'move tier-C candidates into .peaks/memory/archived/')).action((options) => {
121
+ void import('../memory-commands.js').then(({ runMemoryRotate }) => {
122
+ void runMemoryRotate(io, {
123
+ ...(options.project !== undefined ? { project: options.project } : {}),
124
+ ...(options.dryRun === true ? { dryRun: true } : {}),
125
+ ...(options.apply === true ? { apply: true } : {}),
126
+ ...(options.json !== undefined ? { json: options.json } : {}),
127
+ });
128
+ }).catch((error) => {
129
+ printResult(io, fail('memory.rotate', 'MEMORY_ROTATE_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
130
+ process.exitCode = 1;
131
+ });
132
+ });
75
133
  addJsonOption(memory
76
134
  .command('search <query>')
77
135
  .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)')
136
+ .option('--kind <kind>', `filter by memory kind (one of: ${KIND_HELP})`)
79
137
  .option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
80
138
  .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
81
139
  // Lazy import avoids a top-of-file import cycle (memory-commands.ts
@@ -202,7 +202,7 @@ export function registerSkillCommand(program, io) {
202
202
  const projectOption = canonicalizeProjectOption(options.project);
203
203
  const projectRoot = projectOption ?? findProjectRoot(process.cwd()) ?? process.cwd();
204
204
  if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
205
- printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, swarm, strict)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, swarm, or strict']), options.json);
205
+ printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, strict, 24h)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, strict, or 24h']), options.json);
206
206
  process.exitCode = 1;
207
207
  return;
208
208
  }
@@ -36,7 +36,7 @@ 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
38
  import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
39
- import { MemoryPreflightService } from '../../services/context/memory-preflight-service.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
41
  import { computeUiLibraryDispatchBlock } from '../../services/standards/ui-library-dispatch-block.js';
42
42
  import { readFreshContextBlock } from '../../services/fresh-context/fresh-context-block.js';
@@ -373,7 +373,9 @@ export function registerDispatchCommand(parent, io) {
373
373
  // memory preflight block (or silently skip when unavailable) via the
374
374
  // pure-function builder.
375
375
  const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
376
- const memoryBlock = await preflightService.fetchBlock(role);
376
+ // Slice 2026-09-09-memory-retrieval: rank the injected memory by the
377
+ // task at hand (role + first line of the brief), not the bare role.
378
+ const memoryBlock = await preflightService.fetchBlock(deriveMemoryQuery(role, options.prompt));
377
379
  // Slice 2026-09-03-codegraph-preread (Option A): pre-dispatch
378
380
  // codegraph preflight for RD planning. BEFORE the RD sub-agent's
379
381
  // prompt is composed, ensure the codegraph index exists (init +
@@ -13,9 +13,44 @@ export interface MemoryListCommandOptions {
13
13
  project?: string;
14
14
  json?: boolean;
15
15
  }
16
+ export interface MemoryReindexCommandOptions {
17
+ project?: string;
18
+ dryRun?: boolean;
19
+ apply?: boolean;
20
+ json?: boolean;
21
+ }
22
+ export interface MemoryIngestCommandOptions {
23
+ project?: string;
24
+ sourceDir?: string;
25
+ dryRun?: boolean;
26
+ apply?: boolean;
27
+ json?: boolean;
28
+ }
29
+ export interface MemoryRotateCommandOptions {
30
+ project?: string;
31
+ dryRun?: boolean;
32
+ apply?: boolean;
33
+ json?: boolean;
34
+ }
16
35
  export declare function runMemoryList(io: ProgramIO, options: MemoryListCommandOptions): Promise<void>;
17
36
  /**
18
37
  * Run the memory search subcommand. Extracted so unit tests can
19
38
  * exercise the full envelope without spawning a subprocess.
20
39
  */
21
40
  export declare function runMemorySearch(io: ProgramIO, options: MemorySearchCommandOptions): Promise<void>;
41
+ /**
42
+ * `peaks memory reindex` — rebuild `.peaks/memory/index.json` from disk and
43
+ * regenerate `MEMORY.md`. Dry-run by default; `--apply` writes.
44
+ */
45
+ export declare function runMemoryReindex(io: ProgramIO, options: MemoryReindexCommandOptions): Promise<void>;
46
+ /**
47
+ * `peaks memory rotate` — tier-driven retention for `.peaks/memory/`.
48
+ * Implements the sediment pruning policy (tier 1: archive, never delete).
49
+ * Dry-run by default; `--apply` moves tier-C candidates into `archived/`.
50
+ */
51
+ export declare function runMemoryRotate(io: ProgramIO, options: MemoryRotateCommandOptions): Promise<void>;
52
+ /**
53
+ * `peaks memory ingest` — import memories written by the IDE-side agent into
54
+ * `.peaks/memory/`. The IDE-side source is read-only; dry-run by default.
55
+ */
56
+ export declare function runMemoryIngest(io: ProgramIO, options: MemoryIngestCommandOptions): Promise<void>;
@@ -1,20 +1,19 @@
1
1
  import { findProjectRoot } from '../../services/config/config-safety.js';
2
2
  import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
3
3
  import { loadMemoryIndex, searchMemory } from '../../services/memory/memory-search-service.js';
4
+ import { executeMemoryReindex, VALID_PROJECT_MEMORY_KINDS } from '../../services/memory/project-memory-service.js';
5
+ import { executeMemoryIngest } from '../../services/memory/memory-ingest-service.js';
6
+ import { executeMemoryRotate } from '../../services/memory/memory-rotate-service.js';
4
7
  import { pickFromList } from '../../services/fuzzy-matching/fzf-pick-service.js';
5
8
  import { fail, ok } from 'peaks-loop-shared/result';
6
9
  import { getErrorMessage, printResult } from '../cli-helpers.js';
7
10
  import { join } from 'node:path';
8
- const VALID_KINDS = [
9
- 'project',
10
- 'rule',
11
- 'decision',
12
- 'reference',
13
- 'feedback',
14
- 'convention',
15
- 'module',
16
- 'lesson',
17
- ];
11
+ const VALID_KINDS = VALID_PROJECT_MEMORY_KINDS;
12
+ function resolveMemoryProjectRoot(project) {
13
+ return project !== undefined
14
+ ? resolveCanonicalProjectRoot(project)
15
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
16
+ }
18
17
  export async function runMemoryList(io, options) {
19
18
  const projectRoot = options.project !== undefined
20
19
  ? resolveCanonicalProjectRoot(options.project)
@@ -124,3 +123,113 @@ export async function runMemorySearch(io, options) {
124
123
  process.exitCode = 1;
125
124
  }
126
125
  }
126
+ /**
127
+ * `peaks memory reindex` — rebuild `.peaks/memory/index.json` from disk and
128
+ * regenerate `MEMORY.md`. Dry-run by default; `--apply` writes.
129
+ */
130
+ export async function runMemoryReindex(io, options) {
131
+ const projectRoot = resolveMemoryProjectRoot(options.project);
132
+ if (options.dryRun === true && options.apply === true) {
133
+ printResult(io, fail('memory.reindex', 'INVALID_MEMORY_REINDEX_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview the drift report, or pass --apply to rebuild']), options.json);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ try {
138
+ const report = executeMemoryReindex({ projectRoot, apply: options.apply === true });
139
+ const nextActions = [];
140
+ if (options.apply !== true) {
141
+ nextActions.push('Preview only — re-run with --apply to rebuild index.json and regenerate MEMORY.md.');
142
+ }
143
+ if (report.unclassified.length > 0) {
144
+ nextActions.push(`${report.unclassified.length} file(s) have no resolvable kind; add \`metadata.type\` (or \`kind:\`) to index them.`);
145
+ }
146
+ if (report.orphanIndex.length > 0) {
147
+ nextActions.push(`${report.orphanIndex.length} previous index entry(ies) point at missing files; they are dropped from the rebuilt index.`);
148
+ }
149
+ if (report.nameConflicts.length > 0) {
150
+ nextActions.push(`${report.nameConflicts.length} name collision(s) across files; both entries are kept — rename one file to disambiguate.`);
151
+ }
152
+ printResult(io, ok('memory.reindex', report, [], nextActions), options.json);
153
+ }
154
+ catch (error) {
155
+ const message = getErrorMessage(error);
156
+ const code = error.code ?? 'MEMORY_REINDEX_FAILED';
157
+ printResult(io, fail('memory.reindex', code, message, { projectRoot }, ['Check that the project has a readable .peaks/memory directory']), options.json);
158
+ process.exitCode = 1;
159
+ }
160
+ }
161
+ /**
162
+ * `peaks memory rotate` — tier-driven retention for `.peaks/memory/`.
163
+ * Implements the sediment pruning policy (tier 1: archive, never delete).
164
+ * Dry-run by default; `--apply` moves tier-C candidates into `archived/`.
165
+ */
166
+ export async function runMemoryRotate(io, options) {
167
+ const projectRoot = resolveMemoryProjectRoot(options.project);
168
+ if (options.dryRun === true && options.apply === true) {
169
+ printResult(io, fail('memory.rotate', 'INVALID_MEMORY_ROTATE_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview the rotation plan, or pass --apply to archive tier-C candidates']), options.json);
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ try {
174
+ const report = executeMemoryRotate({ projectRoot, apply: options.apply === true });
175
+ const nextActions = [];
176
+ if (report.refused) {
177
+ nextActions.push(`Refused to apply: ${report.refusalReasons.join('; ')}`);
178
+ }
179
+ else if (options.apply !== true) {
180
+ nextActions.push('Preview only — re-run with --apply to move the tier-C candidates into archived/.');
181
+ }
182
+ if (report.excluded.length > 0) {
183
+ nextActions.push(`${report.excluded.length} candidate(s) excluded by a safety gate (see \`excluded\`).`);
184
+ }
185
+ const deleteCandidates = report.candidates.filter((candidate) => candidate.action === 'delete-candidate');
186
+ if (deleteCandidates.length > 0) {
187
+ nextActions.push(`${deleteCandidates.length} tier-D file(s) are delete-candidates only; peaks never deletes them — remove by hand if you are sure.`);
188
+ }
189
+ printResult(io, ok('memory.rotate', report, report.warnings, nextActions), options.json);
190
+ if (report.refused)
191
+ process.exitCode = 1;
192
+ }
193
+ catch (error) {
194
+ const message = getErrorMessage(error);
195
+ const code = error.code ?? 'MEMORY_ROTATE_FAILED';
196
+ printResult(io, fail('memory.rotate', code, message, { projectRoot }, ['Check that the project has a readable .peaks/memory directory']), options.json);
197
+ process.exitCode = 1;
198
+ }
199
+ }
200
+ /**
201
+ * `peaks memory ingest` — import memories written by the IDE-side agent into
202
+ * `.peaks/memory/`. The IDE-side source is read-only; dry-run by default.
203
+ */
204
+ export async function runMemoryIngest(io, options) {
205
+ const projectRoot = resolveMemoryProjectRoot(options.project);
206
+ if (options.dryRun === true && options.apply === true) {
207
+ printResult(io, fail('memory.ingest', 'INVALID_MEMORY_INGEST_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview imports, or pass --apply to write them into .peaks/memory']), options.json);
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ try {
212
+ const report = executeMemoryIngest({
213
+ projectRoot,
214
+ ...(options.sourceDir !== undefined ? { sourceDir: options.sourceDir } : {}),
215
+ apply: options.apply === true
216
+ });
217
+ const nextActions = [];
218
+ if (options.apply !== true && report.imported.length > 0) {
219
+ nextActions.push('Preview only — re-run with --apply to write these memories into .peaks/memory.');
220
+ }
221
+ if (report.conflicts.length > 0) {
222
+ nextActions.push(`${report.conflicts.length} conflict(s) left both copies in place; resolve them by hand.`);
223
+ }
224
+ if (report.needsClassification.length > 0) {
225
+ nextActions.push(`${report.needsClassification.length} file(s) could not be classified; add \`metadata.type\` to the source before re-running.`);
226
+ }
227
+ printResult(io, ok('memory.ingest', report, report.warnings, nextActions), options.json);
228
+ }
229
+ catch (error) {
230
+ const message = getErrorMessage(error);
231
+ const code = error.code ?? 'MEMORY_INGEST_FAILED';
232
+ printResult(io, fail('memory.ingest', code, message, { projectRoot }, ['Check the --source-dir path and that .peaks/memory is writable']), options.json);
233
+ process.exitCode = 1;
234
+ }
235
+ }
@@ -158,7 +158,7 @@ export function registerRequestCommands(program, io) {
158
158
  .option('--session-id <session>', 'restrict to a specific session id')
159
159
  .option('--reason <text>', 'reason appended as a transition note; required when --allow-incomplete is set')
160
160
  .option('--allow-incomplete', 'bypass artifact prerequisite checks; requires --reason and records the bypass in the artifact')
161
- .option('--confirm', 'skip interactive confirmation prompt (for non-interactive / LLM contexts)')
161
+ .option('--confirm', 'skip the confirmation gate (for non-interactive / LLM contexts)')
162
162
  .option('--force-confirm', 'bypass mode-enforced confirmation (use with caution)')).action(async (requestId, options) => {
163
163
  try {
164
164
  const role = options.role;
@@ -186,7 +186,7 @@ export function registerRequestCommands(program, io) {
186
186
  const presence = getSkillPresence(options.project);
187
187
  if (presence?.mode === 'assisted' || presence?.mode === 'strict') {
188
188
  if (options.confirm !== true) {
189
- printResult(io, fail('request.transition', 'ALLOW_INCOMPLETE_RESTRICTED', `--allow-incomplete requires --confirm in ${presence.mode} mode`, { role, requestId, mode: presence.mode }, ['Add --confirm to proceed non-interactively, or run in an interactive terminal.']), options.json);
189
+ printResult(io, fail('request.transition', 'ALLOW_INCOMPLETE_RESTRICTED', `--allow-incomplete requires --confirm in ${presence.mode} mode`, { role, requestId, mode: presence.mode }, ['Ask the user via AskUserQuestion whether to proceed, then re-run with --confirm if they approve.']), options.json);
190
190
  process.exitCode = 1;
191
191
  return;
192
192
  }
@@ -387,11 +387,7 @@ export function registerRequestCommands(program, io) {
387
387
  return;
388
388
  }
389
389
  if (error instanceof ConfirmationRequiredError) {
390
- printResult(io, fail('request.transition', 'CONFIRMATION_REQUIRED', error.message, { role: options.role, requestId }, [
391
- 'Add --confirm to proceed non-interactively.',
392
- 'Or run in an interactive terminal.',
393
- 'In assisted/strict mode, major workflow boundaries require explicit user approval.'
394
- ]), options.json);
390
+ printResult(io, fail('request.transition', 'CONFIRMATION_REQUIRED', error.message, { role: options.role, requestId, transitionKey: error.transitionKey, mode: error.mode }, [...error.nextActions]), options.json);
395
391
  process.exitCode = 1;
396
392
  return;
397
393
  }
@@ -20,6 +20,7 @@
20
20
  import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
21
21
  import { getErrorMessage } from '../cli-helpers.js';
22
22
  import { HANDOFF_EXIT_CONDITIONS, STATES, emptySnapshot, isHandoffExitCondition, isState, read24hState, write24hState } from '../../services/24h-mode/index.js';
23
+ import { applyAutoEngagePresenceMode } from '../../services/24h-mode/auto-engage.js';
23
24
  function splitList(value) {
24
25
  if (!value)
25
26
  return [];
@@ -135,7 +136,14 @@ export function registerSession24hModeCommand(session, io) {
135
136
  exitCondition: target === 'HANDOFF' ? (merged.exitCondition ?? null) : current.exitCondition
136
137
  };
137
138
  const result = write24hState(projectRoot, sid, next);
138
- printJson(io, { ok: true, data: { ...next, path: result.path } }, merged);
139
+ // Slice 2026-09-09-mode-consolidation (Slice B): entering 24H_ACTIVE
140
+ // is an auto-engage of the `24h` mode. Stamp it onto the presence
141
+ // lease; every other target state leaves the mode untouched (the
142
+ // guard in `applyAutoEngagePresenceMode` refuses non-24h modes).
143
+ const presenceMode = target === '24H_ACTIVE'
144
+ ? applyAutoEngagePresenceMode({ projectRoot, sessionId: sid })
145
+ : null;
146
+ printJson(io, { ok: true, data: { ...next, path: result.path, presenceMode } }, merged);
139
147
  }
140
148
  catch (error) {
141
149
  printError(io, '24H_STATE_WRITE_FAILED', getErrorMessage(error), merged);