peaks-loop 4.0.33 → 4.0.34

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 (57) hide show
  1. package/CHANGELOG.md +20 -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/skill-command.js +1 -1
  9. package/dist/cli/commands/request-commands.js +3 -7
  10. package/dist/cli/commands/session-24h-mode.js +9 -1
  11. package/dist/services/24h-mode/auto-engage.d.ts +39 -0
  12. package/dist/services/24h-mode/auto-engage.js +46 -0
  13. package/dist/services/24h-mode/decider.d.ts +11 -5
  14. package/dist/services/24h-mode/decider.js +13 -13
  15. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
  16. package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
  17. package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
  18. package/dist/services/code/auto-compact-lifecycle.js +1 -1
  19. package/dist/services/code/auto-compact-modes.d.ts +1 -1
  20. package/dist/services/code/auto-compact-modes.js +1 -1
  21. package/dist/services/code/auto-compact-orchestrator.js +18 -22
  22. package/dist/services/code/mode-gate.d.ts +6 -2
  23. package/dist/services/code/mode-gate.js +9 -5
  24. package/dist/services/code/orchestrator-can-do.js +2 -2
  25. package/dist/services/config/config-service.d.ts +17 -0
  26. package/dist/services/config/config-service.js +34 -0
  27. package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
  28. package/dist/services/context/auto-compact-dispatcher.js +4 -4
  29. package/dist/services/context/auto-compact-reader.js +8 -1
  30. package/dist/services/context/auto-compact-types.d.ts +13 -1
  31. package/dist/services/context/build-dispatch-system-prompt.js +2 -2
  32. package/dist/services/context/context-schema.d.ts +1 -1
  33. package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
  34. package/dist/services/hooks/auto-compact-hook-install.js +2 -2
  35. package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
  36. package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
  37. package/dist/services/ide/ide-types.d.ts +10 -1
  38. package/dist/services/mode/mode-enforcement.d.ts +4 -1
  39. package/dist/services/mode/mode-enforcement.js +26 -28
  40. package/dist/services/mode/mode-status-service.d.ts +81 -0
  41. package/dist/services/mode/mode-status-service.js +108 -0
  42. package/dist/services/skills/presence-lease-service.d.ts +21 -0
  43. package/dist/services/skills/presence-lease-service.js +26 -0
  44. package/dist/services/skills/skill-presence-service.d.ts +10 -1
  45. package/dist/services/skills/skill-presence-service.js +32 -4
  46. package/dist/services/skills/skill-statusline-service.js +6 -3
  47. package/package.json +5 -5
  48. package/skills/peaks-code/SKILL.md +9 -9
  49. package/skills/peaks-code/references/mode-selection.md +7 -3
  50. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  51. package/skills/peaks-code/references/runbook.md +10 -5
  52. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  53. package/skills/peaks-code/references/standards-preflight.md +1 -1
  54. package/skills/peaks-code/references/startup-sequence.md +1 -1
  55. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  56. package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
  57. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.34 — 2026-09-09 (mode 模型收敛 + auto-compact 死命令 + 上下文窗口覆盖 + 确认门去 TTY)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **mode 模型收敛(用户设计)** — 此前 `SkillPresenceMode` 与 24h 状态机是两个互不相识的自主性开关。
