pi-brain 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="./banner.png" alt="pi-brain banner" width="720" />
3
+ </p>
4
+
1
5
  # pi-brain
2
6
 
3
7
  Versioned memory for the [pi coding agent](https://github.com/badlogic/pi-mono). Agents commit decisions and reasoning to a `.memory/` directory, preserving context across sessions, compactions, and model switches.
@@ -43,9 +47,10 @@ LLM providers cache the prefix of each request. If the prefix changes between tu
43
47
  Brain avoids this entirely:
44
48
 
45
49
  - **Static AGENTS.md** — Written once at init, never updated. No branch names, no commit counts, no dynamic state. The system prompt prefix stays identical across every turn and session.
46
- - **No per-turn injection** — No `before_agent_start` hook, no changing content before the conversation. The agent retrieves memory on demand via tool calls, which appear as conversation messages appended at the end (outside the cached prefix).
47
- - **Fixed tool definitions** — All five tools are registered at startup with static schemas. No tools added or removed mid-conversation.
50
+ - **No per-turn prompt mutation** — Brain does not rewrite `systemPrompt` between turns. Status context is appended as message content, keeping the cached prefix stable.
51
+ - **Fixed tool definitions** — Tool schemas are static at startup. No tools are added or removed mid-conversation.
48
52
  - **Subagent isolation** — Commit distillation runs in a separate API call with its own cache. The main agent's cache is never touched.
53
+ - **Regression-tested safety** — Prompt-cache safety invariants are covered in `src/cache-safety.test.ts` (property tests for append-only prompt behavior, lifecycle-gated status injection, and deterministic status rendering).
49
54
 
50
55
  The result: Brain adds zero overhead to your prompt cache hit rate.
51
56
 
package/banner.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-brain",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Versioned memory extension for the pi coding agent",
5
5
  "keywords": [
6
6
  "agent-memory",
@@ -27,7 +27,8 @@
27
27
  "skills/",
28
28
  "agents/",
29
29
  "package.json",
30
- "README.md"
30
+ "README.md",
31
+ "banner.png"
31
32
  ],
32
33
  "type": "module",
33
34
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: brain
3
- description: Use when working on a project with Brain agent memory management. Triggers on memory_status, memory_commit, memory_branch, memory_merge, memory_switch tool usage, or when the project has a .memory/ directory.
3
+ description: Use when working on a project with Brain agent memory management. Triggers on memory_commit, memory_branch tool usage, or when the project has a .memory/ directory.
4
4
  ---
5
5
 
6
6
  # Brain — Agent Memory
@@ -19,11 +19,11 @@ Replace `/absolute/path/to/skills/brain` with the skill directory shown in the
19
19
 
20
20
  ### After Init
21
21
 
22
- 1. **Tell the user to run `/reload`** — the memory tools won't detect the new `.memory/`
23
- directory until the extension reloads.
24
- 2. **Write `.memory/main.md`** — the project roadmap (see below).
25
- 3. **Call `memory_status`** to verify Brain is active.
26
- 4. **Make your first commit** when you reach a meaningful milestone.
22
+ 1. **Write `.memory/main.md`** — the project roadmap (see below).
23
+ 2. **Make your first commit** when you reach a meaningful milestone.
24
+
25
+ > **Note:** No `/reload` is needed. The memory tools lazily detect `.memory/`
26
+ > on every call via `tryLoad()`.
27
27
 
28
28
  ### Writing main.md — Greenfield vs Brownfield
29
29
 
@@ -39,6 +39,22 @@ questions as you understand them from conversation with the user.
39
39
  Then write the roadmap covering: project purpose, current state, key decisions
40
40
  already made, completed milestones, and planned work.
41
41
 
42
+ ## Context Retrieval
43
+
44
+ Memory status is **automatically injected** at session start (via the
45
+ `before_agent_start` hook) and appended to every successful `memory_branch` and
46
+ `memory_commit` result. Automatic status is compact and may truncate long
47
+ roadmaps, so keep the newest critical context near the top of `.memory/main.md`.
48
+ You do not need to call a separate tool to see status.
49
+
50
+ For deep retrieval, use `read` directly:
51
+
52
+ - `read .memory/branches/<name>/commits.md` — full branch history
53
+ - `read .memory/branches/<name>/log.md` — OTA trace since last commit
54
+ - `read .memory/branches/<name>/metadata.yaml` — structured metadata
55
+ - `read .memory/main.md` — project roadmap
56
+ - `read .memory/AGENTS.md` — full protocol reference
57
+
42
58
  ## When to Commit
43
59
 
44
60
  - You've reached a stable understanding or decision
@@ -72,28 +88,6 @@ then produces the structured commit entry. You just provide a good `summary` str
72
88
  - The branch's findings should inform the main line of thinking
73
89
  - Include what was learned even if the approach was abandoned
74
90
 
75
- **Important:** Always review the source branch history BEFORE calling `memory_merge`.
76
- Use:
77
-
78
- - `memory_status` for high-level status
79
- - `read .memory/branches/<target>/commits.md` for full branch history
80
-
91
+ **Important:** Always review the source branch history BEFORE calling merge.
92
+ Use `read .memory/branches/<target>/commits.md` for full branch history.
81
93
  You need the full context to write a good synthesis.
82
-
83
- ## When to Use Context Retrieval
84
-
85
- - Starting a new session on an existing project — call `memory_status` first
86
- - Before making a decision that might conflict with earlier reasoning
87
- - When you need to recall the rationale behind a previous decision
88
-
89
- ## Context Retrieval
90
-
91
- Use `memory_status` for high-level status only.
92
-
93
- For deep retrieval, use `read` directly:
94
-
95
- - `read .memory/branches/<name>/commits.md` — full branch history
96
- - `read .memory/branches/<name>/log.md` — OTA trace since last commit
97
- - `read .memory/branches/<name>/metadata.yaml` — structured metadata
98
- - `read .memory/main.md` — project roadmap
99
- - `read .memory/AGENTS.md` — full protocol reference
@@ -4,13 +4,18 @@ This directory contains your project's agent memory, managed by the Brain extens
4
4
 
5
5
  ## Tools
6
6
 
7
- | Tool | Purpose |
8
- | --------------- | --------------------------------------- |
9
- | `memory_commit` | Checkpoint a milestone in understanding |
10
- | `memory_branch` | Create a memory branch for exploration |
11
- | `memory_merge` | Synthesize branch conclusions |
12
- | `memory_status` | Multi-resolution retrieval of memory |
13
- | `memory_switch` | Switch active memory branch |
7
+ | Tool | Purpose |
8
+ | --------------- | ----------------------------------------- |
9
+ | `memory_commit` | Checkpoint a milestone in understanding |
10
+ | `memory_branch` | Manage branches: create, switch, or merge |
11
+
12
+ ### memory_branch Actions
13
+
14
+ | Action | Required Params | Description |
15
+ | -------- | --------------------- | ------------------------------------------- |
16
+ | `create` | `name`, `purpose` | Create a new branch and switch to it |
17
+ | `switch` | `branch` | Switch active memory branch |
18
+ | `merge` | `branch`, `synthesis` | Synthesize a branch's insights into current |
14
19
 
15
20
  ## File Structure
16
21
 
@@ -41,4 +46,4 @@ The latest commit always contains a self-contained summary of the full branch hi
41
46
  - **Decisions over details**: Capture "why", not "what" — git tracks file changes
42
47
  - **Rolling summaries**: Each commit re-synthesizes all prior progress
43
48
  - **No direct log.md writes**: The extension maintains log.md automatically
44
- - **Call `memory_status` first**: Always review context before merging or starting new work
49
+ - **Status is automatic**: Memory status is injected at session start and appended to tool results (compact/truncated when large; use `read .memory/main.md` for full roadmap)
@@ -4,4 +4,4 @@ This project uses Brain for agent memory management.
4
4
 
5
5
  **Start here when orienting:** Read `.memory/main.md` for the project roadmap, key decisions, and open problems.
6
6
  Read `.memory/AGENTS.md` for the full Brain protocol reference.
7
- Tools: memory_commit, memory_branch, memory_merge, memory_switch, memory_status
7
+ Tools: memory_commit, memory_branch (create/switch/merge)
package/src/branches.ts CHANGED
@@ -1,6 +1,25 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
+ function sortBranchNames(names: readonly string[]): string[] {
5
+ const sorted: string[] = [];
6
+
7
+ for (const name of names) {
8
+ const insertIndex = sorted.findIndex(
9
+ (existing) => existing.localeCompare(name) > 0
10
+ );
11
+
12
+ if (insertIndex === -1) {
13
+ sorted.push(name);
14
+ continue;
15
+ }
16
+
17
+ sorted.splice(insertIndex, 0, name);
18
+ }
19
+
20
+ return sorted;
21
+ }
22
+
4
23
  /**
5
24
  * Manages `.memory/branches/` directory operations.
6
25
  * Each branch has: log.md, commits.md, metadata.yaml.
@@ -64,15 +83,21 @@ export class BranchManager {
64
83
  return fs.readFileSync(metaPath, "utf8");
65
84
  }
66
85
 
86
+ protected readBranchEntries(): string[] {
87
+ return fs.readdirSync(this.branchesDir);
88
+ }
89
+
67
90
  listBranches(): string[] {
68
91
  if (!fs.existsSync(this.branchesDir)) {
69
92
  return [];
70
93
  }
71
94
 
72
- return fs.readdirSync(this.branchesDir).filter((entry) => {
95
+ const branchNames = this.readBranchEntries().filter((entry) => {
73
96
  const fullPath = path.join(this.branchesDir, entry);
74
97
  return fs.statSync(fullPath).isDirectory();
75
98
  });
99
+
100
+ return sortBranchNames(branchNames);
76
101
  }
77
102
 
78
103
  branchExists(name: string): boolean {
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
 
4
4
  import type {
5
5
  AgentToolResult,
6
+ BeforeAgentStartEvent,
6
7
  ExtensionAPI,
7
8
  ExtensionContext,
8
9
  SessionBeforeCompactEvent,
@@ -13,9 +14,7 @@ import { BranchManager } from "./branches.js";
13
14
  import { LOG_SIZE_WARNING_BYTES } from "./constants.js";
14
15
  import { executeMemoryBranch } from "./memory-branch.js";
15
16
  import { executeMemoryCommit, finalizeMemoryCommit } from "./memory-commit.js";
16
- import { executeMemoryStatus } from "./memory-context.js";
17
- import { executeMemoryMerge } from "./memory-merge.js";
18
- import { executeMemorySwitch } from "./memory-switch.js";
17
+ import { buildStatusView } from "./memory-context.js";
19
18
  import { formatOtaEntry } from "./ota-formatter.js";
20
19
  import { extractOtaInput } from "./ota-logger.js";
21
20
  import { MemoryState } from "./state.js";
@@ -23,6 +22,8 @@ import { extractCommitBlocks, spawnCommitter } from "./subagent.js";
23
22
 
24
23
  const MEMORY_NOT_INITIALIZED_MESSAGE =
25
24
  "Brain not initialized. Run brain-init.sh first.";
25
+ const BEFORE_AGENT_START_ROADMAP_CHAR_LIMIT = 1200;
26
+ const BEFORE_AGENT_START_BRANCH_LIMIT = 8;
26
27
 
27
28
  function createTextResult(text: string): AgentToolResult<unknown> {
28
29
  return {
@@ -52,6 +53,21 @@ function upsertCurrentSession(state: MemoryState, ctx: ExtensionContext): void {
52
53
  state.save();
53
54
  }
54
55
 
56
+ function setBrainFooterStatus(
57
+ ctx: ExtensionContext,
58
+ state: MemoryState | null,
59
+ branchManager: BranchManager | null
60
+ ): void {
61
+ if (!state || !branchManager || !state.isInitialized) {
62
+ ctx.ui.setStatus("brain", undefined);
63
+ return;
64
+ }
65
+
66
+ const turnCount = branchManager.getLogTurnCount(state.activeBranch);
67
+ const turnLabel = `${turnCount} uncommitted turn${turnCount === 1 ? "" : "s"}`;
68
+ ctx.ui.setStatus("brain", `Brain: ${state.activeBranch} (${turnLabel})`);
69
+ }
70
+
55
71
  function buildCompactionReminder(
56
72
  state: MemoryState,
57
73
  branchManager: BranchManager
@@ -85,6 +101,7 @@ function resolveSkillPath(): string {
85
101
  export default function activate(pi: ExtensionAPI) {
86
102
  let state: MemoryState | null = null;
87
103
  let branchManager: BranchManager | null = null;
104
+ let statusInjected = false;
88
105
 
89
106
  function tryLoad(ctx: ExtensionContext): boolean {
90
107
  if (isMemoryReady(state, branchManager)) {
@@ -104,107 +121,56 @@ export default function activate(pi: ExtensionAPI) {
104
121
  return true;
105
122
  }
106
123
 
107
- pi.registerTool({
108
- name: "memory_status",
109
- label: "Memory Status",
110
- description: "Retrieve agent memory status overview.",
111
- parameters: Type.Object({
112
- level: Type.Optional(Type.String()),
113
- branch: Type.Optional(Type.String()),
114
- commit: Type.Optional(Type.String()),
115
- segment: Type.Optional(Type.String()),
116
- }),
117
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
118
- if (
119
- !tryLoad(ctx) ||
120
- !isMemoryReady(state, branchManager) ||
121
- !branchManager
122
- ) {
123
- return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
124
- }
125
-
126
- return createTextResult(
127
- executeMemoryStatus(params, state, branchManager, ctx.cwd)
128
- );
129
- },
130
- });
131
-
132
124
  pi.registerTool({
133
125
  name: "memory_branch",
134
126
  label: "Memory Branch",
135
- description: "Create a new memory branch.",
136
- parameters: Type.Object({
137
- name: Type.String({ description: "Branch name" }),
138
- purpose: Type.String({ description: "Why this branch exists" }),
139
- }),
140
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
141
- if (
142
- !tryLoad(ctx) ||
143
- !isMemoryReady(state, branchManager) ||
144
- !branchManager
145
- ) {
146
- return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
147
- }
148
-
149
- const previousBranch = state.activeBranch;
150
- const result = executeMemoryBranch(params, state, branchManager);
151
-
152
- if (state.activeBranch !== previousBranch) {
153
- upsertCurrentSession(state, ctx);
154
- }
155
-
156
- return createTextResult(result);
157
- },
158
- });
159
-
160
- pi.registerTool({
161
- name: "memory_switch",
162
- label: "Memory Switch",
163
- description: "Switch to another memory branch.",
127
+ description:
128
+ "Manage memory branches. Actions: create (new branch), switch (change active branch), merge (synthesize branch into current).",
164
129
  parameters: Type.Object({
165
- branch: Type.String({ description: "Target branch name" }),
130
+ action: Type.Union(
131
+ [Type.Literal("create"), Type.Literal("switch"), Type.Literal("merge")],
132
+ {
133
+ description: 'Action to perform: "create", "switch", or "merge"',
134
+ }
135
+ ),
136
+ name: Type.Optional(
137
+ Type.String({ description: "Branch name (required for create)" })
138
+ ),
139
+ purpose: Type.Optional(
140
+ Type.String({
141
+ description: "Why this branch exists (required for create)",
142
+ })
143
+ ),
144
+ branch: Type.Optional(
145
+ Type.String({
146
+ description: "Target branch (required for switch and merge)",
147
+ })
148
+ ),
149
+ synthesis: Type.Optional(
150
+ Type.String({ description: "Synthesized insight (required for merge)" })
151
+ ),
166
152
  }),
167
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
153
+ execute(_toolCallId, params, _signal, _onUpdate, ctx) {
168
154
  if (
169
155
  !tryLoad(ctx) ||
170
156
  !isMemoryReady(state, branchManager) ||
171
157
  !branchManager
172
158
  ) {
173
- return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
159
+ setBrainFooterStatus(ctx, state, branchManager);
160
+ return Promise.resolve(
161
+ createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE)
162
+ );
174
163
  }
175
164
 
176
165
  const previousBranch = state.activeBranch;
177
- const result = executeMemorySwitch(params, state, branchManager);
166
+ const result = executeMemoryBranch(params, state, branchManager, ctx.cwd);
178
167
 
179
168
  if (state.activeBranch !== previousBranch) {
180
169
  upsertCurrentSession(state, ctx);
181
170
  }
182
171
 
183
- return createTextResult(result);
184
- },
185
- });
186
-
187
- pi.registerTool({
188
- name: "memory_merge",
189
- label: "Memory Merge",
190
- description:
191
- "Merge insights from one memory branch into the active branch.",
192
- parameters: Type.Object({
193
- branch: Type.String({ description: "Source branch to merge from" }),
194
- synthesis: Type.String({
195
- description: "Synthesized insight from source branch",
196
- }),
197
- }),
198
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
199
- if (
200
- !tryLoad(ctx) ||
201
- !isMemoryReady(state, branchManager) ||
202
- !branchManager
203
- ) {
204
- return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
205
- }
206
-
207
- return createTextResult(executeMemoryMerge(params, state, branchManager));
172
+ setBrainFooterStatus(ctx, state, branchManager);
173
+ return Promise.resolve(createTextResult(result));
208
174
  },
209
175
  });
210
176
 
@@ -222,6 +188,7 @@ export default function activate(pi: ExtensionAPI) {
222
188
  !isMemoryReady(state, branchManager) ||
223
189
  !branchManager
224
190
  ) {
191
+ setBrainFooterStatus(ctx, state, branchManager);
225
192
  return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
226
193
  }
227
194
 
@@ -246,9 +213,11 @@ export default function activate(pi: ExtensionAPI) {
246
213
  params.summary,
247
214
  commitContent,
248
215
  state,
249
- branchManager
216
+ branchManager,
217
+ ctx.cwd
250
218
  );
251
219
 
220
+ setBrainFooterStatus(ctx, state, branchManager);
252
221
  return createTextResult(message);
253
222
  },
254
223
  });
@@ -257,14 +226,15 @@ export default function activate(pi: ExtensionAPI) {
257
226
  state = new MemoryState(ctx.cwd);
258
227
  state.load();
259
228
  branchManager = new BranchManager(ctx.cwd);
229
+ statusInjected = false;
260
230
 
261
231
  if (!state.isInitialized) {
232
+ setBrainFooterStatus(ctx, state, branchManager);
262
233
  return;
263
234
  }
264
235
 
265
236
  upsertCurrentSession(state, ctx);
266
237
 
267
- const turnCount = branchManager.getLogTurnCount(state.activeBranch);
268
238
  const logSizeBytes = branchManager.getLogSizeBytes(state.activeBranch);
269
239
 
270
240
  if (logSizeBytes >= LOG_SIZE_WARNING_BYTES) {
@@ -273,20 +243,66 @@ export default function activate(pi: ExtensionAPI) {
273
243
  `Brain: log.md is large (${sizeKB} KB). You should commit to distill this into structured memory.`,
274
244
  "warning"
275
245
  );
276
- } else {
277
- ctx.ui.notify(
278
- `Brain active: branch "${state.activeBranch}" (${turnCount} uncommitted turn${turnCount === 1 ? "" : "s"}).`,
279
- "info"
280
- );
281
246
  }
247
+
248
+ setBrainFooterStatus(ctx, state, branchManager);
249
+ });
250
+
251
+ pi.on("session_switch", (_event, ctx) => {
252
+ statusInjected = false;
253
+
254
+ state = new MemoryState(ctx.cwd);
255
+ state.load();
256
+ branchManager = new BranchManager(ctx.cwd);
257
+
258
+ if (state.isInitialized) {
259
+ upsertCurrentSession(state, ctx);
260
+ }
261
+
262
+ setBrainFooterStatus(ctx, state, branchManager);
263
+ });
264
+
265
+ pi.on("before_agent_start", (_event: BeforeAgentStartEvent, ctx) => {
266
+ if (statusInjected) {
267
+ return;
268
+ }
269
+
270
+ if (
271
+ !tryLoad(ctx) ||
272
+ !isMemoryReady(state, branchManager) ||
273
+ !branchManager
274
+ ) {
275
+ return;
276
+ }
277
+
278
+ statusInjected = true;
279
+
280
+ const status = buildStatusView(state, branchManager, ctx.cwd, {
281
+ compact: true,
282
+ roadmapCharLimit: BEFORE_AGENT_START_ROADMAP_CHAR_LIMIT,
283
+ branchLimit: BEFORE_AGENT_START_BRANCH_LIMIT,
284
+ });
285
+ return {
286
+ message: {
287
+ customType: "brain-status",
288
+ content: status,
289
+ display: true,
290
+ details: {},
291
+ },
292
+ };
293
+ });
294
+
295
+ pi.on("session_compact", () => {
296
+ statusInjected = false;
282
297
  });
283
298
 
284
299
  pi.on("resources_discover", () => ({
285
300
  skillPaths: [resolveSkillPath()],
286
301
  }));
287
302
 
288
- pi.on("turn_end", (event) => {
303
+ pi.on("turn_end", (event, ctx) => {
289
304
  if (!isMemoryReady(state, branchManager) || !branchManager) {
305
+ setBrainFooterStatus(ctx, state, branchManager);
290
306
  return;
291
307
  }
292
308
 
@@ -297,6 +313,7 @@ export default function activate(pi: ExtensionAPI) {
297
313
 
298
314
  const entry = formatOtaEntry(input);
299
315
  branchManager.appendLog(state.activeBranch, entry);
316
+ setBrainFooterStatus(ctx, state, branchManager);
300
317
  });
301
318
 
302
319
  pi.on("session_before_compact", (event) => {
@@ -1,28 +1,173 @@
1
1
  import type { BranchManager } from "./branches.js";
2
+ import { generateHash } from "./hash.js";
3
+ import { buildStatusView } from "./memory-context.js";
2
4
  import type { MemoryState } from "./state.js";
3
5
 
4
6
  interface MemoryBranchParams {
5
- name: string;
6
- purpose: string;
7
+ action: string;
8
+ name?: string;
9
+ purpose?: string;
10
+ branch?: string;
11
+ synthesis?: string;
7
12
  }
8
13
 
9
- /**
10
- * Execute the memory_branch tool — create a new memory branch.
11
- */
12
- export function executeMemoryBranch(
14
+ interface ActionResult {
15
+ text: string;
16
+ ok: boolean;
17
+ }
18
+
19
+ function executeCreate(
13
20
  params: MemoryBranchParams,
14
21
  state: MemoryState,
15
22
  branches: BranchManager
16
- ): string {
23
+ ): ActionResult {
17
24
  const { name, purpose } = params;
18
25
 
26
+ if (!name || !purpose) {
27
+ return {
28
+ text: '"name" and "purpose" are required for the create action.',
29
+ ok: false,
30
+ };
31
+ }
32
+
19
33
  if (branches.branchExists(name)) {
20
- return `Branch "${name}" already exists. Use memory_switch to switch to it.`;
34
+ return {
35
+ text: `Branch "${name}" already exists. Use action "switch" to switch to it.`,
36
+ ok: false,
37
+ };
21
38
  }
22
39
 
23
40
  branches.createBranch(name, purpose);
24
41
  state.setActiveBranch(name);
25
42
  state.save();
26
43
 
27
- return `Created branch "${name}" and switched to it.\nPurpose: ${purpose}`;
44
+ return {
45
+ text: `Created branch "${name}" and switched to it.\nPurpose: ${purpose}`,
46
+ ok: true,
47
+ };
48
+ }
49
+
50
+ function executeSwitch(
51
+ params: MemoryBranchParams,
52
+ state: MemoryState,
53
+ branches: BranchManager
54
+ ): ActionResult {
55
+ const { branch } = params;
56
+
57
+ if (!branch) {
58
+ return { text: '"branch" is required for the switch action.', ok: false };
59
+ }
60
+
61
+ if (!branches.branchExists(branch)) {
62
+ return {
63
+ text: `Branch "${branch}" not found. Available branches: ${branches.listBranches().join(", ")}`,
64
+ ok: false,
65
+ };
66
+ }
67
+
68
+ state.setActiveBranch(branch);
69
+ state.save();
70
+
71
+ const latest = branches.getLatestCommit(branch);
72
+ const summary = latest ?? "No commits yet.";
73
+
74
+ return { text: `Switched to branch "${branch}".\n\n${summary}`, ok: true };
75
+ }
76
+
77
+ function executeMerge(
78
+ params: MemoryBranchParams,
79
+ state: MemoryState,
80
+ branches: BranchManager
81
+ ): ActionResult {
82
+ const { branch: sourceBranch, synthesis } = params;
83
+
84
+ if (!sourceBranch || !synthesis) {
85
+ return {
86
+ text: '"branch" and "synthesis" are required for the merge action.',
87
+ ok: false,
88
+ };
89
+ }
90
+
91
+ const targetBranch = state.activeBranch;
92
+
93
+ if (sourceBranch === targetBranch) {
94
+ return {
95
+ text: `Cannot merge branch "${sourceBranch}" into itself.`,
96
+ ok: false,
97
+ };
98
+ }
99
+
100
+ if (!branches.branchExists(sourceBranch)) {
101
+ return {
102
+ text: `Branch "${sourceBranch}" not found. Available branches: ${branches.listBranches().join(", ")}`,
103
+ ok: false,
104
+ };
105
+ }
106
+
107
+ const hash = generateHash();
108
+ const timestamp = new Date().toISOString();
109
+ const summary = `Merge from ${sourceBranch}`;
110
+
111
+ const entry = [
112
+ "",
113
+ "---",
114
+ "",
115
+ `## Commit ${hash} | ${timestamp}`,
116
+ "",
117
+ `### Merge from ${sourceBranch}`,
118
+ "",
119
+ synthesis,
120
+ "",
121
+ ].join("\n");
122
+
123
+ branches.appendCommit(targetBranch, entry);
124
+
125
+ state.setLastCommit(targetBranch, hash, timestamp, summary);
126
+ state.save();
127
+
128
+ return {
129
+ text: `Merge commit ${hash} written to branch "${targetBranch}" (merged from "${sourceBranch}").`,
130
+ ok: true,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Execute the unified memory_branch tool.
136
+ * Actions: create, switch, merge.
137
+ * On success, appends the current memory status view.
138
+ */
139
+ export function executeMemoryBranch(
140
+ params: MemoryBranchParams,
141
+ state: MemoryState,
142
+ branches: BranchManager,
143
+ projectDir: string
144
+ ): string {
145
+ let result: ActionResult;
146
+
147
+ switch (params.action) {
148
+ case "create": {
149
+ result = executeCreate(params, state, branches);
150
+ break;
151
+ }
152
+ case "switch": {
153
+ result = executeSwitch(params, state, branches);
154
+ break;
155
+ }
156
+ case "merge": {
157
+ result = executeMerge(params, state, branches);
158
+ break;
159
+ }
160
+ default: {
161
+ return `Unknown action "${params.action}". Valid actions: create, switch, merge.`;
162
+ }
163
+ }
164
+
165
+ if (!result.ok) {
166
+ return result.text;
167
+ }
168
+
169
+ const status = buildStatusView(state, branches, projectDir, {
170
+ compact: true,
171
+ });
172
+ return `${result.text}\n\n${status}`;
28
173
  }
@@ -1,5 +1,6 @@
1
1
  import type { BranchManager } from "./branches.js";
2
2
  import { generateHash } from "./hash.js";
3
+ import { buildStatusView } from "./memory-context.js";
3
4
  import type { MemoryState } from "./state.js";
4
5
  import { buildCommitterTask } from "./subagent.js";
5
6
 
@@ -26,13 +27,14 @@ export function executeMemoryCommit(
26
27
 
27
28
  /**
28
29
  * Step 2: Write the agent's commit content to commits.md,
29
- * clear log.md, and update state.
30
+ * clear log.md, update state, and return result with status.
30
31
  */
31
32
  export function finalizeMemoryCommit(
32
33
  summary: string,
33
34
  commitContent: string,
34
35
  state: MemoryState,
35
- branches: BranchManager
36
+ branches: BranchManager,
37
+ projectDir: string
36
38
  ): string {
37
39
  const branch = state.activeBranch;
38
40
  const hash = generateHash();
@@ -54,5 +56,9 @@ export function finalizeMemoryCommit(
54
56
  state.setLastCommit(branch, hash, timestamp, summary);
55
57
  state.save();
56
58
 
57
- return `Commit ${hash} written to branch "${branch}".`;
59
+ const resultText = `Commit ${hash} written to branch "${branch}".`;
60
+ const status = buildStatusView(state, branches, projectDir, {
61
+ compact: true,
62
+ });
63
+ return `${resultText}\n\n${status}`;
58
64
  }
@@ -4,7 +4,15 @@ import * as path from "node:path";
4
4
  import type { BranchManager } from "./branches.js";
5
5
  import { LOG_SIZE_WARNING_BYTES } from "./constants.js";
6
6
  import type { MemoryState } from "./state.js";
7
- import type { MemoryStatusParams } from "./types.js";
7
+
8
+ interface StatusViewOptions {
9
+ compact?: boolean;
10
+ roadmapCharLimit?: number;
11
+ branchLimit?: number;
12
+ }
13
+
14
+ const DEFAULT_COMPACT_ROADMAP_CHAR_LIMIT = 1200;
15
+ const DEFAULT_COMPACT_BRANCH_LIMIT = 8;
8
16
 
9
17
  function extractCommitSummaryLine(commitEntry: string): string {
10
18
  const marker = "### This Commit's Contribution";
@@ -30,31 +38,91 @@ function extractCommitSummaryLine(commitEntry: string): string {
30
38
  return "(unknown)";
31
39
  }
32
40
 
33
- function buildStatusView(
34
- state: MemoryState,
35
- branches: BranchManager,
36
- projectDir: string
37
- ): string {
38
- const lines = ["# Memory Status", ""];
39
-
41
+ function buildRoadmapSection(
42
+ lines: string[],
43
+ projectDir: string,
44
+ compact: boolean,
45
+ roadmapCharLimit: number
46
+ ): void {
40
47
  const mainMdPath = path.join(projectDir, ".memory", "main.md");
41
- if (fs.existsSync(mainMdPath)) {
42
- const roadmap = fs.readFileSync(mainMdPath, "utf8").trim();
43
- if (roadmap) {
44
- lines.push(roadmap, "");
45
- } else {
46
- lines.push(
47
- "Roadmap is empty. Update `.memory/main.md` with project goals and current state.",
48
- ""
49
- );
50
- }
51
- } else {
48
+ if (!fs.existsSync(mainMdPath)) {
52
49
  lines.push(
53
50
  "No roadmap found. Create `.memory/main.md` to set project goals.",
54
51
  ""
55
52
  );
53
+ return;
54
+ }
55
+
56
+ const roadmap = fs.readFileSync(mainMdPath, "utf8").trim();
57
+ if (!roadmap) {
58
+ lines.push(
59
+ "Roadmap is empty. Update `.memory/main.md` with project goals and current state.",
60
+ ""
61
+ );
62
+ return;
63
+ }
64
+
65
+ if (!compact || roadmap.length <= roadmapCharLimit) {
66
+ lines.push(roadmap, "");
67
+ return;
68
+ }
69
+
70
+ const excerpt = roadmap.slice(0, roadmapCharLimit).trimEnd();
71
+ lines.push(excerpt, "");
72
+ lines.push(
73
+ `_Roadmap truncated for automatic status output. Use \`read .memory/main.md\` for full roadmap._`,
74
+ ""
75
+ );
76
+ }
77
+
78
+ function buildBranchesSection(
79
+ lines: string[],
80
+ state: MemoryState,
81
+ branches: BranchManager,
82
+ compact: boolean,
83
+ branchLimit: number
84
+ ): void {
85
+ const branchList = branches.listBranches();
86
+ if (branchList.length === 0) {
87
+ return;
56
88
  }
57
89
 
90
+ lines.push("## Branches", "");
91
+
92
+ const visibleBranches = compact
93
+ ? branchList.slice(0, branchLimit)
94
+ : branchList;
95
+ for (const name of visibleBranches) {
96
+ const latest = branches.getLatestCommit(name);
97
+ const summary = latest ? extractCommitSummaryLine(latest) : "(no commits)";
98
+ const marker = name === state.activeBranch ? " (active)" : "";
99
+ lines.push(`- **${name}**${marker}: ${summary}`);
100
+ }
101
+
102
+ if (compact && branchList.length > visibleBranches.length) {
103
+ const remaining = branchList.length - visibleBranches.length;
104
+ const branchLabel = remaining === 1 ? "branch" : "branches";
105
+ lines.push(`- ... ${remaining} more ${branchLabel} not shown.`);
106
+ }
107
+
108
+ lines.push("");
109
+ }
110
+
111
+ export function buildStatusView(
112
+ state: MemoryState,
113
+ branches: BranchManager,
114
+ projectDir: string,
115
+ options: StatusViewOptions = {}
116
+ ): string {
117
+ const compact = options.compact ?? false;
118
+ const roadmapCharLimit =
119
+ options.roadmapCharLimit ?? DEFAULT_COMPACT_ROADMAP_CHAR_LIMIT;
120
+ const branchLimit = options.branchLimit ?? DEFAULT_COMPACT_BRANCH_LIMIT;
121
+
122
+ const lines = ["# Memory Status", ""];
123
+
124
+ buildRoadmapSection(lines, projectDir, compact, roadmapCharLimit);
125
+
58
126
  lines.push(`Active branch: ${state.activeBranch}`, "");
59
127
 
60
128
  const logSizeBytes = branches.getLogSizeBytes(state.activeBranch);
@@ -67,21 +135,7 @@ function buildStatusView(
67
135
  );
68
136
  }
69
137
 
70
- const branchList = branches.listBranches();
71
- if (branchList.length > 0) {
72
- lines.push("## Branches", "");
73
-
74
- for (const name of branchList) {
75
- const latest = branches.getLatestCommit(name);
76
- const summary = latest
77
- ? extractCommitSummaryLine(latest)
78
- : "(no commits)";
79
- const marker = name === state.activeBranch ? " (active)" : "";
80
- lines.push(`- **${name}**${marker}: ${summary}`);
81
- }
82
-
83
- lines.push("");
84
- }
138
+ buildBranchesSection(lines, state, branches, compact, branchLimit);
85
139
 
86
140
  lines.push("## Deep Retrieval", "");
87
141
  lines.push("Use `read .memory/branches/<name>/commits.md` for full history.");
@@ -92,16 +146,3 @@ function buildStatusView(
92
146
 
93
147
  return lines.join("\n");
94
148
  }
95
-
96
- /**
97
- * Execute the memory_status tool — status overview.
98
- * Additional parameters are accepted for backward compatibility but ignored.
99
- */
100
- export function executeMemoryStatus(
101
- _params: MemoryStatusParams,
102
- state: MemoryState,
103
- branches: BranchManager,
104
- projectDir: string
105
- ): string {
106
- return buildStatusView(state, branches, projectDir);
107
- }
package/src/types.ts CHANGED
@@ -8,13 +8,6 @@ export interface OtaEntryInput {
8
8
  observations: string[];
9
9
  }
10
10
 
11
- export interface MemoryStatusParams {
12
- level?: string;
13
- branch?: string;
14
- commit?: string;
15
- segment?: string;
16
- }
17
-
18
11
  export interface SubagentResult {
19
12
  text: string;
20
13
  exitCode: number;
@@ -1,52 +0,0 @@
1
- import type { BranchManager } from "./branches.js";
2
- import { generateHash } from "./hash.js";
3
- import type { MemoryState } from "./state.js";
4
-
5
- interface MemoryMergeParams {
6
- branch: string;
7
- synthesis: string;
8
- }
9
-
10
- /**
11
- * Execute the memory_merge tool — synthesize a branch back into the current branch.
12
- * The agent should call memory_status BEFORE calling this.
13
- */
14
- export function executeMemoryMerge(
15
- params: MemoryMergeParams,
16
- state: MemoryState,
17
- branches: BranchManager
18
- ): string {
19
- const { branch: sourceBranch, synthesis } = params;
20
- const targetBranch = state.activeBranch;
21
-
22
- if (sourceBranch === targetBranch) {
23
- return `Cannot merge branch "${sourceBranch}" into itself.`;
24
- }
25
-
26
- if (!branches.branchExists(sourceBranch)) {
27
- return `Branch "${sourceBranch}" not found. Available branches: ${branches.listBranches().join(", ")}`;
28
- }
29
-
30
- const hash = generateHash();
31
- const timestamp = new Date().toISOString();
32
- const summary = `Merge from ${sourceBranch}`;
33
-
34
- const entry = [
35
- "",
36
- "---",
37
- "",
38
- `## Commit ${hash} | ${timestamp}`,
39
- "",
40
- `### Merge from ${sourceBranch}`,
41
- "",
42
- synthesis,
43
- "",
44
- ].join("\n");
45
-
46
- branches.appendCommit(targetBranch, entry);
47
-
48
- state.setLastCommit(targetBranch, hash, timestamp, summary);
49
- state.save();
50
-
51
- return `Merge commit ${hash} written to branch "${targetBranch}" (merged from "${sourceBranch}").`;
52
- }
@@ -1,29 +0,0 @@
1
- import type { BranchManager } from "./branches.js";
2
- import type { MemoryState } from "./state.js";
3
-
4
- interface MemorySwitchParams {
5
- branch: string;
6
- }
7
-
8
- /**
9
- * Execute the memory_switch tool — switch the active memory branch.
10
- */
11
- export function executeMemorySwitch(
12
- params: MemorySwitchParams,
13
- state: MemoryState,
14
- branches: BranchManager
15
- ): string {
16
- const { branch } = params;
17
-
18
- if (!branches.branchExists(branch)) {
19
- return `Branch "${branch}" not found. Available branches: ${branches.listBranches().join(", ")}`;
20
- }
21
-
22
- state.setActiveBranch(branch);
23
- state.save();
24
-
25
- const latest = branches.getLatestCommit(branch);
26
- const summary = latest ?? "No commits yet.";
27
-
28
- return `Switched to branch "${branch}".\n\n${summary}`;
29
- }