pi-brain 0.1.2 → 0.1.5

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,7 +1,13 @@
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.
4
8
 
9
+ Credit: This project is my implementation for Pi of this paper [Git Context Controller: Manage the Context of LLM-based Agents like Git](https://arxiv.org/html/2508.00031v1)
10
+
5
11
  ## Getting Started
6
12
 
7
13
  ```bash
@@ -10,7 +16,7 @@ pi install npm:pi-brain
10
16
 
11
17
  Open pi in any project and say "initialize Brain" (or run `/skill:brain`). The agent creates `.memory/` and starts remembering.
12
18
 
13
- That's it. The agent decides when to commit, branch, and merge — you don't need to manage anything.
19
+ That's it. The agent decides when to commit, branch, and merge — you don't **need** to manage anything. However, you can always prompt the agent to remember something specific if you'd like.
14
20
 
15
21
  ## How It Works
16
22
 
@@ -43,9 +49,10 @@ LLM providers cache the prefix of each request. If the prefix changes between tu
43
49
  Brain avoids this entirely:
44
50
 
45
51
  - **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.
52
+ - **No per-turn prompt mutation** — Brain does not rewrite `systemPrompt` between turns. Status context is appended as message content, keeping the cached prefix stable.
53
+ - **Fixed tool definitions** — Tool schemas are static at startup. No tools are added or removed mid-conversation.
48
54
  - **Subagent isolation** — Commit distillation runs in a separate API call with its own cache. The main agent's cache is never touched.
55
+ - **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
56
 
50
57
  The result: Brain adds zero overhead to your prompt cache hit rate.
51
58
 
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.5",
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
@@ -60,6 +76,20 @@ already made, completed milestones, and planned work.
60
76
  A subagent handles commit distillation — it reads your `log.md` and prior commits,
61
77
  then produces the structured commit entry. You just provide a good `summary` string.
62
78
 
79
+ ## After Every Commit
80
+
81
+ **Update `.memory/main.md` to reflect the current state.** The roadmap is the first
82
+ thing a new session reads — if it's stale, every future session starts with a wrong
83
+ picture. After `memory_commit` returns, review and update:
84
+
85
+ - **Current State** section — reflect what's actually true now
86
+ - **Key Decisions Made** — add any new decisions from this commit
87
+ - **Milestones** — move completed items, add new planned ones
88
+
89
+ For trivial commits that don't change the project's state, decisions, or milestones
90
+ (e.g., minor refactors, typo fixes), you can pass `update_roadmap: false` to skip
91
+ the reminder. Most commits should update the roadmap.
92
+
63
93
  ## When to Branch
64
94
 
65
95
  - You want to explore an alternative approach without contaminating current thinking
@@ -72,28 +102,6 @@ then produces the structured commit entry. You just provide a good `summary` str
72
102
  - The branch's findings should inform the main line of thinking
73
103
  - Include what was learned even if the approach was abandoned
74
104
 
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
-
105
+ **Important:** Always review the source branch history BEFORE calling merge.
106
+ Use `read .memory/branches/<target>/commits.md` for full branch history.
81
107
  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
 
@@ -35,10 +40,22 @@ Each commit in `commits.md` has three blocks:
35
40
 
36
41
  The latest commit always contains a self-contained summary of the full branch history.
37
42
 
43
+ ## When to Commit
44
+
45
+ Call `memory_commit` when one of these is true:
46
+
47
+ - You reached a stable decision or understanding worth preserving.
48
+ - You finished an exploration branch with a clear conclusion.
49
+ - You are about to change direction significantly.
50
+ - You completed meaningful progress and are about to end the session.
51
+ - The extension warns that `log.md` is getting large.
52
+ - You are about to claim the task is complete or hand off to another agent.
53
+
38
54
  ## Conventions
39
55
 
40
56
  - **Agent-driven**: You decide when to commit, branch, and merge
41
57
  - **Decisions over details**: Capture "why", not "what" — git tracks file changes
42
58
  - **Rolling summaries**: Each commit re-synthesizes all prior progress
43
59
  - **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
60
+ - **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)
61
+ - **Keep main.md current**: After every commit, update `.memory/main.md` to reflect the new state — current state, decisions, and milestones. The roadmap is the first thing new sessions read; stale roadmaps cause wrong orientation. For trivial commits that don't change project state (e.g., minor refactors), pass `update_roadmap: false` to skip the reminder.
@@ -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,54 @@ 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.String({
131
+ enum: ["create", "switch", "merge"],
132
+ description: 'Action to perform: "create", "switch", or "merge"',
133
+ }),
134
+ name: Type.Optional(
135
+ Type.String({ description: "Branch name (required for create)" })
136
+ ),
137
+ purpose: Type.Optional(
138
+ Type.String({
139
+ description: "Why this branch exists (required for create)",
140
+ })
141
+ ),
142
+ branch: Type.Optional(
143
+ Type.String({
144
+ description: "Target branch (required for switch and merge)",
145
+ })
146
+ ),
147
+ synthesis: Type.Optional(
148
+ Type.String({ description: "Synthesized insight (required for merge)" })
149
+ ),
166
150
  }),
167
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
151
+ execute(_toolCallId, params, _signal, _onUpdate, ctx) {
168
152
  if (
169
153
  !tryLoad(ctx) ||
170
154
  !isMemoryReady(state, branchManager) ||
171
155
  !branchManager
172
156
  ) {
173
- return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
157
+ setBrainFooterStatus(ctx, state, branchManager);
158
+ return Promise.resolve(
159
+ createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE)
160
+ );
174
161
  }
175
162
 
176
163
  const previousBranch = state.activeBranch;
177
- const result = executeMemorySwitch(params, state, branchManager);
164
+ const result = executeMemoryBranch(params, state, branchManager, ctx.cwd);
178
165
 
179
166
  if (state.activeBranch !== previousBranch) {
180
167
  upsertCurrentSession(state, ctx);
181
168
  }
182
169
 
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));
170
+ setBrainFooterStatus(ctx, state, branchManager);
171
+ return Promise.resolve(createTextResult(result));
208
172
  },
209
173
  });
210
174
 
@@ -214,7 +178,12 @@ export default function activate(pi: ExtensionAPI) {
214
178
  description: "Checkpoint a milestone in agent memory.",
215
179
  parameters: Type.Object({
216
180
  summary: Type.String({ description: "Short summary of this checkpoint" }),
217
- update_roadmap: Type.Optional(Type.Boolean()),
181
+ update_roadmap: Type.Optional(
182
+ Type.Boolean({
183
+ description:
184
+ "Update .memory/main.md after commit. Defaults to true — set false to skip for trivial commits.",
185
+ })
186
+ ),
218
187
  }),
219
188
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
220
189
  if (
@@ -222,6 +191,7 @@ export default function activate(pi: ExtensionAPI) {
222
191
  !isMemoryReady(state, branchManager) ||
223
192
  !branchManager
224
193
  ) {
194
+ setBrainFooterStatus(ctx, state, branchManager);
225
195
  return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
226
196
  }
227
197
 
@@ -246,9 +216,12 @@ export default function activate(pi: ExtensionAPI) {
246
216
  params.summary,
247
217
  commitContent,
248
218
  state,
249
- branchManager
219
+ branchManager,
220
+ ctx.cwd,
221
+ params.update_roadmap
250
222
  );
251
223
 
224
+ setBrainFooterStatus(ctx, state, branchManager);
252
225
  return createTextResult(message);
253
226
  },
254
227
  });
@@ -257,14 +230,15 @@ export default function activate(pi: ExtensionAPI) {
257
230
  state = new MemoryState(ctx.cwd);
258
231
  state.load();
259
232
  branchManager = new BranchManager(ctx.cwd);
233
+ statusInjected = false;
260
234
 
261
235
  if (!state.isInitialized) {
236
+ setBrainFooterStatus(ctx, state, branchManager);
262
237
  return;
263
238
  }
264
239
 
265
240
  upsertCurrentSession(state, ctx);
266
241
 
267
- const turnCount = branchManager.getLogTurnCount(state.activeBranch);
268
242
  const logSizeBytes = branchManager.getLogSizeBytes(state.activeBranch);
269
243
 
270
244
  if (logSizeBytes >= LOG_SIZE_WARNING_BYTES) {
@@ -273,20 +247,66 @@ export default function activate(pi: ExtensionAPI) {
273
247
  `Brain: log.md is large (${sizeKB} KB). You should commit to distill this into structured memory.`,
274
248
  "warning"
275
249
  );
276
- } else {
277
- ctx.ui.notify(
278
- `Brain active: branch "${state.activeBranch}" (${turnCount} uncommitted turn${turnCount === 1 ? "" : "s"}).`,
279
- "info"
280
- );
281
250
  }
251
+
252
+ setBrainFooterStatus(ctx, state, branchManager);
253
+ });
254
+
255
+ pi.on("session_switch", (_event, ctx) => {
256
+ statusInjected = false;
257
+
258
+ state = new MemoryState(ctx.cwd);
259
+ state.load();
260
+ branchManager = new BranchManager(ctx.cwd);
261
+
262
+ if (state.isInitialized) {
263
+ upsertCurrentSession(state, ctx);
264
+ }
265
+
266
+ setBrainFooterStatus(ctx, state, branchManager);
267
+ });
268
+
269
+ pi.on("before_agent_start", (_event: BeforeAgentStartEvent, ctx) => {
270
+ if (statusInjected) {
271
+ return;
272
+ }
273
+
274
+ if (
275
+ !tryLoad(ctx) ||
276
+ !isMemoryReady(state, branchManager) ||
277
+ !branchManager
278
+ ) {
279
+ return;
280
+ }
281
+
282
+ statusInjected = true;
283
+
284
+ const status = buildStatusView(state, branchManager, ctx.cwd, {
285
+ compact: true,
286
+ roadmapCharLimit: BEFORE_AGENT_START_ROADMAP_CHAR_LIMIT,
287
+ branchLimit: BEFORE_AGENT_START_BRANCH_LIMIT,
288
+ });
289
+ return {
290
+ message: {
291
+ customType: "brain-status",
292
+ content: status,
293
+ display: true,
294
+ details: {},
295
+ },
296
+ };
297
+ });
298
+
299
+ pi.on("session_compact", () => {
300
+ statusInjected = false;
282
301
  });
283
302
 
284
303
  pi.on("resources_discover", () => ({
285
304
  skillPaths: [resolveSkillPath()],
286
305
  }));
287
306
 
288
- pi.on("turn_end", (event) => {
307
+ pi.on("turn_end", (event, ctx) => {
289
308
  if (!isMemoryReady(state, branchManager) || !branchManager) {
309
+ setBrainFooterStatus(ctx, state, branchManager);
290
310
  return;
291
311
  }
292
312
 
@@ -297,6 +317,7 @@ export default function activate(pi: ExtensionAPI) {
297
317
 
298
318
  const entry = formatOtaEntry(input);
299
319
  branchManager.appendLog(state.activeBranch, entry);
320
+ setBrainFooterStatus(ctx, state, branchManager);
300
321
  });
301
322
 
302
323
  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,15 @@ 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,
38
+ updateRoadmap?: boolean
36
39
  ): string {
37
40
  const branch = state.activeBranch;
38
41
  const hash = generateHash();
@@ -54,5 +57,15 @@ export function finalizeMemoryCommit(
54
57
  state.setLastCommit(branch, hash, timestamp, summary);
55
58
  state.save();
56
59
 
57
- return `Commit ${hash} written to branch "${branch}".`;
60
+ const resultText = `Commit ${hash} written to branch "${branch}".`;
61
+ const status = buildStatusView(state, branches, projectDir, {
62
+ compact: true,
63
+ });
64
+
65
+ const roadmapReminder =
66
+ updateRoadmap === false
67
+ ? ""
68
+ : "\n\n**Action required:** Update `.memory/main.md` to reflect this commit — current state, decisions, and milestones. The roadmap is the first thing new sessions read.";
69
+
70
+ return `${resultText}\n\n${status}${roadmapReminder}`;
58
71
  }
@@ -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
- }