8
+
9
+ - `SkillPresenceMode = full-auto | assisted | strict | 24h`(四选一,并列)。
10
+ - `swarm` 从"模式"降级为**所有 mode 的默认执行策略**(与"fan-out 强制、不可退回串行"一致)。
11
+ - 24h 的 6 状态机保留为 **24h mode 的内部状态**;24h 是**唯一可自动开启**的 mode(T1–T5),其余 mode 仍需用户选。
12
+ - 新增 `peaks code mode status`:一条命令输出 presence mode / 24h 状态 / job mode / auto-compact profile。
13
+ - **顺带修好一个严重 bug**:`getSkillPresence()` 一直在丢掉 `mode` 字段,导致所有读 `presence.mode` 的守卫(mode-enforcement、assisted/strict 边界、post-compact-detector)**全是死代码**——用户选的 mode 从未真正生效。
14
+
15
+ 2. **auto-compact 契约指向不存在的命令(根因)** — `peaks compact auto --execute` 在 `skills/` + `src/` 出现 **49 次**(含 CLI 自己返回的 `next`),实际执行是 `COMMAND_NOT_FOUND`。真命令是 `peaks code auto-compact`。LLM 按契约执行 → 撞死命令 → 只能回头让用户手动 compact。全部引用已改指真命令,并加守卫测试防回归。
16
+
17
+ 3. **上下文窗口不再对未知模型默认 20w** — 新增 `PEAKS_CONTEXT_WINDOW_TOKENS`(env)与 `context.windowTokens`(config)覆盖,优先级 env > config > 启发式 > 默认;`peaks code context-now` 新增 `capacitySource` 字段,一眼看出窗口值来自哪。
18
+
19
+ 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`)完全不变。
20
+
21
+ **验证**:build clean、tsc clean、全量 126 files / 1102 passed(1 skipped);实测 `capacitySource` 默认 `model-heuristic`、`PEAKS_CONTEXT_WINDOW_TOKENS=400000` → `env-override`。
22
+
3
23
  ## 4.0.33 — 2026-09-09 (ECC 去插件化 + 动态获取修复 + 移除 understand-anything)
4
24
 
5
25
  **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);
@@ -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
  }
@@ -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);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Slice 2026-09-09-mode-consolidation (Slice B): auto-engage may set the
3
+ * 24h MODE — and ONLY the 24h mode.
4
+ *
5
+ * `24h` is the single mode that may be set without an explicit user pick
6
+ * (the T1–T5 triggers, and `peaks code run --24h`). Every other mode
7
+ * still requires the Step-1 hard-pause AskUserQuestion
8
+ * (`mode-gate.shouldPauseAtGate` keeps `step-1-mode-select` in
9
+ * `HARD_PAUSE_STEPS`).
10
+ *
11
+ * Before this slice the auto-engage paths wrote only
12
+ * `.peaks/_runtime/<sid>/24h-state.json`; the presence lease kept
13
+ * whatever mode it had (or none), so `24h` was invisible to the mode
14
+ * gate and `strict` + `24H_ACTIVE` still paused every gate. This module
15
+ * is the single place that closes that gap, and the single guard that
16
+ * refuses to stamp any other mode.
17
+ */
18
+ export declare const AUTO_ENGAGE_PRESENCE_MODE: "24h";
19
+ export type AutoEngagePresenceResult = {
20
+ applied: true;
21
+ mode: typeof AUTO_ENGAGE_PRESENCE_MODE;
22
+ updated: number;
23
+ } | {
24
+ applied: false;
25
+ mode: string;
26
+ reason: 'mode-not-auto-settable' | 'no-in-flight-lease' | 'stamp-failed';
27
+ };
28
+ /**
29
+ * Stamp the 24h mode onto every in-flight presence lease of the bound
30
+ * session. Fail-soft: never throws — a missing lease / unreadable store
31
+ * degrades to `{ applied: false }` so the auto-engage caller can still
32
+ * report the 24h-state transition.
33
+ */
34
+ export declare function applyAutoEngagePresenceMode(input: {
35
+ projectRoot: string;
36
+ sessionId: string;
37
+ /** Defaults to `24h`; any other value is refused. */
38
+ mode?: string;
39
+ }): AutoEngagePresenceResult;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Slice 2026-09-09-mode-consolidation (Slice B): auto-engage may set the
3
+ * 24h MODE — and ONLY the 24h mode.
4
+ *
5
+ * `24h` is the single mode that may be set without an explicit user pick
6
+ * (the T1–T5 triggers, and `peaks code run --24h`). Every other mode
7
+ * still requires the Step-1 hard-pause AskUserQuestion
8
+ * (`mode-gate.shouldPauseAtGate` keeps `step-1-mode-select` in
9
+ * `HARD_PAUSE_STEPS`).
10
+ *
11
+ * Before this slice the auto-engage paths wrote only
12
+ * `.peaks/_runtime/<sid>/24h-state.json`; the presence lease kept
13
+ * whatever mode it had (or none), so `24h` was invisible to the mode
14
+ * gate and `strict` + `24H_ACTIVE` still paused every gate. This module
15
+ * is the single place that closes that gap, and the single guard that
16
+ * refuses to stamp any other mode.
17
+ */
18
+ import { stampPresenceLeaseMode } from '../skills/presence-lease-service.js';
19
+ export const AUTO_ENGAGE_PRESENCE_MODE = '24h';
20
+ /**
21
+ * Stamp the 24h mode onto every in-flight presence lease of the bound
22
+ * session. Fail-soft: never throws — a missing lease / unreadable store
23
+ * degrades to `{ applied: false }` so the auto-engage caller can still
24
+ * report the 24h-state transition.
25
+ */
26
+ export function applyAutoEngagePresenceMode(input) {
27
+ const mode = input.mode ?? AUTO_ENGAGE_PRESENCE_MODE;
28
+ if (mode !== AUTO_ENGAGE_PRESENCE_MODE) {
29
+ return { applied: false, mode, reason: 'mode-not-auto-settable' };
30
+ }
31
+ let updated;
32
+ try {
33
+ updated = stampPresenceLeaseMode({
34
+ projectRoot: input.projectRoot,
35
+ sessionId: input.sessionId,
36
+ mode: AUTO_ENGAGE_PRESENCE_MODE
37
+ }).updated;
38
+ }
39
+ catch {
40
+ return { applied: false, mode, reason: 'stamp-failed' };
41
+ }
42
+ if (updated === 0) {
43
+ return { applied: false, mode, reason: 'no-in-flight-lease' };
44
+ }
45
+ return { applied: true, mode: AUTO_ENGAGE_PRESENCE_MODE, updated };
46
+ }
@@ -95,9 +95,15 @@ export type HandoffExitCondition = (typeof HANDOFF_EXIT_CONDITIONS)[number];
95
95
  export declare function isHandoffExitCondition(s: string): s is HandoffExitCondition;
96
96
  export declare function isHandoffState(s: State): boolean;
97
97
  /**
98
- * Slice 2026-07-28 (rid-027): resolve the auto-compact mode from
99
- * 24h-mode awareness. Returns `'partial'` when the session is
100
- * `24H_ACTIVE`, otherwise `'standard'`. This is the CLI-side default;
101
- * the explicit `--mode` flag in `peaks compact auto` overrides it.
98
+ * Slice 2026-07-28 (rid-027), re-keyed by slice
99
+ * 2026-09-09-mode-consolidation: resolve the auto-compact mode from
100
+ * the PRESENCE MODE, not the 24h state machine. Returns `'partial'`
101
+ * when the mode is `24h`, otherwise `'standard'`. This is the CLI-side
102
+ * default; the explicit `--mode` flag in `peaks code auto-compact`
103
+ * overrides it.
104
+ *
105
+ * `sessionId` is retained in the signature for callers that still pass
106
+ * it; the resolution itself is project-scoped (the presence lease is
107
+ * per project + caller).
102
108
  */
103
- export declare function getAutoCompactMode(projectRoot: string, sessionId: string): AutoCompactMode;
109
+ export declare function getAutoCompactMode(projectRoot: string, _sessionId?: string): AutoCompactMode;
@@ -11,7 +11,7 @@
11
11
  * WAITING_USER — proposal §1 T3/T4 exception).
12
12
  */
13
13
  import { B3_THRESHOLD, isDecisionKey } from './state.js';
14
- import { read24hState } from './store.js';
14
+ import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
15
15
  export const B3_TRIGGER_REASONS = [
16
16
  'prd_direction_change',
17
17
  'blocker_3_consecutive_slices',
@@ -128,17 +128,17 @@ export function isHandoffState(s) {
128
128
  return s === 'HANDOFF';
129
129
  }
130
130
  /**
131
- * Slice 2026-07-28 (rid-027): resolve the auto-compact mode from
132
- * 24h-mode awareness. Returns `'partial'` when the session is
133
- * `24H_ACTIVE`, otherwise `'standard'`. This is the CLI-side default;
134
- * the explicit `--mode` flag in `peaks compact auto` overrides it.
131
+ * Slice 2026-07-28 (rid-027), re-keyed by slice
132
+ * 2026-09-09-mode-consolidation: resolve the auto-compact mode from
133
+ * the PRESENCE MODE, not the 24h state machine. Returns `'partial'`
134
+ * when the mode is `24h`, otherwise `'standard'`. This is the CLI-side
135
+ * default; the explicit `--mode` flag in `peaks code auto-compact`
136
+ * overrides it.
137
+ *
138
+ * `sessionId` is retained in the signature for callers that still pass
139
+ * it; the resolution itself is project-scoped (the presence lease is
140
+ * per project + caller).
135
141
  */
136
- export function getAutoCompactMode(projectRoot, sessionId) {
137
- try {
138
- const snap = read24hState(projectRoot, sessionId);
139
- return snap.state === '24H_ACTIVE' ? 'partial' : 'standard';
140
- }
141
- catch {
142
- return 'standard';
143
- }
142
+ export function getAutoCompactMode(projectRoot, _sessionId) {
143
+ return resolveAutoCompactProfile(projectRoot);
144
144
  }
@@ -30,7 +30,9 @@ export interface ActiveSkillResolution {
30
30
  readonly sessionId: string | null;
31
31
  /**
32
32
  * Mode token recorded on the canonical lease (e.g. `full-auto`,
33
- * `assisted`, `swarm`, `strict`). `null` when the source is not
33
+ * `assisted`, `strict`, `24h`). Normalized on read: a legacy
34
+ * on-disk `'swarm'` surfaces as `'full-auto'` (slice
35
+ * 2026-09-09-mode-consolidation). `null` when the source is not
34
36
  * `canonical` (the legacy `active-skill-*.json` files do not
35
37
  * surface a mode field, and the `env` / `none` cases are test
36
38
  * overrides). Slice 2026-08-04-rid-005 surfaced this so the
@@ -29,7 +29,7 @@ import { join } from 'node:path';
29
29
  import { getSessionIdCanonical } from '../../session/session-manager.js';
30
30
  import { getSessionDir } from '../../session/getSessionDir.js';
31
31
  import { readPresenceLease, listPresenceLeases } from '../../skills/presence-lease-service.js';
32
- import { getCurrentSessionId } from '../../skills/skill-presence-service.js';
32
+ import { getCurrentSessionId, normalizeSkillPresenceMode } from '../../skills/skill-presence-service.js';
33
33
  const ACTIVE_SKILL_PREFIX = 'active-skill-';
34
34
  /**
35
35
  * Resolve the active peak skill for the current hook invocation.
@@ -99,7 +99,7 @@ export function resolveActiveSkillForCaller(projectRoot, opts) {
99
99
  skill: projection.lease.skill,
100
100
  callerId: lease.callerId,
101
101
  sessionId,
102
- mode: typeof projection.mode === 'string' && projection.mode.length > 0 ? projection.mode : null,
102
+ mode: normalizeSkillPresenceMode(typeof projection.mode === 'string' ? projection.mode : null) ?? null,
103
103
  source: 'canonical',
104
104
  };
105
105
  }
@@ -127,9 +127,7 @@ export function resolveActiveSkillForCaller(projectRoot, opts) {
127
127
  const raw = readFileSync(filePath, 'utf8');
128
128
  const parsed = JSON.parse(raw);
129
129
  if (typeof parsed.skill === 'string' && parsed.skill.length > 0) {
130
- const legacyMode = typeof parsed.mode === 'string' && parsed.mode.length > 0
131
- ? parsed.mode
132
- : null;
130
+ const legacyMode = normalizeSkillPresenceMode(typeof parsed.mode === 'string' ? parsed.mode : null) ?? null;
133
131
  return { skill: parsed.skill, callerId, sessionId, mode: legacyMode, source: 'file' };
134
132
  }
135
133
  }
@@ -76,7 +76,7 @@ export declare function summarizeLifecycleError(error: unknown): string;
76
76
  *
77
77
  * This is the integration with the actual post-compact detection path.
78
78
  * The claude-code adapter's `postCompactDetectCommand` is
79
- * `peaks compact auto --json` — i.e. the next probe through this very
79
+ * `peaks code auto-compact --json` — i.e. the next probe through this very
80
80
  * function. So when a probe finds a run still sitting at `compacting`
81
81
  * and MEASURES a ratio that has dropped below the auto-fire threshold,
82
82
  * that measurement is the proof the compact landed. Only then do we
@@ -168,7 +168,7 @@ export function summarizeLifecycleError(error) {
168
168
  *
169
169
  * This is the integration with the actual post-compact detection path.
170
170
  * The claude-code adapter's `postCompactDetectCommand` is
171
- * `peaks compact auto --json` — i.e. the next probe through this very
171
+ * `peaks code auto-compact --json` — i.e. the next probe through this very
172
172
  * function. So when a probe finds a run still sitting at `compacting`
173
173
  * and MEASURES a ratio that has dropped below the auto-fire threshold,
174
174
  * that measurement is the proof the compact landed. Only then do we
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Slice 2026-07-29-context-evaluation-accuracy Part 22: a new
10
10
  * `autoFire` tier is added between soft-warn and preCompact. peaks-loop
11
- * preempts and runs `peaks compact auto --execute` itself when
11
+ * preempts and runs `peaks code auto-compact` itself when
12
12
  * context crosses `autoFire`; the LLM is NOT asked to "decide"
13
13
  * (the LLM-judged 0.85–0.95 zone was the source of false-positive
14
14
  * "context too low" reports at 60%+ free — the LLM was guessing).
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Slice 2026-07-29-context-evaluation-accuracy Part 22: a new
10
10
  * `autoFire` tier is added between soft-warn and preCompact. peaks-loop
11
- * preempts and runs `peaks compact auto --execute` itself when
11
+ * preempts and runs `peaks code auto-compact` itself when
12
12
  * context crosses `autoFire`; the LLM is NOT asked to "decide"
13
13
  * (the LLM-judged 0.85–0.95 zone was the source of false-positive
14
14
  * "context too low" reports at 60%+ free — the LLM was guessing).
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-compact orchestrator (v2.13.0 AC-2 + AC-3 + AC-4).
3
3
  *
4
- * Closes the loop between `peaks compact auto` (AC-1) and the IDE's
4
+ * Closes the loop between `peaks code auto-compact` (AC-1) and the IDE's
5
5
  * native compact capability (AC-3). peaks-loop is project-aware: it
6
6
  * knows the current plan, open questions, recent decisions, in-flight
7
7
  * batches, todo state, git status, and active skills. That context is
@@ -16,7 +16,7 @@
16
16
  * 3. If 0.85 ≤ ratio < 0.95 (pre-compact zone): peaks-loop prepares
17
17
  * the convergence toolkit (checkpoint + auto-decisions log +
18
18
  * IDE-dispatch handle) and surfaces it to the LLM. The LLM
19
- * DECIDES when to fire `peaks compact auto --execute`;
19
+ * DECIDES when to fire `peaks code auto-compact`;
20
20
  * peaks-loop does NOT auto-fire. The toolkit is ready so the
21
21
  * LLM doesn't lose context to a last-second `/compact` panic.
22
22
  * 4. If ratio < 0.85: skip — return a one-line info row.
@@ -34,7 +34,7 @@ import { getSessionIdCanonical } from '../session/session-manager.js';
34
34
  import { resolveOuterSessionId } from '../session/binding-status-service.js';
35
35
  import { AUTO_COMPACT_PRE_COMPACT_RATIO, AUTO_COMPACT_THRESHOLD_RATIO } from '../context/auto-compact-types.js';
36
36
  import { describeMode, thresholdFor } from './auto-compact-modes.js';
37
- import { read24hState } from '../24h-mode/store.js';
37
+ import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
38
38
  import { CompactLifecyclePublisher, newCompactRunId, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
39
39
  const PRE_COMPACT_REASON = 'pre-compact-auto';
40
40
  /**
@@ -72,7 +72,7 @@ export function evaluateCompactTrigger(ratio, mode = 'standard') {
72
72
  }
73
73
  if (ratio < preCompact) {
74
74
  // Part 22: auto-fire zone (0.80 ≤ ratio < 0.85). peaks-loop
75
- // preempts and runs `peaks compact auto --execute` itself
75
+ // preempts and runs `peaks code auto-compact` itself
76
76
  // without LLM involvement. The LLM is not asked to "decide";
77
77
  // the toolkit is applied synchronously. Closes the
78
78
  // LLM-misjudges-context window that previously let the
@@ -255,20 +255,15 @@ function writePreCompactCheckpoint(input) {
255
255
  return path;
256
256
  }
257
257
  /**
258
- * Slice 2026-07-28 (rid-027): resolve the auto-compact mode from
259
- * 24h-mode awareness. Returns `'partial'` when the session is
260
- * `24H_ACTIVE`, otherwise `'standard'`. The CLI flag `--mode` takes
261
- * precedence (caller passes `input.mode` directly), so this helper is
262
- * only consulted when the flag is absent.
258
+ * Slice 2026-07-28 (rid-027), re-keyed by slice
259
+ * 2026-09-09-mode-consolidation: resolve the auto-compact mode from
260
+ * the PRESENCE MODE, not the 24h state machine. Returns `'partial'`
261
+ * when the mode is `24h`, otherwise `'standard'`. The CLI flag
262
+ * `--mode` takes precedence (caller passes `input.mode` directly), so
263
+ * this helper is only consulted when the flag is absent.
263
264
  */
264
- function resolveAutoCompactMode(projectRoot, sessionId) {
265
- try {
266
- const snap = read24hState(projectRoot, sessionId);
267
- return snap.state === '24H_ACTIVE' ? 'partial' : 'standard';
268
- }
269
- catch {
270
- return 'standard';
271
- }
265
+ function resolveAutoCompactMode(projectRoot) {
266
+ return resolveAutoCompactProfile(projectRoot);
272
267
  }
273
268
  /**
274
269
  * Execute the auto-compact flow.
@@ -300,9 +295,10 @@ export async function runAutoCompact(input) {
300
295
  };
301
296
  }
302
297
  // Slice 2026-07-28 (rid-027): resolve mode. CLI flag `--mode` wins;
303
- // 24h-mode awareness yields 'partial' when 24h_ACTIVE; default
304
- // 'standard' preserves v2.13.0 zero-pause contract.
305
- const mode = input.mode ?? resolveAutoCompactMode(input.projectRoot, sessionId);
298
+ // slice 2026-09-09-mode-consolidation keys the default off the
299
+ // presence MODE ('24h' → 'partial'); 'standard' preserves the
300
+ // v2.13.0 zero-pause contract.
301
+ const mode = input.mode ?? resolveAutoCompactMode(input.projectRoot);
306
302
  // Lazy import to avoid the AC-1 module depending on the orchestrator.
307
303
  const { readContextPercent } = await import('../context/auto-compact-reader.js');
308
304
  const outerSessionId = resolveOuterSessionId(input.projectRoot, sessionId, input.env ?? process.env);
@@ -398,12 +394,12 @@ export async function runAutoCompact(input) {
398
394
  ? [
399
395
  'RED-LINE compact dispatched — further sub-agent dispatch BLOCKED until ratio < 0.85',
400
396
  'Post-compact resume picks up the convergence plan from auto-decisions.md',
401
- 'Next `peaks compact auto` probe will confirm ratio dropped below 0.85'
397
+ 'Next `peaks code auto-compact` probe will confirm ratio dropped below 0.85'
402
398
  ]
403
399
  : [
404
400
  'Pre-compact dispatched — IDE compact in progress (async)',
405
401
  'Post-compact resume picks up the convergence plan from auto-decisions.md',
406
- 'Next `peaks compact auto` probe will confirm ratio dropped below 0.85'
402
+ 'Next `peaks code auto-compact` probe will confirm ratio dropped below 0.85'
407
403
  ];
408
404
  plan = buildConvergencePlan({
409
405
  sessionId,