peaks-loop 4.0.0-beta.24 → 4.0.0-beta.25

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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,18 @@
11
11
  - peaks-loop-doctor@0.0.12
12
12
  - peaks-loop-final-review@0.0.12
13
13
 
14
+ ## 4.1.0 (Unreleased)
15
+
16
+ ### Added
17
+
18
+ - **Sub-agent memory preflight**: peaks-code orchestrator now
19
+ automatically injects a token-bounded `feedback / layer A` memory
20
+ block from `.peaks/memory/index.json` into every sub-agent's
21
+ system prompt. Defaults to 1.2k token cap (configurable via
22
+ `.peaks/preferences.json::memoryPreflight.maxTokens`); silent
23
+ degradation when the index is missing. No new CLI surface.
24
+ See `docs/superpowers/specs/2026-07-22-orchestrator-memory-preflight-design.md`.
25
+
14
26
  ## 4.0.0
15
27
 
16
28
  ### Patch Changes
package/README-en.md CHANGED
@@ -123,7 +123,7 @@ Every lane opens with **one slash command**.
123
123
 
124
124
  | | |
125
125
  | --- | --- |
126
- | **Latest** | [`4.0.0-beta.7`](https://github.com/SquabbyZ/peaks-loop/releases) — 4.x GA is in the works |
126
+ | **Latest** | [`4.0.0-beta.25`](https://github.com/SquabbyZ/peaks-loop/releases) — 4.x GA is in the works |
127
127
  | **Domains** | Code (`peaks-code`) · Content (`peaks-content`) · Project health (`peaks-doctor`) · Issue sweep (`peaks-issue-fix-orchestrator`) · Custom SOP (`peaks-sop`) · Cross-domain primitives (`peaks-solo` dispatcher · `peaks-resume` · `peaks-status` · `peaks-test`) |
128
128
  | **Sediment pool** | `~/.peaks/` local pool · twice-clean runs auto-promote to a bee · broken runs come back for you to redefine · the bee grows with your taste |
129
129
  | **Test suite** | 5,439 passed · 19 skipped · 0 failed |
package/README.md CHANGED
@@ -123,7 +123,7 @@ npm i -g peaks-loop
123
123
 
124
124
  | | |
125
125
  | --- | --- |
126
- | **最新版本** | [`4.0.0-beta.7`](https://github.com/SquabbyZ/peaks-loop/releases) — 4.x 正式版筹备中 |
126
+ | **最新版本** | [`4.0.0-beta.25`](https://github.com/SquabbyZ/peaks-loop/releases) — 4.x 正式版筹备中 |
127
127
  | **覆盖域** | 代码(`peaks-code`) · 内容(`peaks-content`) · 项目健康(`peaks-doctor`) · 批量修 issue(`peaks-issue-fix-orchestrator`) · 自定义 SOP(`peaks-sop`) · 通用原语(`peaks-solo` 分诊 / `peaks-resume` 续 / `peaks-status` 看 / `peaks-test` 测) |
128
128
  | **沉淀池** | `~/.peaks/` 本地池 · 跑两次自动晋升成 bee · 跑翻车让你重定义 · bee 跟着你的口味长 |
129
129
  | **测试套件** | 5,439 passed · 19 skipped · 0 failed |
@@ -36,6 +36,8 @@ import { writeLogEntry } from '../../services/log/logger.js';
36
36
  import { HEADROOM_MODES, PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
37
37
  import { runDispatchFromDag } from './dispatch-from-dag.js';
38
38
  import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
39
+ import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
40
+ import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
39
41
  export function registerDispatchCommand(parent, io) {
40
42
  addJsonOption(parent
41
43
  .command('dispatch')
@@ -152,9 +154,11 @@ export function registerDispatchCommand(parent, io) {
152
154
  // Preferences hard-block when headroom.enabled=false (returns HEADROOM_DISABLED_BY_PREFERENCE).
153
155
  // loadPreferences can throw on schema mismatch; we fall back to defaults to avoid
154
156
  // breaking the dispatch on a stale preferences.json file.
157
+ let projectPrefs = DEFAULT_PREFERENCES;
155
158
  let headroomPrefs = DEFAULT_PREFERENCES.headroom;
156
159
  try {
157
- headroomPrefs = loadPreferences(projectRoot).headroom;
160
+ projectPrefs = loadPreferences(projectRoot);
161
+ headroomPrefs = projectPrefs.headroom;
158
162
  }
159
163
  catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
160
164
  // Keep default preferences; the user can re-run with explicit --headroom-mode
@@ -187,7 +191,18 @@ export function registerDispatchCommand(parent, io) {
187
191
  }
188
192
  // G7.7 headroom compress (opt-in). If headroom fails or is unavailable,
189
193
  // fall back to the original prompt + emit warning.
190
- let effectivePrompt = `${formatTestToolDetection()}\n\n${options.prompt}`;
194
+ // Slice 2026-07-22-orchestrator-memory-preflight (Task 5): prepend the
195
+ // memory preflight block (or silently skip when unavailable) via the
196
+ // pure-function builder, BEFORE the headroom-ai compress step so the
197
+ // compressor sees the augmented payload.
198
+ const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
199
+ const memoryBlock = await preflightService.fetchBlock(role);
200
+ const memoryAugmentedBody = buildDispatchSystemPrompt({
201
+ taskTitle: role,
202
+ taskBody: options.prompt,
203
+ memoryBlock,
204
+ });
205
+ let effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}`;
191
206
  let headroomCompressed = false;
192
207
  let headroomResult = null;
193
208
  const warnings = [...decision.warnings];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * buildDispatchSystemPrompt — pure-function prompt composer for sub-agent dispatch.
3
+ *
4
+ * Slice 2026-07-22-orchestrator-memory-preflight (Task 5). The orchestrator
5
+ * dispatch flow (`src/cli/commands/dispatch-commands.ts`) calls
6
+ * `MemoryPreflightService.fetchBlock` and feeds the result into this builder
7
+ * so the memory block, when available, is prepended BEFORE the task brief.
8
+ *
9
+ * Keeping the builder a pure function (no IO) makes the three acceptance
10
+ * cases easy to test in isolation:
11
+ * 1. returns the original prompt (sans memory block) when unavailable
12
+ * 2. prepends the memory block when available
13
+ * 3. never pushes the memory block below the task brief
14
+ */
15
+ import type { MemoryPreflightResult } from './memory-preflight-service.js';
16
+ export interface DispatchPromptInput {
17
+ taskTitle: string;
18
+ taskBody: string;
19
+ memoryBlock: MemoryPreflightResult;
20
+ }
21
+ /**
22
+ * Compose the system-prompt body that the dispatch site prepends to
23
+ * `formatTestToolDetection()\n\n`.
24
+ *
25
+ * Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
26
+ * controller brief): when the memory block is unavailable, the caller does
27
+ * `formatTestToolDetection()\n\n${taskBody}` — i.e. the final prompt is exactly
28
+ * `${formatTestToolDetection()}\n\n${taskBody}`. Today's pre-change behavior
29
+ * produced the same string from `src/cli/commands/dispatch-commands.ts:220`,
30
+ * so the unavailable branch MUST return `taskBody` (NOT a `# title\n\n` wrap).
31
+ *
32
+ * Available branch prepends the memory block before the `## Task` heading so
33
+ * `## Project memory …` always sits above the task brief (never pushed below
34
+ * it).
35
+ */
36
+ export declare function buildDispatchSystemPrompt(input: DispatchPromptInput): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Compose the system-prompt body that the dispatch site prepends to
3
+ * `formatTestToolDetection()\n\n`.
4
+ *
5
+ * Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
6
+ * controller brief): when the memory block is unavailable, the caller does
7
+ * `formatTestToolDetection()\n\n${taskBody}` — i.e. the final prompt is exactly
8
+ * `${formatTestToolDetection()}\n\n${taskBody}`. Today's pre-change behavior
9
+ * produced the same string from `src/cli/commands/dispatch-commands.ts:220`,
10
+ * so the unavailable branch MUST return `taskBody` (NOT a `# title\n\n` wrap).
11
+ *
12
+ * Available branch prepends the memory block before the `## Task` heading so
13
+ * `## Project memory …` always sits above the task brief (never pushed below
14
+ * it).
15
+ */
16
+ export function buildDispatchSystemPrompt(input) {
17
+ const { taskBody, memoryBlock } = input;
18
+ if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
19
+ return `${memoryBlock.block}\n## Task\n${taskBody}\n`;
20
+ }
21
+ return taskBody;
22
+ }
@@ -0,0 +1,8 @@
1
+ import type { MemoryIndexEntry } from '../memory/memory-search-service.js';
2
+ export declare class MemoryIndexReader {
3
+ private readonly projectRoot;
4
+ private cache;
5
+ constructor(projectRoot: string);
6
+ loadIfStale(): MemoryIndexEntry[];
7
+ selectFeedbackLayerA(cap: number): MemoryIndexEntry[];
8
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const LAYER_A_RE = /peaks-feedback-promoted:\s*layer=A\b/;
4
+ export class MemoryIndexReader {
5
+ projectRoot;
6
+ cache = null;
7
+ constructor(projectRoot) {
8
+ this.projectRoot = projectRoot;
9
+ }
10
+ loadIfStale() {
11
+ const indexPath = join(this.projectRoot, '.peaks', 'memory', 'index.json');
12
+ if (!existsSync(indexPath))
13
+ return [];
14
+ const { mtimeMs } = statSync(indexPath);
15
+ if (this.cache && this.cache.mtimeMs === mtimeMs)
16
+ return this.cache.entries;
17
+ let raw;
18
+ try {
19
+ raw = JSON.parse(readFileSync(indexPath, 'utf8'));
20
+ }
21
+ catch {
22
+ this.cache = null;
23
+ return [];
24
+ }
25
+ const entries = flattenIndex(raw);
26
+ this.cache = { mtimeMs, entries };
27
+ return entries;
28
+ }
29
+ selectFeedbackLayerA(cap) {
30
+ const all = this.loadIfStale();
31
+ return all
32
+ .filter((e) => e.kind === 'feedback')
33
+ .filter((e) => LAYER_A_RE.test(e.description))
34
+ .slice(0, Math.max(1, Math.trunc(cap)));
35
+ }
36
+ }
37
+ function flattenIndex(raw) {
38
+ if (!raw || typeof raw !== 'object')
39
+ return [];
40
+ const out = [];
41
+ const obj = raw;
42
+ for (const layer of ['hot', 'warm', 'cold']) {
43
+ const bucket = obj[layer];
44
+ if (!bucket || typeof bucket !== 'object')
45
+ continue;
46
+ for (const k of ['feedback', 'project', 'reference', 'user']) {
47
+ const list = bucket[k];
48
+ if (!Array.isArray(list))
49
+ continue;
50
+ for (const item of list) {
51
+ if (item && typeof item === 'object')
52
+ out.push(item);
53
+ }
54
+ }
55
+ }
56
+ return out;
57
+ }
@@ -0,0 +1,12 @@
1
+ export declare class MemoryLruCache {
2
+ private readonly budgetBytes;
3
+ private readonly store;
4
+ private currentBytes;
5
+ constructor(budgetBytes: number);
6
+ set(key: string, body: string): void;
7
+ get(key: string): string | undefined;
8
+ delete(key: string): void;
9
+ size(): number;
10
+ private evictIfOver;
11
+ private evictAll;
12
+ }
@@ -0,0 +1,60 @@
1
+ export class MemoryLruCache {
2
+ budgetBytes;
3
+ store = new Map();
4
+ currentBytes = 0;
5
+ constructor(budgetBytes) {
6
+ this.budgetBytes = budgetBytes;
7
+ }
8
+ set(key, body) {
9
+ if (this.store.has(key))
10
+ this.delete(key);
11
+ const bytes = Buffer.byteLength(body, 'utf8');
12
+ if (bytes > this.budgetBytes) {
13
+ // Oversized insert: evict ALL existing entries (LRU order) so only
14
+ // the newly inserted key remains in the cache. This prevents the
15
+ // unbounded growth that a naive "single oversized exception" would
16
+ // allow when oversized values are inserted consecutively.
17
+ this.evictAll();
18
+ this.store.set(key, { body, bytes });
19
+ this.currentBytes += bytes;
20
+ return;
21
+ }
22
+ this.store.set(key, { body, bytes });
23
+ this.currentBytes += bytes;
24
+ this.evictIfOver();
25
+ }
26
+ get(key) {
27
+ const entry = this.store.get(key);
28
+ if (!entry)
29
+ return undefined;
30
+ // refresh recency
31
+ this.store.delete(key);
32
+ this.store.set(key, entry);
33
+ return entry.body;
34
+ }
35
+ delete(key) {
36
+ const entry = this.store.get(key);
37
+ if (!entry)
38
+ return;
39
+ this.store.delete(key);
40
+ this.currentBytes -= entry.bytes;
41
+ }
42
+ size() {
43
+ return this.currentBytes;
44
+ }
45
+ evictIfOver() {
46
+ while (this.currentBytes > this.budgetBytes && this.store.size > 0) {
47
+ // Map preserves insertion order — first key is least-recently-touched
48
+ const oldestKey = this.store.keys().next().value;
49
+ if (oldestKey === undefined)
50
+ break;
51
+ this.delete(oldestKey);
52
+ }
53
+ }
54
+ evictAll() {
55
+ // Empty the store; called before an oversized insert so only the new
56
+ // entry remains, per the eviction contract documented above.
57
+ this.store.clear();
58
+ this.currentBytes = 0;
59
+ }
60
+ }
@@ -0,0 +1,19 @@
1
+ import type { ProjectPreferences } from '../preferences/preferences-types.js';
2
+ export interface MemoryPreflightConfig {
3
+ readonly enabled: boolean;
4
+ readonly maxTokens: number;
5
+ readonly listCap: number;
6
+ readonly contentCacheBytes: number;
7
+ }
8
+ /**
9
+ * Loose input type for `resolveMemoryPreflightConfig`. Accepts either a
10
+ * full `ProjectPreferences` (e.g. the output of `loadPreferences()`) or
11
+ * any object that at minimum carries `memoryPreflight`. Lets callers
12
+ * pass a partial overlay literal — e.g. `{}` or `{ memoryPreflight: {...} }`
13
+ * — without having to construct a fully-populated preferences fixture.
14
+ *
15
+ * Downstream tasks (orchestrator service, dispatch hook) should import
16
+ * this alias rather than re-deriving the structural type.
17
+ */
18
+ export type MemoryPreflightPrefsInput = ProjectPreferences | Pick<ProjectPreferences, 'memoryPreflight'>;
19
+ export declare function resolveMemoryPreflightConfig(prefs: MemoryPreflightPrefsInput): MemoryPreflightConfig;
@@ -0,0 +1,25 @@
1
+ const DEFAULTS = Object.freeze({
2
+ enabled: true,
3
+ maxTokens: 1200,
4
+ listCap: 12,
5
+ contentCacheBytes: 6000,
6
+ });
7
+ const LIST_CAP_MIN = 1;
8
+ const LIST_CAP_MAX = 50;
9
+ function asFiniteInt(value, fallback) {
10
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
11
+ ? Math.trunc(value)
12
+ : fallback;
13
+ }
14
+ export function resolveMemoryPreflightConfig(prefs) {
15
+ const m = prefs.memoryPreflight ?? {};
16
+ const listCapRaw = asFiniteInt(m.listCap, DEFAULTS.listCap);
17
+ return {
18
+ enabled: m.enabled === false ? false : DEFAULTS.enabled,
19
+ maxTokens: m.maxTokens && m.maxTokens > 0 ? m.maxTokens : DEFAULTS.maxTokens,
20
+ listCap: Math.min(LIST_CAP_MAX, Math.max(LIST_CAP_MIN, listCapRaw)),
21
+ contentCacheBytes: m.contentCacheBytes && m.contentCacheBytes > 0
22
+ ? m.contentCacheBytes
23
+ : DEFAULTS.contentCacheBytes,
24
+ };
25
+ }
@@ -0,0 +1,19 @@
1
+ import { type MemoryPreflightPrefsInput } from './memory-preflight-config.js';
2
+ export interface MemoryPreflightResult {
3
+ available: boolean;
4
+ block?: string;
5
+ feedbackListItems?: number;
6
+ cachedItemCount?: number;
7
+ reason?: string;
8
+ truncated?: boolean | undefined;
9
+ droppedCount?: number | undefined;
10
+ }
11
+ export declare class MemoryPreflightService {
12
+ private readonly reader;
13
+ private readonly config;
14
+ /** path -> body; sub-agent-requested memo contents (Task 4 deviation, see module header). */
15
+ private readonly cachedMemoContents;
16
+ constructor(projectRoot: string, prefs: MemoryPreflightPrefsInput);
17
+ cacheMemoContent(path: string, content: string): void;
18
+ fetchBlock(_taskTitle: string): Promise<MemoryPreflightResult>;
19
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * MemoryPreflightService — orchestrator-facing API for the
3
+ * "Orchestrator Memory Preflight" slice (see
4
+ * docs/superpowers/specs/2026-07-22-orchestrator-memory-preflight-design.md).
5
+ *
6
+ * Consumes:
7
+ * - resolveMemoryPreflightConfig (Task 1) — merged defaults + per-project overlay
8
+ * - MemoryIndexReader (Task 3) — reads .peaks/memory/index.json + layer=A filter
9
+ * - MemoryLruCache (Task 2) — constructed for capacity parity, but NOT used
10
+ * for memo content (see deviation note below)
11
+ * - compressPrompt (existing) — headroom-ai wrapper for hard-cap compression
12
+ *
13
+ * Deviation from brief — controller-accepted (pre-task 4):
14
+ * The brief mandated use of `MemoryLruCache` for memo content caching. The
15
+ * orchestrator dispatch flow only ever has at most a handful of explicit
16
+ * `cacheMemoContent` calls per task (typically 0-3), and ordering / explicit
17
+ * invalidation is preferred over LRU eviction by recency. To keep the
18
+ * semantic obvious and the byte budget check trivial, this implementation
19
+ * uses a plain `Map<path, body>` for memo path -> body content. The
20
+ * `MemoryLruCache` class remains as a separate, reusable LRU primitive
21
+ * (Task 2) and is not consumed by this service.
22
+ */
23
+ import { compressPrompt } from './headroom-client.js';
24
+ import { MemoryIndexReader } from './memory-index-reader.js';
25
+ import { resolveMemoryPreflightConfig, } from './memory-preflight-config.js';
26
+ async function compressToCap(text, capBytes, mode) {
27
+ try {
28
+ const result = await compressPrompt(text, mode);
29
+ if (result.warning !== null || result.compressedPrompt === null) {
30
+ return { text, truncated: false };
31
+ }
32
+ const compressed = result.compressedPrompt;
33
+ if (Buffer.byteLength(compressed, 'utf8') > capBytes) {
34
+ const sliced = compressed.slice(0, Math.max(0, capBytes - 64)) + '\n…[truncated]';
35
+ return { text: sliced, truncated: true };
36
+ }
37
+ return { text: compressed, truncated: false };
38
+ }
39
+ catch {
40
+ return { text, truncated: false };
41
+ }
42
+ }
43
+ export class MemoryPreflightService {
44
+ reader;
45
+ config;
46
+ /** path -> body; sub-agent-requested memo contents (Task 4 deviation, see module header). */
47
+ cachedMemoContents = new Map();
48
+ constructor(projectRoot, prefs) {
49
+ this.config = resolveMemoryPreflightConfig(prefs);
50
+ this.reader = new MemoryIndexReader(projectRoot);
51
+ }
52
+ cacheMemoContent(path, content) {
53
+ if (!this.config.enabled)
54
+ return;
55
+ const bytes = Buffer.byteLength(content, 'utf8');
56
+ if (bytes > this.config.contentCacheBytes) {
57
+ // too big to ever fit; do not cache.
58
+ return;
59
+ }
60
+ this.cachedMemoContents.set(path, content);
61
+ }
62
+ async fetchBlock(_taskTitle) {
63
+ if (!this.config.enabled)
64
+ return { available: false, reason: 'DISABLED' };
65
+ const selected = this.reader.selectFeedbackLayerA(this.config.listCap);
66
+ if (selected.length === 0) {
67
+ const any = this.reader.loadIfStale();
68
+ if (any.length === 0)
69
+ return { available: false, reason: 'MEMORY_INDEX_MISSING' };
70
+ return { available: false, reason: 'NO_FEEDBACK_LAYER_A' };
71
+ }
72
+ const listLines = selected
73
+ .map((e) => `- * ${e.name}\n Path: ${e.sourcePath}\n One-line: ${summarize(e.description)}`)
74
+ .join('\n');
75
+ let tail = '\n';
76
+ let cachedCount = 0;
77
+ if (this.cachedMemoContents.size > 0) {
78
+ const sections = [];
79
+ for (const [path, body] of this.cachedMemoContents) {
80
+ sections.push(`### ${path}\n\n${body}`);
81
+ cachedCount += 1;
82
+ }
83
+ tail = `\n\n## Requested memory details:\n${sections.join('\n\n')}\n`;
84
+ }
85
+ const header = '## Project memory relevant to this task\n';
86
+ const composed = `${header}${listLines}${tail}`;
87
+ const capBytes = Math.max(64, this.config.maxTokens * 4);
88
+ const { text, truncated } = await compressToCap(composed, capBytes, 'balanced');
89
+ const droppedCount = truncated ? selected.length - countItemsInBlock(text) : 0;
90
+ return {
91
+ available: true,
92
+ block: text,
93
+ feedbackListItems: selected.length,
94
+ cachedItemCount: cachedCount,
95
+ truncated,
96
+ droppedCount: droppedCount > 0 ? droppedCount : undefined,
97
+ };
98
+ }
99
+ }
100
+ function summarize(description) {
101
+ // Drop the <!-- peaks-feedback-promoted: layer=A --> marker, take the next 1 line.
102
+ const cleaned = description.replace(/<!--[^>]*-->/g, '').trim();
103
+ return cleaned.split('\n')[0] ?? cleaned;
104
+ }
105
+ function countItemsInBlock(text) {
106
+ // Count `- * ` markers ONLY in the list portion (between the
107
+ // `## Project memory relevant to this task` header and the
108
+ // `## Requested memory details:` sub-section, if present). Memo
109
+ // bodies appended under `## Requested memory details:` may
110
+ // legitimately contain their own `- * ` markdown bullets, which
111
+ // would otherwise inflate the count and produce a wrong
112
+ // `droppedCount` in the truncated case.
113
+ const headerEnd = text.indexOf('\n## ');
114
+ if (headerEnd === -1) {
115
+ return (text.match(/- \* /g) ?? []).length;
116
+ }
117
+ const tailStart = text.indexOf('\n## Requested memory details:', headerEnd + 1);
118
+ const listEnd = tailStart === -1 ? text.length : tailStart;
119
+ return (text.slice(0, listEnd).match(/- \* /g) ?? []).length;
120
+ }
@@ -9,6 +9,16 @@ export function preferencesPath(projectRoot) {
9
9
  export function loadPreferences(projectRoot) {
10
10
  const filePath = preferencesPath(projectRoot);
11
11
  if (!existsSync(filePath)) {
12
+ // Slice 2026-07-22-orchestrator-memory-preflight: when
13
+ // .peaks/preferences.json is missing, this returns a clone of
14
+ // DEFAULT_PREFERENCES, which now carries the `memoryPreflight`
15
+ // block (see preferences-types.ts). Partial overlays fall through
16
+ // to mergePreferences below, where the new `memoryPreflight` block
17
+ // shallow-replaces the default block — `mergePreferences` does not
18
+ // deep-merge `memoryPreflight` (only `fanout` is deep-merged), so a
19
+ // partial overlay will replace the whole block. Resolution still
20
+ // works because `resolveMemoryPreflightConfig` applies field-level
21
+ // defaults over the partial input (see memory-preflight-config.ts).
12
22
  return structuredClone(DEFAULT_PREFERENCES);
13
23
  }
14
24
  let raw;
@@ -131,6 +131,19 @@ export interface ProjectPreferences {
131
131
  * saved file is rejected at load time.
132
132
  */
133
133
  readonly fanout: FanoutPreference;
134
+ /**
135
+ * Slice 2026-07-22-orchestrator-memory-preflight: orchestrator-side memory
136
+ * preflight knobs. Optional — `resolveMemoryPreflightConfig` (see
137
+ * src/services/context/memory-preflight-config.ts) merges over a hard-coded
138
+ * default block when this key is absent, so legacy preferences.json files
139
+ * keep working unchanged.
140
+ */
141
+ readonly memoryPreflight?: {
142
+ readonly enabled?: boolean;
143
+ readonly maxTokens?: number;
144
+ readonly listCap?: number;
145
+ readonly contentCacheBytes?: number;
146
+ };
134
147
  }
135
148
  export type FanoutMode = 'fan-out';
136
149
  export declare const FANOUT_MODES: readonly FanoutMode[];
@@ -50,4 +50,15 @@ export const DEFAULT_PREFERENCES = {
50
50
  fanout: {
51
51
  defaultMode: 'fan-out'
52
52
  },
53
+ // Slice 2026-07-22-orchestrator-memory-preflight: defaults aligned with
54
+ // memory-preflight-config.ts::DEFAULTS. Kept in sync manually because
55
+ // loadPreferences() returns DEFAULT_PREFERENCES verbatim when the on-disk
56
+ // file is absent, and partial overlays rely on memoryPreflight being
57
+ // defined here.
58
+ memoryPreflight: {
59
+ enabled: true,
60
+ maxTokens: 1200,
61
+ listCap: 12,
62
+ contentCacheBytes: 6000,
63
+ },
53
64
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.0-beta.24",
3
+ "version": "4.0.0-beta.25",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -54,9 +54,9 @@
54
54
  "headroom-ai": "0.22.4",
55
55
  "yaml": "^2.9.0",
56
56
  "zod": "^3.25.76",
57
+ "peaks-loop-audit-independent": "0.0.11",
57
58
  "peaks-loop-crystallization": "0.0.12",
58
59
  "peaks-loop-doctor": "0.0.11",
59
- "peaks-loop-audit-independent": "0.0.11",
60
60
  "peaks-loop-mut": "0.1.0",
61
61
  "peaks-loop-final-review": "0.0.11",
62
62
  "peaks-loop-job-snapshot": "0.0.5",