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
@@ -1,4 +1,3 @@
1
- import * as readline from 'node:readline';
2
1
  import { getSkillPresence } from '../skills/skill-presence-service.js';
3
2
  const ASSISTED_CONFIRM_TRANSITIONS = new Set([
4
3
  'prd:confirmed-by-user',
@@ -6,7 +5,9 @@ const ASSISTED_CONFIRM_TRANSITIONS = new Set([
6
5
  'qa:verdict-issued'
7
6
  ]);
8
7
  export function requiresConfirmation(mode, transitionKey) {
9
- if (mode === 'full-auto' || mode === 'swarm') {
8
+ // Slice 2026-09-09-mode-consolidation: `swarm` removed as a mode; the
9
+ // two auto-proceed peers are `full-auto` and `24h`.
10
+ if (mode === 'full-auto' || mode === '24h') {
10
11
  return false;
11
12
  }
12
13
  if (mode === 'strict') {
@@ -22,12 +23,23 @@ function describeTransition(transitionKey) {
22
23
  return `Transition ${role.toUpperCase()} → ${state}`;
23
24
  }
24
25
  export class ConfirmationRequiredError extends Error {
25
- constructor(transitionKey) {
26
+ transitionKey;
27
+ mode;
28
+ nextActions;
29
+ constructor(transitionKey, mode) {
26
30
  const description = describeTransition(transitionKey);
27
- super(`Confirmation required for: ${description}\n` +
28
- 'Add --confirm to proceed non-interactively, or run in an interactive terminal.\n' +
29
- 'In assisted/strict mode, major workflow boundaries require explicit user approval.');
31
+ const nextActions = [
32
+ `Ask the user via AskUserQuestion whether to proceed with ${description} in ${mode} mode.`,
33
+ 'If the user approves, re-run the same command with --confirm.'
34
+ ];
35
+ super(`Confirmation required for: ${description} (mode: ${mode})\n` +
36
+ `${nextActions[0]}\n` +
37
+ `${nextActions[1]}\n` +
38
+ 'No terminal prompt is available: this gate never reads stdin.');
30
39
  this.name = 'ConfirmationRequiredError';
40
+ this.transitionKey = transitionKey;
41
+ this.mode = mode;
42
+ this.nextActions = nextActions;
31
43
  }
32
44
  }
33
45
  export async function requireUserConfirmation(options) {
@@ -40,11 +52,11 @@ export async function requireUserConfirmation(options) {
40
52
  if (!requiresConfirmation(mode, options.transitionKey)) {
41
53
  return;
42
54
  }
43
- // --confirm flag bypasses interactive prompt
55
+ // --confirm flag bypasses the gate
44
56
  if (options.confirmed) {
45
57
  return;
46
58
  }
47
- // PEAKS_AUTO_CONFIRM=1 only works for full-auto/swarm (already returned above)
59
+ // PEAKS_AUTO_CONFIRM=1 only works for full-auto/24h (already returned above)
48
60
  // For assisted/strict, env var is ignored unless --force-confirm is also set
49
61
  if (process.env.PEAKS_AUTO_CONFIRM === '1') {
50
62
  if (options.forceConfirm) {
@@ -53,7 +65,7 @@ export async function requireUserConfirmation(options) {
53
65
  'This bypasses user confirmation. Use with caution.');
54
66
  return;
55
67
  }
56
- throw new ConfirmationRequiredError(options.transitionKey);
68
+ throw new ConfirmationRequiredError(options.transitionKey, mode);
57
69
  }
58
70
  // --force-confirm without env var
59
71
  if (options.forceConfirm) {
@@ -62,23 +74,9 @@ export async function requireUserConfirmation(options) {
62
74
  'This bypasses user confirmation. Use with caution.');
63
75
  return;
64
76
  }
65
- // Interactive prompt
66
- const rl = readline.createInterface({
67
- input: process.stdin,
68
- output: process.stderr
69
- });
70
- return new Promise((resolve, reject) => {
71
- const description = describeTransition(options.transitionKey);
72
- const prompt = `\n[CONFIRM] ${description}\nProceed? (y/N) `;
73
- rl.question(prompt, (answer) => {
74
- rl.close();
75
- const normalized = answer.trim().toLowerCase();
76
- if (normalized === 'y' || normalized === 'yes') {
77
- resolve();
78
- }
79
- else {
80
- reject(new ConfirmationRequiredError(options.transitionKey));
81
- }
82
- });
83
- });
77
+ // No bypass flag: refuse immediately. Never read stdin — in an LLM-driven
78
+ // session there is no TTY, so a prompt would hang forever, and a `y/N`
79
+ // terminal prompt would also violate the Human-NL-Choice-Only rule (the
80
+ // user answers via AskUserQuestion, never by typing into a shell).
81
+ throw new ConfirmationRequiredError(options.transitionKey, mode);
84
82
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Slice 2026-09-09-mode-consolidation (Slice D): one read for the whole
3
+ * stacked mode picture.
4
+ *
5
+ * The autonomy model has two layers that used to be read by nothing in
6
+ * common:
7
+ *
8
+ * - MODE — `SkillPresenceMode` on the presence lease: the autonomy
9
+ * level (`full-auto | assisted | strict | 24h`).
10
+ * - STATE — `.peaks/_runtime/<sid>/24h-state.json`, the 6-state
11
+ * machine (`IDLE / BRAINSTORM / USER_CONFIRM / 24H_ACTIVE /
12
+ * WAITING_USER / HANDOFF`): the long-run PHASE *inside* the `24h`
13
+ * mode.
14
+ *
15
+ * This module is the single place that reads both, plus the Job-mode
16
+ * decision and the resolved auto-compact profile, so `peaks code mode
17
+ * status` (and any future consumer) does not re-derive the stack.
18
+ *
19
+ * Read-only + fail-soft: every sub-read is wrapped so a missing or
20
+ * malformed artifact degrades to `null` / `false` rather than throwing.
21
+ */
22
+ import { type SkillPresenceMode } from '../skills/skill-presence-service.js';
23
+ import type { State24hSnapshot } from '../24h-mode/state.js';
24
+ import { type AutoCompactMode } from '../code/auto-compact-modes.js';
25
+ export interface ResolvedPresenceMode {
26
+ readonly mode: SkillPresenceMode | null;
27
+ readonly skill: string | null;
28
+ readonly source: 'canonical' | 'file' | 'env' | 'none';
29
+ }
30
+ export interface ModeStatus {
31
+ /** Autonomy level from the presence lease; `null` when no lease / no mode recorded. */
32
+ readonly mode: SkillPresenceMode | null;
33
+ /** Active skill name from the same lease read (display aid). */
34
+ readonly skill: string | null;
35
+ /** Where the mode came from. */
36
+ readonly modeSource: 'canonical' | 'file' | 'env' | 'none';
37
+ /** `true` when the resolved mode is `24h`. */
38
+ readonly is24h: boolean;
39
+ /**
40
+ * The 24h internal state machine snapshot. Populated only when
41
+ * `is24h` is true (the phase is meaningless outside the 24h mode);
42
+ * `null` when the mode is not `24h` or the snapshot is unreadable.
43
+ */
44
+ readonly h24State: State24hSnapshot | null;
45
+ /** Job-mode flag from `job-shape.json` (`false` when undecided / unreadable). */
46
+ readonly jobMode: boolean;
47
+ /** Resolved auto-compact profile: `partial` iff the mode is `24h`. */
48
+ readonly autoCompactProfile: AutoCompactMode;
49
+ /** Thresholds backing `autoCompactProfile` (display aid). */
50
+ readonly autoCompactThresholds: {
51
+ readonly autoFire: number;
52
+ readonly preCompact: number;
53
+ readonly redLine: number;
54
+ };
55
+ }
56
+ /**
57
+ * Read the active presence mode via the canonical lease projection.
58
+ * Never throws.
59
+ */
60
+ export declare function resolvePresenceMode(projectRoot: string): ResolvedPresenceMode;
61
+ /**
62
+ * Resolve the auto-compact profile from the MODE, not from the 24h
63
+ * state machine. `24h` mode → `partial` (earlier thresholds for
64
+ * long-run sessions); every other mode → `standard`.
65
+ *
66
+ * Slice 2026-09-09-mode-consolidation: this replaced the previous
67
+ * `24h-state.state === '24H_ACTIVE'` mapping. The state machine is the
68
+ * long-run *phase*; the mode is the autonomy level the user (or the
69
+ * T3/T4 auto-engage) selected. Keying off the mode means a `24h`
70
+ * session keeps the partial profile even while it is in
71
+ * `WAITING_USER`, which is the correct long-run behaviour.
72
+ */
73
+ export declare function resolveAutoCompactProfile(projectRoot: string): AutoCompactMode;
74
+ /**
75
+ * Assemble the full stacked status. `sessionId` defaults to the
76
+ * canonical binding when omitted.
77
+ */
78
+ export declare function resolveModeStatus(input: {
79
+ projectRoot: string;
80
+ sessionId?: string | null | undefined;
81
+ }): ModeStatus;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Slice 2026-09-09-mode-consolidation (Slice D): one read for the whole
3
+ * stacked mode picture.
4
+ *
5
+ * The autonomy model has two layers that used to be read by nothing in
6
+ * common:
7
+ *
8
+ * - MODE — `SkillPresenceMode` on the presence lease: the autonomy
9
+ * level (`full-auto | assisted | strict | 24h`).
10
+ * - STATE — `.peaks/_runtime/<sid>/24h-state.json`, the 6-state
11
+ * machine (`IDLE / BRAINSTORM / USER_CONFIRM / 24H_ACTIVE /
12
+ * WAITING_USER / HANDOFF`): the long-run PHASE *inside* the `24h`
13
+ * mode.
14
+ *
15
+ * This module is the single place that reads both, plus the Job-mode
16
+ * decision and the resolved auto-compact profile, so `peaks code mode
17
+ * status` (and any future consumer) does not re-derive the stack.
18
+ *
19
+ * Read-only + fail-soft: every sub-read is wrapped so a missing or
20
+ * malformed artifact degrades to `null` / `false` rather than throwing.
21
+ */
22
+ import { resolveActiveSkillForCaller } from '../audit/enforcers/active-skill-resolver.js';
23
+ import { getCurrentSessionId } from '../skills/skill-presence-service.js';
24
+ // Import the 24h store directly (NOT the `24h-mode/index.js` barrel) —
25
+ // the barrel re-exports `decider.ts`, which imports this module, and
26
+ // that would form an import cycle.
27
+ import { read24hState } from '../24h-mode/store.js';
28
+ import { readJobShapeDecision } from '../code/job-shape-decision.js';
29
+ import { AUTO_COMPACT_THRESHOLDS } from '../code/auto-compact-modes.js';
30
+ /**
31
+ * Read the active presence mode via the canonical lease projection.
32
+ * Never throws.
33
+ */
34
+ export function resolvePresenceMode(projectRoot) {
35
+ try {
36
+ const resolved = resolveActiveSkillForCaller(projectRoot);
37
+ const mode = resolved.mode;
38
+ return {
39
+ mode: mode === null ? null : mode,
40
+ skill: resolved.skill,
41
+ source: resolved.source
42
+ };
43
+ }
44
+ catch {
45
+ return { mode: null, skill: null, source: 'none' };
46
+ }
47
+ }
48
+ /**
49
+ * Resolve the auto-compact profile from the MODE, not from the 24h
50
+ * state machine. `24h` mode → `partial` (earlier thresholds for
51
+ * long-run sessions); every other mode → `standard`.
52
+ *
53
+ * Slice 2026-09-09-mode-consolidation: this replaced the previous
54
+ * `24h-state.state === '24H_ACTIVE'` mapping. The state machine is the
55
+ * long-run *phase*; the mode is the autonomy level the user (or the
56
+ * T3/T4 auto-engage) selected. Keying off the mode means a `24h`
57
+ * session keeps the partial profile even while it is in
58
+ * `WAITING_USER`, which is the correct long-run behaviour.
59
+ */
60
+ export function resolveAutoCompactProfile(projectRoot) {
61
+ return resolvePresenceMode(projectRoot).mode === '24h' ? 'partial' : 'standard';
62
+ }
63
+ /**
64
+ * Assemble the full stacked status. `sessionId` defaults to the
65
+ * canonical binding when omitted.
66
+ */
67
+ export function resolveModeStatus(input) {
68
+ const { projectRoot } = input;
69
+ const presence = resolvePresenceMode(projectRoot);
70
+ const is24h = presence.mode === '24h';
71
+ let sessionId = input.sessionId ?? null;
72
+ if (sessionId === null) {
73
+ try {
74
+ sessionId = getCurrentSessionId(projectRoot);
75
+ }
76
+ catch {
77
+ sessionId = null;
78
+ }
79
+ }
80
+ let h24State = null;
81
+ if (is24h && sessionId !== null) {
82
+ try {
83
+ h24State = read24hState(projectRoot, sessionId);
84
+ }
85
+ catch {
86
+ h24State = null;
87
+ }
88
+ }
89
+ let jobMode = false;
90
+ if (sessionId !== null) {
91
+ try {
92
+ jobMode = readJobShapeDecision(projectRoot, sessionId).decision.isJob;
93
+ }
94
+ catch {
95
+ jobMode = false;
96
+ }
97
+ }
98
+ return {
99
+ mode: presence.mode,
100
+ skill: presence.skill,
101
+ modeSource: presence.source,
102
+ is24h,
103
+ h24State,
104
+ jobMode,
105
+ autoCompactProfile: is24h ? 'partial' : 'standard',
106
+ autoCompactThresholds: AUTO_COMPACT_THRESHOLDS[is24h ? 'partial' : 'standard']
107
+ };
108
+ }
@@ -66,6 +66,27 @@ export interface GcPresenceLeasesInput {
66
66
  readonly trigger?: 'manual' | 'workspace-init' | 'presence-set';
67
67
  }
68
68
  export declare function setPresenceLease(input: SetPresenceLeaseInput): SetPresenceLeaseResult;
69
+ /**
70
+ * Slice 2026-09-09-mode-consolidation (Slice B): stamp `mode` onto every
71
+ * in-flight lease of the bound session, in place.
72
+ *
73
+ * Used by the 24h auto-engage path (T3/T4, `peaks code run --24h`,
74
+ * `peaks session 24h-mode transition --state 24H_ACTIVE`) to record the
75
+ * autonomy level without re-creating the lease (a fresh `setPresenceLease`
76
+ * would reset `startedAt` / `status`). `lastHeartbeat` is intentionally
77
+ * NOT touched — stamping a mode is not a liveness signal.
78
+ *
79
+ * Returns the number of leases updated; 0 when there is nothing in flight.
80
+ * Callers are responsible for the "only `24h` may be auto-set" guard.
81
+ */
82
+ export declare function stampPresenceLeaseMode(input: {
83
+ readonly projectRoot: string;
84
+ readonly sessionId: string;
85
+ readonly mode: string;
86
+ }): {
87
+ updated: number;
88
+ leasePaths: string[];
89
+ };
69
90
  export declare function readPresenceLease(input: ReadPresenceLeaseInput): PresenceProjection;
70
91
  export declare function markPresenceLost(input: MarkPresenceLostInput): SkillPresenceLease;
71
92
  export declare function gcStalePresenceLeases(input: GcPresenceLeasesInput): GcResult;
@@ -175,6 +175,32 @@ export function setPresenceLease(input) {
175
175
  gc,
176
176
  };
177
177
  }
178
+ /**
179
+ * Slice 2026-09-09-mode-consolidation (Slice B): stamp `mode` onto every
180
+ * in-flight lease of the bound session, in place.
181
+ *
182
+ * Used by the 24h auto-engage path (T3/T4, `peaks code run --24h`,
183
+ * `peaks session 24h-mode transition --state 24H_ACTIVE`) to record the
184
+ * autonomy level without re-creating the lease (a fresh `setPresenceLease`
185
+ * would reset `startedAt` / `status`). `lastHeartbeat` is intentionally
186
+ * NOT touched — stamping a mode is not a liveness signal.
187
+ *
188
+ * Returns the number of leases updated; 0 when there is nothing in flight.
189
+ * Callers are responsible for the "only `24h` may be auto-set" guard.
190
+ */
191
+ export function stampPresenceLeaseMode(input) {
192
+ const sessionId = validateSessionId(input.sessionId);
193
+ const leasePaths = [];
194
+ for (const lease of listPresenceLeases(input.projectRoot, sessionId)) {
195
+ if (lease.status !== 'preparing' && lease.status !== 'running')
196
+ continue;
197
+ const path = leaseFilePath(input.projectRoot, sessionId, lease.callerId, lease.workflowId);
198
+ const next = { ...lease, mode: input.mode };
199
+ writeAtomic(path, JSON.stringify(next, null, 2));
200
+ leasePaths.push(path);
201
+ }
202
+ return { updated: leasePaths.length, leasePaths };
203
+ }
178
204
  export function readPresenceLease(input) {
179
205
  const sessionId = validateSessionId(input.sessionId ?? null);
180
206
  const callerId = validateCallerId(input.callerId);
@@ -1,6 +1,15 @@
1
- export type SkillPresenceMode = 'full-auto' | 'assisted' | 'swarm' | 'strict';
1
+ export type SkillPresenceMode = 'full-auto' | 'assisted' | 'strict' | '24h';
2
2
  export declare const VALID_SKILL_PRESENCE_MODES: ReadonlyArray<SkillPresenceMode>;
3
+ export declare const LEGACY_SKILL_PRESENCE_MODE_ALIASES: Readonly<Record<string, SkillPresenceMode>>;
3
4
  export declare function isSkillPresenceMode(value: string): value is SkillPresenceMode;
5
+ /**
6
+ * Normalize a raw on-disk / CLI mode token to a current
7
+ * `SkillPresenceMode`. Current values pass through; the legacy
8
+ * `'swarm'` alias maps to `'full-auto'`; anything else returns
9
+ * `undefined` so the caller can decide (drop the field, not the
10
+ * presence). Never throws.
11
+ */
12
+ export declare function normalizeSkillPresenceMode(value: string | undefined | null): SkillPresenceMode | undefined;
4
13
  export type SkillPresence = {
5
14
  skill: string;
6
15
  mode?: SkillPresenceMode;
@@ -20,12 +20,33 @@ void null;
20
20
  export const VALID_SKILL_PRESENCE_MODES = [
21
21
  'full-auto',
22
22
  'assisted',
23
- 'swarm',
24
- 'strict'
23
+ 'strict',
24
+ '24h'
25
25
  ];
26
+ // Migration note (slice 2026-09-09-mode-consolidation): `swarm` was REMOVED
27
+ // as a mode — parallel fan-out is now the default execution strategy in
28
+ // every mode. A legacy on-disk `mode: 'swarm'` is normalized on read to
29
+ // `'full-auto'` (never crash, never drop the presence).
30
+ export const LEGACY_SKILL_PRESENCE_MODE_ALIASES = {
31
+ swarm: 'full-auto'
32
+ };
26
33
  export function isSkillPresenceMode(value) {
27
34
  return VALID_SKILL_PRESENCE_MODES.includes(value);
28
35
  }
36
+ /**
37
+ * Normalize a raw on-disk / CLI mode token to a current
38
+ * `SkillPresenceMode`. Current values pass through; the legacy
39
+ * `'swarm'` alias maps to `'full-auto'`; anything else returns
40
+ * `undefined` so the caller can decide (drop the field, not the
41
+ * presence). Never throws.
42
+ */
43
+ export function normalizeSkillPresenceMode(value) {
44
+ if (typeof value !== 'string' || value.length === 0)
45
+ return undefined;
46
+ if (isSkillPresenceMode(value))
47
+ return value;
48
+ return LEGACY_SKILL_PRESENCE_MODE_ALIASES[value];
49
+ }
29
50
  /**
30
51
  * The current outer session id, exposed to Bash tool calls via the
31
52
  * `PEAKS_OUTER_SESSION_ID` environment variable. Stamping it onto the
@@ -118,8 +139,15 @@ function readSkillPresenceFromLease(projectRootOverride) {
118
139
  // The lease carries `callerId` (the adapter / harness session id),
119
140
  // not the legacy `outerSessionId` field. Project it for back-compat
120
141
  // with `clearStalePresenceOnRotation`'s outer-mismatch check.
142
+ //
143
+ // Slice 2026-09-09-mode-consolidation: `mode` is projected too (it was
144
+ // silently dropped before, which made `getSkillPresence().mode` always
145
+ // undefined and left every `presence.mode` consumer dead). Normalize on
146
+ // read so a legacy `'swarm'` lease surfaces as `'full-auto'`.
147
+ const leaseMode = normalizeSkillPresenceMode(lease.mode);
121
148
  return {
122
149
  skill: lease.skill,
150
+ ...(leaseMode !== undefined ? { mode: leaseMode } : {}),
123
151
  sessionId,
124
152
  outerSessionId: lease.callerId,
125
153
  setAt: lease.startedAt,
@@ -208,7 +236,7 @@ function getPreviousOuterSessionId(projectRootOverride) {
208
236
  * `peakSessionId`, OR via `ensureSession` for the first-time case).
209
237
  */
210
238
  export function setSkillPresenceForCaller(projectRootOverride, callerId, peakSessionId, skill, mode, gate) {
211
- const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
239
+ const validatedMode = normalizeSkillPresenceMode(mode);
212
240
  const now = new Date().toISOString();
213
241
  const presence = {
214
242
  skill,
@@ -257,7 +285,7 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
257
285
  // the old shape don't break) but we surface a warning to the
258
286
  // CLI boundary via the `outerSessionMismatch` field.
259
287
  // 3. delegates the canonical write to `setPresenceLease`.
260
- const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
288
+ const validatedMode = normalizeSkillPresenceMode(mode);
261
289
  const sessionId = getCurrentSessionId(projectRootOverride);
262
290
  const outerSessionId = getCurrentOuterSessionId();
263
291
  const previousOuterSessionId = getPreviousOuterSessionId(projectRootOverride);
@@ -30,6 +30,7 @@ import { readActiveDispatchIndex } from '../dispatch/dispatch-record-writer.js';
30
30
  * picks the most recent in-flight lease across all callers; this is the
31
31
  * documented back-compat path.
32
32
  */
33
+ import { normalizeSkillPresenceMode } from './skill-presence-service.js';
33
34
  const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
34
35
  /**
35
36
  * Slice rid-statusline-24h-overlay (2026-08-10): name-distinct from
@@ -245,12 +246,14 @@ function readPresenceReadOnly(projectRoot, callerId) {
245
246
  }
246
247
  // Leases on disk may carry an optional `mode` field (the lease constructor
247
248
  // spreads `input.mode` when present); the typed `SkillPresenceLease` does
248
- // not declare it, so widen the read shape here.
249
- const latestMode = latest.mode;
249
+ // not declare it, so widen the read shape here. Normalized on read: a
250
+ // legacy `'swarm'` lease renders as `'full-auto'` (slice
251
+ // 2026-09-09-mode-consolidation).
252
+ const latestMode = normalizeSkillPresenceMode(latest.mode);
250
253
  return {
251
254
  presence: {
252
255
  skill: latest.skill,
253
- ...(typeof latestMode === 'string' && latestMode.length > 0 ? { mode: latestMode } : {}),
256
+ ...(latestMode !== undefined ? { mode: latestMode } : {}),
254
257
  setAt: latest.startedAt,
255
258
  },
256
259
  invalid: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.33",
3
+ "version": "4.0.34",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -101,10 +101,10 @@
101
101
  "fzf": "^0.5.2",
102
102
  "yaml": "^2.9.0",
103
103
  "zod": "^4.4.3",
104
- "peaks-loop-internal-runtime": "0.0.18",
105
- "peaks-loop-shared": "0.0.67",
106
- "peaks-loop-mut": "0.1.31",
107
- "peaks-loop-shared-channel": "0.0.35"
104
+ "peaks-loop-internal-runtime": "0.0.19",
105
+ "peaks-loop-shared": "0.0.68",
106
+ "peaks-loop-shared-channel": "0.0.36",
107
+ "peaks-loop-mut": "0.1.32"
108
108
  },
109
109
  "devDependencies": {
110
110
  "@changesets/cli": "2.31.1",
@@ -104,7 +104,7 @@ peaks sub-agent dispatch rd --prompt '<your task>' --graph-node <nid> --workflow
104
104
  **Integration surfaces:** `peaks code run --24h` / `peaks dashboard long-run --since 24h` / `peaks session 24h-mode state|transition|attempts|reset`.
105
105
 
106
106
  **Red lines (peaks-code side):**
107
- - No auto-compact prose ban: never write "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks compact auto` manually" / "the user is responsible for context management" / legacy 50/75/90 percent tiers. **0.85 / 0.95 contract is mandatory.**
107
+ - No auto-compact prose ban: never write "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks code auto-compact` manually" / "the user is responsible for context management" / legacy 50/75/90 percent tiers. **0.85 / 0.95 contract is mandatory.**
108
108
  - SquabbyZ sole-author rule: no `Co-Authored-By: Claude/Anthropic` trailer.
109
109
  - 24h mode is a flag on `peaks-code`; MUST NOT introduce a sibling `peaks-24h` skill or competing top-level verb.
110
110
 
@@ -151,7 +151,7 @@ Before the first planning action, run `peaks fresh-context preflight --prompt "<
151
151
  1. **PreToolUse hook — `peaks code gate-step-08`.** Installed by `peaks workspace init` on the `Bash` matcher; checks `job-shape.json` presence + fail-closed backup regex. If `job-shape.json` AND `progress.json` exist, surfaces `Next: slice #N of M (<currentSlice>)` so the LLM cannot wake up cold.
152
152
  2. **Size-fear ban — `peaks code emit-handoff`.** Refuses to emit a final handoff while `remaining > 0` under Job mode. Pass `--force-under-job` only with explicit user approval.
153
153
  3. **On-disk slice progress — `peaks job progress`.** `peaks job checkpoint --state done` writes `progress.json`. `peaks job progress --job-id <jid> [--allow-missing]` is the canonical reader.
154
- 4. **Forced auto-compact — `--enforce-job-mode`.** `peaks code context-now --enforce-job-mode` returns `action: 'auto-compact-now'` at ≥ 0.85. **Job mode at ≥ 0.85 is MANDATORY auto-compact** — Code MUST call `peaks compact auto --execute` without confirmation.
154
+ 4. **Forced auto-compact — `--enforce-job-mode`.** `peaks code context-now --enforce-job-mode` returns `action: 'auto-compact-now'` at ≥ 0.85. **Job mode at ≥ 0.85 is MANDATORY auto-compact** — Code MUST call `peaks code auto-compact` without confirmation.
155
155
 
156
156
  **Step 0.7 resume rule (read-FIRST):** on resume, `peaks code gate-step-08` reads `progress.json` first and surfaces `Next: slice #N of M (<currentSlice>)` so the orchestrator picks up at the right slice without re-reading the artifact tree.
157
157
 
@@ -161,26 +161,26 @@ Before the first planning action, run `peaks fresh-context preflight --prompt "<
161
161
 
162
162
  ### Peaks-Loop Step N+2: Auto-compact at the warning line (v2.13.0 zero-pause contract)
163
163
 
164
- > **Zero-pause contract.** When context usage crosses the pre-compact threshold, peaks-loop **automatically fires `peaks compact auto --execute` — the LLM does NOT prompt the user to run `/compact` manually**. Stale prose that says "ask the user to compact" silently stalls the workflow. The v2.13.0 contract makes auto-compact a system responsibility, not a user action.
164
+ > **Zero-pause contract.** When context usage crosses the pre-compact threshold, peaks-loop **automatically fires `peaks code auto-compact` — the LLM does NOT prompt the user to run `/compact` manually**. Stale prose that says "ask the user to compact" silently stalls the workflow. The v2.13.0 contract makes auto-compact a system responsibility, not a user action.
165
165
 
166
166
  **Thresholds (v2.13.0, replacing legacy 50/75/90):**
167
167
 
168
168
  | ratio zone | zone name | action |
169
169
  |---|---|---|
170
170
  | `< 0.85` | normal | skip — LLM keeps working |
171
- | `0.85 ≤ ratio < 0.95` | **pre-compact zone** | `peaks compact auto --execute` fires **automatically** (deferred only when in-flight sub-agent batch is running; fires the moment the batch lands). The LLM does not prompt the user. |
172
- | `ratio ≥ 0.95` | **red-line (Karpathy §4)** | synchronous gate — `peaks compact auto --execute` invoked immediately; `peaks code context-now` returns `action: 'red-line'` and refuses to advance until ratio drops below 0.85. **Karpathy §4 automatic exception** — LLM cannot opt out. |
171
+ | `0.85 ≤ ratio < 0.95` | **pre-compact zone** | `peaks code auto-compact` fires **automatically** (deferred only when in-flight sub-agent batch is running; fires the moment the batch lands). The LLM does not prompt the user. |
172
+ | `ratio ≥ 0.95` | **red-line (Karpathy §4)** | synchronous gate — `peaks code auto-compact` invoked immediately; `peaks code context-now` returns `action: 'red-line'` and refuses to advance until ratio drops below 0.85. **Karpathy §4 automatic exception** — LLM cannot opt out. |
173
173
 
174
- **Probe primitive (single source of truth):** `peaks code context-now --json`. Do NOT use `peaks context check --prompt-size` (deprecated, will silently under-report ratio). Returns `{ ratio, action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' }` — Code reads `action` and dispatches `peaks compact auto --execute` on `auto-compact-now` or `red-line` without user confirmation.
174
+ **Probe primitive (single source of truth):** `peaks code context-now --json`. Do NOT use `peaks context check --prompt-size` (deprecated, will silently under-report ratio). Returns `{ ratio, action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' }` — Code reads `action` and dispatches `peaks code auto-compact` on `auto-compact-now` or `red-line` without user confirmation.
175
175
 
176
176
  **Enforcement layers (defense in depth):**
177
177
  1. `src/services/code/auto-compact-orchestrator.ts` — `evaluateAutoCompactDecision` default-returns `shouldCompact: true` for both `pre-compact` and `red-line`. Only deferral is `inFlightBatch.hasInFlightBatch` (D6.e); no LLM/human approval branch.
178
178
  2. `--enforce-job-mode` (v3.1.2) — Job mode elevates ≥0.85 to MANDATORY regardless of in-flight batch.
179
179
  3. `peaks code gate-step-08` (PreToolUse hook) — surfaces `auto-compact-now` on every Bash call when ratio is in the zone, so the LLM cannot wake up cold and forget.
180
- 4. Karpathy §4 exception: `peaks compact auto --execute` is fired *by the orchestrator*, not by the user. If you find yourself about to write "ask the user to compact" / "prompt the user to run `/compact`", STOP — that is the regression.
180
+ 4. Karpathy §4 exception: `peaks code auto-compact` is fired *by the orchestrator*, not by the user. If you find yourself about to write "ask the user to compact" / "prompt the user to run `/compact`", STOP — that is the regression.
181
181
 
182
182
  **Anti-pattern (DO NOT introduce):** any of these strings in skills/* or comments signals the zero-pause contract has been broken:
183
- - "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks compact auto` manually" / "the user is responsible for context management" / legacy mid/seventy-five/ninety percent tiers (current is 0.85 / 0.95).
183
+ - "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks code auto-compact` manually" / "the user is responsible for context management" / legacy mid/seventy-five/ninety percent tiers (current is 0.85 / 0.95).
184
184
 
185
185
  If the prose audit (`peaks audit red-lines`) flags any of the above, the slice is **blocked** until the prose is rewritten.
186
186
 
@@ -270,7 +270,7 @@ After `peaks-rd` finishes, Code MUST auto-route to `peaks-qa` without waiting fo
270
270
 
271
271
  ## Default runbook
272
272
 
273
- The end-to-end CLI sequence for `full-auto` lives in `references/runbook.md`. `assisted`/`strict` pause at `[CONFIRM]`; `full-auto`/`swarm` auto-proceed.
273
+ The end-to-end CLI sequence for `full-auto` lives in `references/runbook.md`. `assisted`/`strict` pause at `[CONFIRM]`; `full-auto`/`24h` auto-proceed. At a `[CONFIRM]` gate the CLI never prompts: the transition throws `CONFIRMATION_REQUIRED`, so Code asks the user via `AskUserQuestion` and, on approval, re-runs the same command with `--confirm`. Never expect or ask for a terminal `y/N` prompt.
274
274
 
275
275
  When adding new CLI commands, mirror into `references/runbook.md` and `tests/unit/skill-default-runbook.test.ts` (test falls back to the reference).
276
276
 
@@ -3,8 +3,8 @@
3
3
  > Body of `### Peaks-Loop Step 1`. After Step 0 has anchored the workspace and presence, when the user invokes Peaks-Loop Code without explicitly naming an execution profile, use `AskUserQuestion` to pick the profile. Present the recommended full-auto path as the first/default option with a practical description for each:
4
4
 
5
5
  1. **Full auto (Recommended)** — Peaks-Loop handles planning, role coordination, validation, and compact handoff end-to-end while preserving required confirmation gates for risky or shared-state actions.
6
- 2. **Assisted** — Peaks-Loop proposes plans, artifacts, and checks, then pauses for user decisions at major workflow boundaries.
7
- 3. **Swarm** — Peaks-Loop maximizes safe parallel role/worker execution for larger RD or QA workloads while keeping reducer validation and artifact boundaries explicit.
6
+ 2. **24h** — Long-run autonomy profile: auto-proceed gates, periodic checkpoints, and the partial auto-compact cadence for overnight / multi-hour runs. It may be auto-engaged by the T1–T5 triggers without a pick.
7
+ 3. **Assisted** — Peaks-Loop proposes plans, artifacts, and checks, then pauses for user decisions at major workflow boundaries.
8
8
  4. **Strict** — Peaks-Loop uses the most conservative gates: explicit confirmations, strict slice specs, coverage evidence, QA acceptance, and commit boundaries before continuing.
9
9
 
10
10
  Map the user's selection to the `--mode` flag value (used by `peaks skill presence:set`; `presence:set --mode` accepts any string, so the name matches the user-facing label rather than overloading "code" which is also the skill name):
@@ -12,10 +12,14 @@ Map the user's selection to the `--mode` flag value (used by `peaks skill presen
12
12
  | User selects | `--mode` value |
13
13
  |---|---|
14
14
  | Full auto | `full-auto` |
15
+ | 24h | `24h` |
15
16
  | Assisted | `assisted` |
16
- | Swarm | `swarm` |
17
17
  | Strict | `strict` |
18
18
 
19
+ > Note: in `assisted` / `strict`, a workflow-boundary confirmation is resolved by asking the user via `AskUserQuestion` and, on approval, re-running the same CLI command with `--confirm`. The CLI never opens a terminal prompt (no stdin read, no `y/N`) — an LLM-driven session has no TTY.
20
+
21
+ > Note: parallel role/worker fan-out is the default execution strategy in **every** mode — it is not a mode of its own. The old `swarm` profile was removed; a legacy on-disk `mode: 'swarm'` is read as `full-auto`.
22
+
19
23
  > Note: `peaks workflow route --mode code|team` is a **different** CLI dimension (code developer vs team flow) and is unrelated to the profile choice here. Do not conflate them.
20
24
 
21
25
  If the user already names a profile in their invocation (e.g. `/peaks-code --full-auto`, "用全自动模式"), skip this question and use the named profile directly.
@@ -72,10 +72,10 @@ This step is strictly IDE-agnostic. The LLM calls the peaks CLI; no IDE-specific
72
72
 
73
73
  ## Companion surface — auto-compact (zero-pause)
74
74
 
75
- When the runner crosses the v2.13.0 auto-compact thresholds (0.85 pre-compact / 0.95 red-line), `peaks session checkpoint` is NOT enough on its own — the LLM must also fire `peaks compact auto --execute` (or wait for the in-band PreToolUse hook at ratio ≥ 0.95 to compact the runner itself). See:
75
+ When the runner crosses the v2.13.0 auto-compact thresholds (0.85 pre-compact / 0.95 red-line), `peaks session checkpoint` is NOT enough on its own — the LLM must also fire `peaks code auto-compact` (or wait for the in-band PreToolUse hook at ratio ≥ 0.95 to compact the runner itself). See:
76
76
 
77
- - `peaks compact auto --execute` — orchestrator that writes the convergence plan + dispatches the compact pathway
78
- - `peaks compact auto` — the PreToolUse hook entrypoint fired by `.claude/settings.local.json` (slice 2026-07-02-auto-compact-zero-pause)
77
+ - `peaks code auto-compact` — orchestrator that writes the convergence plan + dispatches the compact pathway
78
+ - `peaks code auto-compact` — the PreToolUse hook entrypoint fired by `.claude/settings.local.json` (slice 2026-07-02-auto-compact-zero-pause)
79
79
  - `.peaks/memory/2026-06-27-auto-compact-design.md` — zero-human-intervention design rationale
80
80
 
81
- The two surfaces compose: `peaks session checkpoint` persists the context state, `peaks compact auto` collapses the runner's window. The LLM fires both in the 0.85–0.95 zone; the hook fires alone at ≥ 0.95.
81
+ The two surfaces compose: `peaks session checkpoint` persists the context state, `peaks code auto-compact` collapses the runner's window. The LLM fires both in the 0.85–0.95 zone; the hook fires alone at ≥ 0.95.