pi-brain 0.1.1
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/LICENSE +21 -0
- package/README.md +109 -0
- package/agents/memory-committer.md +49 -0
- package/package.json +79 -0
- package/skills/brain/SKILL.md +99 -0
- package/skills/brain/scripts/brain-init.sh +87 -0
- package/skills/brain/templates/agents-md.md +44 -0
- package/skills/brain/templates/root-agents-section.md +7 -0
- package/src/branches.ts +125 -0
- package/src/constants.ts +2 -0
- package/src/hash.ts +8 -0
- package/src/index.ts +310 -0
- package/src/memory-branch.ts +28 -0
- package/src/memory-commit.ts +58 -0
- package/src/memory-context.ts +107 -0
- package/src/memory-merge.ts +52 -0
- package/src/memory-switch.ts +29 -0
- package/src/ota-formatter.ts +31 -0
- package/src/ota-logger.ts +132 -0
- package/src/state.ts +143 -0
- package/src/subagent.ts +342 -0
- package/src/types.ts +22 -0
- package/src/yaml.ts +221 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AgentToolResult,
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
SessionBeforeCompactEvent,
|
|
9
|
+
} from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
|
|
12
|
+
import { BranchManager } from "./branches.js";
|
|
13
|
+
import { LOG_SIZE_WARNING_BYTES } from "./constants.js";
|
|
14
|
+
import { executeMemoryBranch } from "./memory-branch.js";
|
|
15
|
+
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";
|
|
19
|
+
import { formatOtaEntry } from "./ota-formatter.js";
|
|
20
|
+
import { extractOtaInput } from "./ota-logger.js";
|
|
21
|
+
import { MemoryState } from "./state.js";
|
|
22
|
+
import { extractCommitBlocks, spawnCommitter } from "./subagent.js";
|
|
23
|
+
|
|
24
|
+
const MEMORY_NOT_INITIALIZED_MESSAGE =
|
|
25
|
+
"Brain not initialized. Run brain-init.sh first.";
|
|
26
|
+
|
|
27
|
+
function createTextResult(text: string): AgentToolResult<unknown> {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text }],
|
|
30
|
+
details: {},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isMemoryReady(
|
|
35
|
+
state: MemoryState | null,
|
|
36
|
+
branchManager: BranchManager | null
|
|
37
|
+
): state is MemoryState {
|
|
38
|
+
return state !== null && branchManager !== null && state.isInitialized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function upsertCurrentSession(state: MemoryState, ctx: ExtensionContext): void {
|
|
42
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
43
|
+
if (!sessionFile) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
state.upsertSession(
|
|
48
|
+
sessionFile,
|
|
49
|
+
state.activeBranch,
|
|
50
|
+
new Date().toISOString()
|
|
51
|
+
);
|
|
52
|
+
state.save();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildCompactionReminder(
|
|
56
|
+
state: MemoryState,
|
|
57
|
+
branchManager: BranchManager
|
|
58
|
+
): string {
|
|
59
|
+
const branch = state.activeBranch;
|
|
60
|
+
const turns = branchManager.getLogTurnCount(branch);
|
|
61
|
+
const summary = state.lastCommit?.summary ?? "No commits yet";
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
`Brain memory active on branch "${branch}".`,
|
|
65
|
+
`${turns} uncommitted turn${turns === 1 ? "" : "s"} in .memory/branches/${branch}/log.md.`,
|
|
66
|
+
`Latest commit summary: ${summary}.`,
|
|
67
|
+
].join(" ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function appendCompactionReminder(
|
|
71
|
+
event: SessionBeforeCompactEvent,
|
|
72
|
+
reminder: string
|
|
73
|
+
): void {
|
|
74
|
+
event.customInstructions = event.customInstructions
|
|
75
|
+
? `${event.customInstructions}\n\n${reminder}`
|
|
76
|
+
: reminder;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveSkillPath(): string {
|
|
80
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
81
|
+
const currentDir = path.dirname(currentFile);
|
|
82
|
+
return path.resolve(currentDir, "../skills/brain");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default function activate(pi: ExtensionAPI) {
|
|
86
|
+
let state: MemoryState | null = null;
|
|
87
|
+
let branchManager: BranchManager | null = null;
|
|
88
|
+
|
|
89
|
+
function tryLoad(ctx: ExtensionContext): boolean {
|
|
90
|
+
if (isMemoryReady(state, branchManager)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const candidate = new MemoryState(ctx.cwd);
|
|
95
|
+
candidate.load();
|
|
96
|
+
|
|
97
|
+
if (!candidate.isInitialized) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
state = candidate;
|
|
102
|
+
branchManager = new BranchManager(ctx.cwd);
|
|
103
|
+
upsertCurrentSession(state, ctx);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
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
|
+
pi.registerTool({
|
|
133
|
+
name: "memory_branch",
|
|
134
|
+
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.",
|
|
164
|
+
parameters: Type.Object({
|
|
165
|
+
branch: Type.String({ description: "Target branch name" }),
|
|
166
|
+
}),
|
|
167
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
168
|
+
if (
|
|
169
|
+
!tryLoad(ctx) ||
|
|
170
|
+
!isMemoryReady(state, branchManager) ||
|
|
171
|
+
!branchManager
|
|
172
|
+
) {
|
|
173
|
+
return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const previousBranch = state.activeBranch;
|
|
177
|
+
const result = executeMemorySwitch(params, state, branchManager);
|
|
178
|
+
|
|
179
|
+
if (state.activeBranch !== previousBranch) {
|
|
180
|
+
upsertCurrentSession(state, ctx);
|
|
181
|
+
}
|
|
182
|
+
|
|
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));
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.registerTool({
|
|
212
|
+
name: "memory_commit",
|
|
213
|
+
label: "Memory Commit",
|
|
214
|
+
description: "Checkpoint a milestone in agent memory.",
|
|
215
|
+
parameters: Type.Object({
|
|
216
|
+
summary: Type.String({ description: "Short summary of this checkpoint" }),
|
|
217
|
+
update_roadmap: Type.Optional(Type.Boolean()),
|
|
218
|
+
}),
|
|
219
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
220
|
+
if (
|
|
221
|
+
!tryLoad(ctx) ||
|
|
222
|
+
!isMemoryReady(state, branchManager) ||
|
|
223
|
+
!branchManager
|
|
224
|
+
) {
|
|
225
|
+
return createTextResult(MEMORY_NOT_INITIALIZED_MESSAGE);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { task } = executeMemoryCommit(params, state, branchManager);
|
|
229
|
+
|
|
230
|
+
const result = await spawnCommitter(ctx.cwd, task, signal);
|
|
231
|
+
|
|
232
|
+
if (result.exitCode !== 0 || result.error) {
|
|
233
|
+
return createTextResult(
|
|
234
|
+
`Commit failed: ${result.error ?? "subagent exited with non-zero code"}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const commitContent = extractCommitBlocks(result.text);
|
|
239
|
+
if (!commitContent) {
|
|
240
|
+
return createTextResult(
|
|
241
|
+
"Commit failed: could not extract commit blocks from subagent response."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const message = finalizeMemoryCommit(
|
|
246
|
+
params.summary,
|
|
247
|
+
commitContent,
|
|
248
|
+
state,
|
|
249
|
+
branchManager
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return createTextResult(message);
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
pi.on("session_start", (_event, ctx) => {
|
|
257
|
+
state = new MemoryState(ctx.cwd);
|
|
258
|
+
state.load();
|
|
259
|
+
branchManager = new BranchManager(ctx.cwd);
|
|
260
|
+
|
|
261
|
+
if (!state.isInitialized) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
upsertCurrentSession(state, ctx);
|
|
266
|
+
|
|
267
|
+
const turnCount = branchManager.getLogTurnCount(state.activeBranch);
|
|
268
|
+
const logSizeBytes = branchManager.getLogSizeBytes(state.activeBranch);
|
|
269
|
+
|
|
270
|
+
if (logSizeBytes >= LOG_SIZE_WARNING_BYTES) {
|
|
271
|
+
const sizeKB = Math.round(logSizeBytes / 1024);
|
|
272
|
+
ctx.ui.notify(
|
|
273
|
+
`Brain: log.md is large (${sizeKB} KB). You should commit to distill this into structured memory.`,
|
|
274
|
+
"warning"
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
ctx.ui.notify(
|
|
278
|
+
`Brain active: branch "${state.activeBranch}" (${turnCount} uncommitted turn${turnCount === 1 ? "" : "s"}).`,
|
|
279
|
+
"info"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
pi.on("resources_discover", () => ({
|
|
285
|
+
skillPaths: [resolveSkillPath()],
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
pi.on("turn_end", (event) => {
|
|
289
|
+
if (!isMemoryReady(state, branchManager) || !branchManager) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const input = extractOtaInput(event);
|
|
294
|
+
if (!input) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const entry = formatOtaEntry(input);
|
|
299
|
+
branchManager.appendLog(state.activeBranch, entry);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
pi.on("session_before_compact", (event) => {
|
|
303
|
+
if (!isMemoryReady(state, branchManager) || !branchManager) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const reminder = buildCompactionReminder(state, branchManager);
|
|
308
|
+
appendCompactionReminder(event, reminder);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BranchManager } from "./branches.js";
|
|
2
|
+
import type { MemoryState } from "./state.js";
|
|
3
|
+
|
|
4
|
+
interface MemoryBranchParams {
|
|
5
|
+
name: string;
|
|
6
|
+
purpose: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute the memory_branch tool — create a new memory branch.
|
|
11
|
+
*/
|
|
12
|
+
export function executeMemoryBranch(
|
|
13
|
+
params: MemoryBranchParams,
|
|
14
|
+
state: MemoryState,
|
|
15
|
+
branches: BranchManager
|
|
16
|
+
): string {
|
|
17
|
+
const { name, purpose } = params;
|
|
18
|
+
|
|
19
|
+
if (branches.branchExists(name)) {
|
|
20
|
+
return `Branch "${name}" already exists. Use memory_switch to switch to it.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
branches.createBranch(name, purpose);
|
|
24
|
+
state.setActiveBranch(name);
|
|
25
|
+
state.save();
|
|
26
|
+
|
|
27
|
+
return `Created branch "${name}" and switched to it.\nPurpose: ${purpose}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { BranchManager } from "./branches.js";
|
|
2
|
+
import { generateHash } from "./hash.js";
|
|
3
|
+
import type { MemoryState } from "./state.js";
|
|
4
|
+
import { buildCommitterTask } from "./subagent.js";
|
|
5
|
+
|
|
6
|
+
interface MemoryCommitParams {
|
|
7
|
+
summary: string;
|
|
8
|
+
update_roadmap?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the subagent task string for commit distillation.
|
|
13
|
+
* The subagent reads log.md and commits.md itself.
|
|
14
|
+
*/
|
|
15
|
+
export function executeMemoryCommit(
|
|
16
|
+
params: MemoryCommitParams,
|
|
17
|
+
state: MemoryState,
|
|
18
|
+
_branches: BranchManager
|
|
19
|
+
): { task: string } {
|
|
20
|
+
const branch = state.activeBranch;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
task: buildCommitterTask(branch, params.summary),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Step 2: Write the agent's commit content to commits.md,
|
|
29
|
+
* clear log.md, and update state.
|
|
30
|
+
*/
|
|
31
|
+
export function finalizeMemoryCommit(
|
|
32
|
+
summary: string,
|
|
33
|
+
commitContent: string,
|
|
34
|
+
state: MemoryState,
|
|
35
|
+
branches: BranchManager
|
|
36
|
+
): string {
|
|
37
|
+
const branch = state.activeBranch;
|
|
38
|
+
const hash = generateHash();
|
|
39
|
+
const timestamp = new Date().toISOString();
|
|
40
|
+
|
|
41
|
+
const entry = [
|
|
42
|
+
"",
|
|
43
|
+
"---",
|
|
44
|
+
"",
|
|
45
|
+
`## Commit ${hash} | ${timestamp}`,
|
|
46
|
+
"",
|
|
47
|
+
commitContent,
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
|
|
51
|
+
branches.appendCommit(branch, entry);
|
|
52
|
+
branches.clearLog(branch);
|
|
53
|
+
|
|
54
|
+
state.setLastCommit(branch, hash, timestamp, summary);
|
|
55
|
+
state.save();
|
|
56
|
+
|
|
57
|
+
return `Commit ${hash} written to branch "${branch}".`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { BranchManager } from "./branches.js";
|
|
5
|
+
import { LOG_SIZE_WARNING_BYTES } from "./constants.js";
|
|
6
|
+
import type { MemoryState } from "./state.js";
|
|
7
|
+
import type { MemoryStatusParams } from "./types.js";
|
|
8
|
+
|
|
9
|
+
function extractCommitSummaryLine(commitEntry: string): string {
|
|
10
|
+
const marker = "### This Commit's Contribution";
|
|
11
|
+
const markerIndex = commitEntry.indexOf(marker);
|
|
12
|
+
|
|
13
|
+
if (markerIndex !== -1) {
|
|
14
|
+
const afterMarker = commitEntry.slice(markerIndex + marker.length);
|
|
15
|
+
const firstContentLine = afterMarker
|
|
16
|
+
.split("\n")
|
|
17
|
+
.map((line) => line.trim())
|
|
18
|
+
.find((line) => line.length > 0);
|
|
19
|
+
|
|
20
|
+
if (firstContentLine) {
|
|
21
|
+
return firstContentLine.slice(0, 100);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const headerMatch = /## Commit ([a-f0-9]+)/.exec(commitEntry);
|
|
26
|
+
if (headerMatch) {
|
|
27
|
+
return `commit ${headerMatch[1]}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return "(unknown)";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildStatusView(
|
|
34
|
+
state: MemoryState,
|
|
35
|
+
branches: BranchManager,
|
|
36
|
+
projectDir: string
|
|
37
|
+
): string {
|
|
38
|
+
const lines = ["# Memory Status", ""];
|
|
39
|
+
|
|
40
|
+
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 {
|
|
52
|
+
lines.push(
|
|
53
|
+
"No roadmap found. Create `.memory/main.md` to set project goals.",
|
|
54
|
+
""
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
lines.push(`Active branch: ${state.activeBranch}`, "");
|
|
59
|
+
|
|
60
|
+
const logSizeBytes = branches.getLogSizeBytes(state.activeBranch);
|
|
61
|
+
if (logSizeBytes >= LOG_SIZE_WARNING_BYTES) {
|
|
62
|
+
const sizeKB = Math.round(logSizeBytes / 1024);
|
|
63
|
+
lines.push(
|
|
64
|
+
`**Warning:** log.md is large (${sizeKB} KB). ` +
|
|
65
|
+
"You should commit to distill this into structured memory.",
|
|
66
|
+
""
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
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
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push("## Deep Retrieval", "");
|
|
87
|
+
lines.push("Use `read .memory/branches/<name>/commits.md` for full history.");
|
|
88
|
+
lines.push("Use `read .memory/branches/<name>/log.md` for OTA trace.");
|
|
89
|
+
lines.push("Use `read .memory/branches/<name>/metadata.yaml` for metadata.");
|
|
90
|
+
lines.push("Use `read .memory/main.md` for roadmap.");
|
|
91
|
+
lines.push("Use `read .memory/AGENTS.md` for protocol details.");
|
|
92
|
+
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats OTA (Observation-Thought-Action) log entries for log.md.
|
|
3
|
+
* Follows the Brain spec format with full fidelity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OtaEntryInput } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export type { OtaEntryInput } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export function formatOtaEntry(input: OtaEntryInput): string {
|
|
11
|
+
const lines = [
|
|
12
|
+
`## Turn ${input.turnNumber} | ${input.timestamp} | ${input.model}`,
|
|
13
|
+
"",
|
|
14
|
+
`**Thought**: ${input.thought}`,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
if (input.thinking) {
|
|
18
|
+
lines.push(`**Thinking**: ${input.thinking}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (input.actions.length > 0) {
|
|
22
|
+
lines.push(`**Action**: ${input.actions.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (input.observations.length > 0) {
|
|
26
|
+
lines.push(`**Observation**: ${input.observations.join("; ")}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
lines.push("");
|
|
30
|
+
return `${lines.join("\n")}\n`;
|
|
31
|
+
}
|