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
@@ -12,22 +12,18 @@
12
12
  // are stored as standard YAML frontmatter (name / description /
13
13
  // metadata.type / metadata.sourceArtifact) followed by the body.
14
14
  //
15
- // Both parsers share the 8-kind `VALID_MEMORY_KINDS` allow-list and the
15
+ // Both parsers share the `VALID_MEMORY_KINDS` allow-list (derived from the
16
+ // canonical `PROJECT_MEMORY_KINDS` tuple in `../types.ts`) and the
16
17
  // `slugify` helper used to derive filenames from titles.
17
18
  // ---------------------------------------------------------------------------
18
- export const VALID_MEMORY_KINDS = new Set([
19
- 'project',
20
- 'rule',
21
- 'decision',
22
- 'reference',
23
- 'feedback',
24
- 'convention',
25
- 'module',
26
- 'lesson'
27
- ]);
28
- /** Exported for guard tests + tooling that needs to enumerate the valid
29
- * set without duplicating the literal. Single source of truth. */
30
- export const VALID_PROJECT_MEMORY_KINDS = Array.from(VALID_MEMORY_KINDS);
19
+ import { basename } from 'node:path';
20
+ import { PROJECT_MEMORY_KINDS } from '../types.js';
21
+ /** Accepted-kind set, derived from the canonical `PROJECT_MEMORY_KINDS`
22
+ * tuple so the parser cannot drift from the union type / tier map. */
23
+ export const VALID_MEMORY_KINDS = new Set(PROJECT_MEMORY_KINDS);
24
+ /** Exported for guard tests + tooling that needs to enumerate the accepted
25
+ * set (CLI help text, `--kind` validation) without duplicating the literal. */
26
+ export const VALID_PROJECT_MEMORY_KINDS = PROJECT_MEMORY_KINDS;
31
27
  export function slugify(title) {
32
28
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
33
29
  return slug.length > 0 ? slug : 'project-memory';
@@ -69,36 +65,129 @@ export function renderMemoryFile(memory) {
69
65
  ''
70
66
  ].join('\n');
71
67
  }
72
- export function parseStoredMemoryFile(content, filePath) {
68
+ /**
69
+ * Resolve the peaks memory kind from a stored memory file's frontmatter.
70
+ *
71
+ * Resolution order (first *valid* kind wins):
72
+ * 1. nested `metadata.type` — the canonical peaks contract
73
+ * 2. top-level `kind:` — the legacy alias (was silently dropped before)
74
+ * 3. top-level `type:` — tolerated by the pre-existing trim-based reader
75
+ * 4. `none` — reported as unclassified; never invented
76
+ *
77
+ * Slice 2026-09-09-memory-system-overhaul (B): before this helper, files
78
+ * using a top-level `kind:` were silently dropped by the reader (defect
79
+ * #2). Falling through to `kind:` / `type:` is a strict superset of the
80
+ * old behaviour — no previously-indexed file changes kind.
81
+ */
82
+ export function resolveMemoryKind(content) {
83
+ const parsed = parseMemoryFrontmatter(content);
84
+ return parsed.kind;
85
+ }
86
+ /**
87
+ * Single parse surface for stored memory frontmatter. Both
88
+ * `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
89
+ * classifiers consume this so there is exactly one kind-resolution rule
90
+ * in the codebase.
91
+ */
92
+ export function parseMemoryFrontmatter(content) {
73
93
  const normalized = content.replace(/\r\n/g, '\n');
74
- if (!normalized.startsWith('---\n'))
75
- return null;
94
+ if (!normalized.startsWith('---\n')) {
95
+ return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
96
+ }
76
97
  const endIndex = normalized.indexOf('\n---\n', 4);
77
- if (endIndex < 0)
78
- return null;
98
+ if (endIndex < 0) {
99
+ return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
100
+ }
79
101
  const frontmatter = normalized.slice(4, endIndex);
80
102
  const body = normalized.slice(endIndex + '\n---\n'.length).trim();
81
103
  let name;
104
+ let titleField;
82
105
  let description;
83
- let kind;
84
106
  let sourceArtifact;
107
+ let nestedType;
108
+ let topType;
109
+ let kindField;
110
+ let inMetadata = false;
85
111
  for (const rawLine of frontmatter.split('\n')) {
112
+ const indented = /^\s/.test(rawLine);
86
113
  const line = rawLine.trim();
114
+ if (!indented) {
115
+ inMetadata = line === 'metadata:';
116
+ }
87
117
  if (line.startsWith('name:'))
88
118
  name = line.slice('name:'.length).trim();
119
+ else if (line.startsWith('title:')) {
120
+ if (!indented)
121
+ titleField = line.slice('title:'.length).trim();
122
+ }
89
123
  else if (line.startsWith('description:'))
90
124
  description = line.slice('description:'.length).trim();
91
- else if (line.startsWith('type:'))
92
- kind = line.slice('type:'.length).trim();
125
+ else if (line.startsWith('type:')) {
126
+ const value = line.slice('type:'.length).trim();
127
+ if (indented || inMetadata)
128
+ nestedType ??= value;
129
+ else
130
+ topType ??= value;
131
+ }
132
+ else if (line.startsWith('kind:'))
133
+ kindField ??= line.slice('kind:'.length).trim();
93
134
  else if (line.startsWith('sourceArtifact:'))
94
135
  sourceArtifact = line.slice('sourceArtifact:'.length).trim();
95
136
  }
96
- if (!name || !kind || !VALID_MEMORY_KINDS.has(kind) || body.length === 0)
137
+ const candidates = [
138
+ ['metadata.type', nestedType],
139
+ ['kind', kindField],
140
+ ['type', topType]
141
+ ];
142
+ let kind = { kind: null, source: 'none', rawKind: null };
143
+ for (const [source, raw] of candidates) {
144
+ if (raw === undefined || raw === '')
145
+ continue;
146
+ if (kind.rawKind === null)
147
+ kind = { kind: null, source: 'none', rawKind: raw };
148
+ if (VALID_MEMORY_KINDS.has(raw)) {
149
+ kind = { kind: raw, source, rawKind: raw };
150
+ break;
151
+ }
152
+ }
153
+ return { hasFrontmatter: true, frontmatter, ...(name !== undefined ? { name } : {}), ...(titleField !== undefined ? { title: titleField } : {}), ...(description !== undefined ? { description } : {}), ...(sourceArtifact !== undefined ? { sourceArtifact } : {}), kind, body };
154
+ }
155
+ /**
156
+ * Deterministic name fallback chain for the read path:
157
+ *
158
+ * 1. `name:` — the canonical field written by `renderMemoryFile`
159
+ * 2. `title:` — hand-written / legacy files (the 5 on-disk files
160
+ * this slice fixes carried only `title:` + `kind:`)
161
+ * 3. filename stem — last resort, so a well-formed memory with a valid
162
+ * kind is never dropped just for missing a name
163
+ *
164
+ * Empty values are skipped rather than accepted: a `name:` of `''` still
165
+ * falls through, and a file whose stem is also empty resolves to null so the
166
+ * caller's validation is preserved (never invents a name).
167
+ */
168
+ export function resolveMemoryName(parsed, filePath) {
169
+ if (parsed.name !== undefined && parsed.name.length > 0)
170
+ return { name: parsed.name, source: 'name' };
171
+ if (parsed.title !== undefined && parsed.title.length > 0)
172
+ return { name: parsed.title, source: 'title' };
173
+ const stem = basename(filePath, '.md');
174
+ if (stem.length > 0)
175
+ return { name: stem, source: 'stem' };
176
+ return { name: null, source: 'none' };
177
+ }
178
+ export function parseStoredMemoryFile(content, filePath) {
179
+ const parsed = parseMemoryFrontmatter(content);
180
+ if (!parsed.hasFrontmatter)
181
+ return null;
182
+ const { description, sourceArtifact, body } = parsed;
183
+ const kind = parsed.kind.kind;
184
+ const { name } = resolveMemoryName(parsed, filePath);
185
+ if (name === null || kind === null || body.length === 0)
97
186
  return null;
98
187
  return {
99
188
  name,
100
189
  title: description ?? name,
101
- kind: kind,
190
+ kind,
102
191
  sourceArtifact: sourceArtifact && sourceArtifact !== 'undefined' ? sourceArtifact : null,
103
192
  body,
104
193
  filePath
@@ -1,4 +1,34 @@
1
- export type ProjectMemoryKind = 'project' | 'rule' | 'decision' | 'reference' | 'feedback' | 'convention' | 'module' | 'lesson';
1
+ /**
2
+ * Canonical memory-kind vocabulary — the single source of truth.
3
+ *
4
+ * The union type, the accepted-kind set in the frontmatter parser, the
5
+ * hot/warm tier map below, `KIND_ORDER` in the reindex view, and the CLI
6
+ * `--kind` help text all derive from this tuple. Adding a kind here is the
7
+ * only edit required; TypeScript then forces `MEMORY_KIND_TIER` to cover it.
8
+ *
9
+ * Slice 2026-09-10-memory-vocab-and-rotate (E): the original 8 kinds are
10
+ * unchanged. The 13 appended kinds were observed on disk with real values
11
+ * that the index schema rejected (`peaks memory reindex` reported them as
12
+ * `unrecognized kind value`). Accepting them moves those files into the
13
+ * index; files with no kind field at all stay reported as unclassified.
14
+ */
15
+ export declare const PROJECT_MEMORY_KINDS: readonly ["project", "rule", "decision", "reference", "feedback", "convention", "module", "lesson", "bug", "investigation", "technical-pattern", "project-rule", "design", "handoff", "session-handoff", "project-todo", "publish-closure", "project-closure", "slice-closure", "slice-pilot-findings", "sediment"];
16
+ export type ProjectMemoryKind = (typeof PROJECT_MEMORY_KINDS)[number];
17
+ /** Hot kinds keep their body in the always-available index; warm kinds are
18
+ * indexed with the same entry shape but read on demand. */
19
+ export type MemoryKindTier = 'hot' | 'warm';
20
+ /**
21
+ * Hot/warm tier per kind. Insertion order IS the deterministic section order
22
+ * used by the generated `MEMORY.md` (`KIND_ORDER` in `index/reindex.ts`),
23
+ * so this object's key order is load-bearing — append new kinds at the end
24
+ * of their tier block rather than reshuffling existing entries.
25
+ *
26
+ * The `Record<ProjectMemoryKind, MemoryKindTier>` annotation makes a missing
27
+ * tier a compile error whenever `PROJECT_MEMORY_KINDS` grows.
28
+ */
29
+ export declare const MEMORY_KIND_TIER: Record<ProjectMemoryKind, MemoryKindTier>;
30
+ export declare const HOT_MEMORY_KINDS: readonly ProjectMemoryKind[];
31
+ export declare const WARM_MEMORY_KINDS: readonly ProjectMemoryKind[];
2
32
  export type ExtractedProjectMemory = {
3
33
  title: string;
4
34
  kind: ProjectMemoryKind;
@@ -6,4 +6,79 @@
6
6
  // from here, and the top-level `index.ts` re-exports them for back-compat
7
7
  // with downstream callers (CLI commands, audit writers, presence service).
8
8
  // ---------------------------------------------------------------------------
9
- export {};
9
+ /**
10
+ * Canonical memory-kind vocabulary — the single source of truth.
11
+ *
12
+ * The union type, the accepted-kind set in the frontmatter parser, the
13
+ * hot/warm tier map below, `KIND_ORDER` in the reindex view, and the CLI
14
+ * `--kind` help text all derive from this tuple. Adding a kind here is the
15
+ * only edit required; TypeScript then forces `MEMORY_KIND_TIER` to cover it.
16
+ *
17
+ * Slice 2026-09-10-memory-vocab-and-rotate (E): the original 8 kinds are
18
+ * unchanged. The 13 appended kinds were observed on disk with real values
19
+ * that the index schema rejected (`peaks memory reindex` reported them as
20
+ * `unrecognized kind value`). Accepting them moves those files into the
21
+ * index; files with no kind field at all stay reported as unclassified.
22
+ */
23
+ export const PROJECT_MEMORY_KINDS = [
24
+ // Original 8 (pre-2026-09-10).
25
+ 'project',
26
+ 'rule',
27
+ 'decision',
28
+ 'reference',
29
+ 'feedback',
30
+ 'convention',
31
+ 'module',
32
+ 'lesson',
33
+ // Slice E expansion — observed on-disk values, hot tier.
34
+ 'bug',
35
+ 'investigation',
36
+ 'technical-pattern',
37
+ 'project-rule',
38
+ // Slice E expansion — observed on-disk values, warm tier.
39
+ 'design',
40
+ 'handoff',
41
+ 'session-handoff',
42
+ 'project-todo',
43
+ 'publish-closure',
44
+ 'project-closure',
45
+ 'slice-closure',
46
+ 'slice-pilot-findings',
47
+ 'sediment'
48
+ ];
49
+ /**
50
+ * Hot/warm tier per kind. Insertion order IS the deterministic section order
51
+ * used by the generated `MEMORY.md` (`KIND_ORDER` in `index/reindex.ts`),
52
+ * so this object's key order is load-bearing — append new kinds at the end
53
+ * of their tier block rather than reshuffling existing entries.
54
+ *
55
+ * The `Record<ProjectMemoryKind, MemoryKindTier>` annotation makes a missing
56
+ * tier a compile error whenever `PROJECT_MEMORY_KINDS` grows.
57
+ */
58
+ export const MEMORY_KIND_TIER = {
59
+ // hot — full body kept in the index
60
+ feedback: 'hot',
61
+ decision: 'hot',
62
+ rule: 'hot',
63
+ convention: 'hot',
64
+ module: 'hot',
65
+ lesson: 'hot',
66
+ bug: 'hot',
67
+ investigation: 'hot',
68
+ 'technical-pattern': 'hot',
69
+ 'project-rule': 'hot',
70
+ // warm — same entry shape, read on demand
71
+ project: 'warm',
72
+ reference: 'warm',
73
+ design: 'warm',
74
+ handoff: 'warm',
75
+ 'session-handoff': 'warm',
76
+ 'project-todo': 'warm',
77
+ 'publish-closure': 'warm',
78
+ 'project-closure': 'warm',
79
+ 'slice-closure': 'warm',
80
+ 'slice-pilot-findings': 'warm',
81
+ sediment: 'warm'
82
+ };
83
+ export const HOT_MEMORY_KINDS = PROJECT_MEMORY_KINDS.filter((kind) => MEMORY_KIND_TIER[kind] === 'hot');
84
+ export const WARM_MEMORY_KINDS = PROJECT_MEMORY_KINDS.filter((kind) => MEMORY_KIND_TIER[kind] === 'warm');
@@ -8,7 +8,10 @@ export type ConfirmationOptions = {
8
8
  forceConfirm?: boolean | undefined;
9
9
  };
10
10
  export declare class ConfirmationRequiredError extends Error {
11
- constructor(transitionKey: TransitionKey);
11
+ readonly transitionKey: TransitionKey;
12
+ readonly mode: SkillPresenceMode;
13
+ readonly nextActions: readonly string[];
14
+ constructor(transitionKey: TransitionKey, mode: SkillPresenceMode);
12
15
  }
13
16
  export declare function requireUserConfirmation(options: ConfirmationOptions): Promise<void>;
14
17
  export {};
@@ -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
+ }
@@ -127,9 +127,23 @@ export interface ProjectPreferences {
127
127
  */
128
128
  readonly memoryPreflight?: {
129
129
  readonly enabled?: boolean;
130
+ /** Back-compat token cap; converted to bytes as `maxTokens * 4`. */
130
131
  readonly maxTokens?: number;
132
+ /** Back-compat hot item cap; `hotItemCap` overrides it when set. */
131
133
  readonly listCap?: number;
132
134
  readonly contentCacheBytes?: number;
135
+ /** Slice 2026-09-09-memory-retrieval: hard byte cap on the block. */
136
+ readonly maxBytes?: number;
137
+ /** Slice 2026-09-09-memory-retrieval: max hot items (default 10). */
138
+ readonly hotItemCap?: number;
139
+ /** Slice 2026-09-09-memory-retrieval: max warm items (default 4; 0 disables). */
140
+ readonly warmItemCap?: number;
141
+ /** Slice 2026-09-09-memory-retrieval: min task-token hits for warm eligibility. */
142
+ readonly warmMinTokenHits?: number;
143
+ /** Slice 2026-09-09-memory-retrieval: soft selection wall-clock budget (ms). */
144
+ readonly selectionTimeBudgetMs?: number;
145
+ /** Slice 2026-09-09-memory-retrieval: inline memo bodies (default false). */
146
+ readonly includeBodies?: boolean;
133
147
  };
134
148
  }
135
149
  export type FanoutMode = 'fan-out';
@@ -48,5 +48,13 @@ export const DEFAULT_PREFERENCES = {
48
48
  maxTokens: 1200,
49
49
  listCap: 12,
50
50
  contentCacheBytes: 6000,
51
+ // Slice 2026-09-09-memory-retrieval tiered budget (see
52
+ // memory-preflight-config.ts::DEFAULTS).
53
+ maxBytes: 4800,
54
+ hotItemCap: 10,
55
+ warmItemCap: 4,
56
+ warmMinTokenHits: 1,
57
+ selectionTimeBudgetMs: 200,
58
+ includeBodies: false,
51
59
  },
52
60
  };
@@ -64,8 +64,8 @@ export declare const RunStateContractSchema: z.ZodObject<{
64
64
  status: z.ZodEnum<{
65
65
  blocked: "blocked";
66
66
  failed: "failed";
67
- running: "running";
68
67
  done: "done";
68
+ running: "running";
69
69
  paused: "paused";
70
70
  }>;
71
71
  current_step: z.ZodString;
@@ -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;