doer-agent 0.7.7 → 0.7.9
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/dist/agent-bundled-skills.js +3 -6
- package/dist/agent-notes-local.js +309 -0
- package/dist/agent-notes-rpc.js +106 -0
- package/dist/agent-runtime-utils.js +8 -1
- package/dist/agent.js +34 -1
- package/dist/daemon-mcp-server.js +125 -0
- package/package.json +2 -1
- package/runtime/skills/doer-agent-notes/SKILL.md +73 -0
|
@@ -46,8 +46,7 @@ async function removeLegacyManagedSkill(args) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
export async function ensureBundledDoerSkills(args) {
|
|
49
|
-
|
|
50
|
-
if (!sourceRootExists) {
|
|
49
|
+
if (!(await pathExists(args.bundledSkillsRoot))) {
|
|
51
50
|
return;
|
|
52
51
|
}
|
|
53
52
|
const entries = await readdir(args.bundledSkillsRoot, { withFileTypes: true });
|
|
@@ -55,8 +54,7 @@ export async function ensureBundledDoerSkills(args) {
|
|
|
55
54
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
56
55
|
continue;
|
|
57
56
|
}
|
|
58
|
-
const
|
|
59
|
-
const sourceSkillFile = path.join(sourceSkillDir, "SKILL.md");
|
|
57
|
+
const sourceSkillFile = path.join(args.bundledSkillsRoot, entry.name, "SKILL.md");
|
|
60
58
|
const targetSkillDir = path.join(args.codexHome, "skills", entry.name);
|
|
61
59
|
const targetSkillFile = path.join(targetSkillDir, "SKILL.md");
|
|
62
60
|
try {
|
|
@@ -65,9 +63,8 @@ export async function ensureBundledDoerSkills(args) {
|
|
|
65
63
|
args.onInfo?.(`bundled skill skipped name=${entry.name} reason=user-managed-target`);
|
|
66
64
|
continue;
|
|
67
65
|
}
|
|
68
|
-
const managedSource = withManagedMarker(source);
|
|
69
66
|
await mkdir(targetSkillDir, { recursive: true });
|
|
70
|
-
await writeFile(targetSkillFile,
|
|
67
|
+
await writeFile(targetSkillFile, withManagedMarker(source), "utf8");
|
|
71
68
|
await removeLegacyManagedSkill({ codexHome: args.codexHome, skillName: entry.name, onInfo: args.onInfo });
|
|
72
69
|
args.onInfo?.(`bundled skill synced name=${entry.name} path=.codex/skills/${entry.name}/SKILL.md`);
|
|
73
70
|
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { formatPatch, structuredPatch } from "diff";
|
|
5
|
+
const AGENT_NOTES_ROOT = ".doer-agent/notes";
|
|
6
|
+
const PATCHES_ROOT = `${AGENT_NOTES_ROOT}/patches`;
|
|
7
|
+
function resolveSystemTimeZone() {
|
|
8
|
+
const configured = process.env.TZ?.trim();
|
|
9
|
+
if (configured) {
|
|
10
|
+
return configured;
|
|
11
|
+
}
|
|
12
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
13
|
+
}
|
|
14
|
+
function resolveTimeZoneOffsetString(date, timeZone) {
|
|
15
|
+
try {
|
|
16
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
17
|
+
timeZone,
|
|
18
|
+
timeZoneName: "shortOffset",
|
|
19
|
+
hour: "2-digit",
|
|
20
|
+
minute: "2-digit",
|
|
21
|
+
hour12: false,
|
|
22
|
+
}).formatToParts(date);
|
|
23
|
+
const token = parts.find((part) => part.type === "timeZoneName")?.value || "GMT+0";
|
|
24
|
+
const matched = token.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
25
|
+
if (!matched) {
|
|
26
|
+
return "+00:00";
|
|
27
|
+
}
|
|
28
|
+
const hourRaw = matched[1] || "+0";
|
|
29
|
+
const minuteRaw = matched[2] || "00";
|
|
30
|
+
const sign = hourRaw.startsWith("-") ? "-" : "+";
|
|
31
|
+
const absHour = String(Math.abs(Number.parseInt(hourRaw, 10))).padStart(2, "0");
|
|
32
|
+
const absMinute = String(Math.abs(Number.parseInt(minuteRaw, 10))).padStart(2, "0");
|
|
33
|
+
return `${sign}${absHour}:${absMinute}`;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "+00:00";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function systemTimeParts(date) {
|
|
40
|
+
const timeZone = resolveSystemTimeZone();
|
|
41
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
42
|
+
timeZone,
|
|
43
|
+
year: "numeric",
|
|
44
|
+
month: "2-digit",
|
|
45
|
+
day: "2-digit",
|
|
46
|
+
hour: "2-digit",
|
|
47
|
+
minute: "2-digit",
|
|
48
|
+
second: "2-digit",
|
|
49
|
+
hour12: false,
|
|
50
|
+
hourCycle: "h23",
|
|
51
|
+
}).formatToParts(date);
|
|
52
|
+
const pick = (type) => {
|
|
53
|
+
return parts.find((part) => part.type === type)?.value || "00";
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
year: pick("year"),
|
|
57
|
+
month: pick("month"),
|
|
58
|
+
day: pick("day"),
|
|
59
|
+
hours: pick("hour"),
|
|
60
|
+
minutes: pick("minute"),
|
|
61
|
+
seconds: pick("second"),
|
|
62
|
+
milliseconds: String(date.getMilliseconds()).padStart(3, "0"),
|
|
63
|
+
offset: resolveTimeZoneOffsetString(date, timeZone),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function nowIso() {
|
|
67
|
+
const parts = systemTimeParts(new Date());
|
|
68
|
+
return `${parts.year}-${parts.month}-${parts.day}T${parts.hours}:${parts.minutes}:${parts.seconds}.${parts.milliseconds}${parts.offset}`;
|
|
69
|
+
}
|
|
70
|
+
function createTimeBasedPatchId(createdAt) {
|
|
71
|
+
const stamp = createdAt
|
|
72
|
+
.replace("T", "t")
|
|
73
|
+
.replace(/([+-])(\d{2}):?(\d{2})$/, (_match, sign, hours, minutes) => {
|
|
74
|
+
return `${sign === "-" ? "m" : "p"}${hours}${minutes}`;
|
|
75
|
+
})
|
|
76
|
+
.replace(/[-:.]/g, "")
|
|
77
|
+
.replace("Z", "z");
|
|
78
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
79
|
+
return `${stamp}-${random}`;
|
|
80
|
+
}
|
|
81
|
+
function patchRelPath(id, createdAt) {
|
|
82
|
+
const date = new Date(createdAt);
|
|
83
|
+
const { year, month } = systemTimeParts(Number.isNaN(date.getTime()) ? new Date() : date);
|
|
84
|
+
return path.posix.join(PATCHES_ROOT, year, month, `${id}.patch`);
|
|
85
|
+
}
|
|
86
|
+
export function sanitizeNoteId(value) {
|
|
87
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
88
|
+
const baseName = path.posix.basename(trimmed);
|
|
89
|
+
const withoutSlashes = baseName.replace(/[\\/]/g, "");
|
|
90
|
+
if (!withoutSlashes || withoutSlashes === "." || withoutSlashes === "..") {
|
|
91
|
+
throw new Error("note filename is required");
|
|
92
|
+
}
|
|
93
|
+
return withoutSlashes.endsWith(".md") ? withoutSlashes : `${withoutSlashes}.md`;
|
|
94
|
+
}
|
|
95
|
+
function workspacePath(workspaceRoot, relPath) {
|
|
96
|
+
const root = path.resolve(workspaceRoot);
|
|
97
|
+
const abs = path.resolve(root, relPath.replace(/^\/+/, ""));
|
|
98
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
99
|
+
throw new Error("path escapes workspace root");
|
|
100
|
+
}
|
|
101
|
+
return abs;
|
|
102
|
+
}
|
|
103
|
+
function noteRelPath(noteId) {
|
|
104
|
+
return path.posix.join(AGENT_NOTES_ROOT, sanitizeNoteId(noteId));
|
|
105
|
+
}
|
|
106
|
+
function sha256(text) {
|
|
107
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
108
|
+
}
|
|
109
|
+
async function readTextIfExists(abs) {
|
|
110
|
+
try {
|
|
111
|
+
return await readFile(abs, "utf8");
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function metadata(args) {
|
|
121
|
+
const rows = [
|
|
122
|
+
`# doer-note-patch-id: ${args.patchId}`,
|
|
123
|
+
`# created-at: ${args.createdAt}`,
|
|
124
|
+
`# operation: ${args.operation}`,
|
|
125
|
+
`# note-id: ${args.noteId}`,
|
|
126
|
+
`# note-path: ${noteRelPath(args.noteId)}`,
|
|
127
|
+
];
|
|
128
|
+
if (args.nextNoteId) {
|
|
129
|
+
rows.push(`# next-note-id: ${args.nextNoteId}`);
|
|
130
|
+
rows.push(`# next-note-path: ${noteRelPath(args.nextNoteId)}`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof args.oldText === "string" && typeof args.nextText === "string") {
|
|
133
|
+
rows.push(`# base-sha256: ${sha256(args.oldText)}`);
|
|
134
|
+
rows.push(`# next-sha256: ${sha256(args.nextText)}`);
|
|
135
|
+
}
|
|
136
|
+
return rows;
|
|
137
|
+
}
|
|
138
|
+
function changedPatch(args) {
|
|
139
|
+
const patch = structuredPatch(`a/${args.noteId}`, `b/${args.noteId}`, args.oldText, args.nextText, undefined, undefined, { context: 3 });
|
|
140
|
+
patch.isGit = true;
|
|
141
|
+
return `${metadata(args).join("\n")}\n${formatPatch(patch)}`;
|
|
142
|
+
}
|
|
143
|
+
function renamePatch(args) {
|
|
144
|
+
const patch = {
|
|
145
|
+
oldFileName: `a/${args.oldNoteId}`,
|
|
146
|
+
newFileName: `b/${args.nextNoteId}`,
|
|
147
|
+
oldHeader: undefined,
|
|
148
|
+
newHeader: undefined,
|
|
149
|
+
hunks: [],
|
|
150
|
+
isGit: true,
|
|
151
|
+
isRename: true,
|
|
152
|
+
};
|
|
153
|
+
return `${metadata({
|
|
154
|
+
patchId: args.patchId,
|
|
155
|
+
createdAt: args.createdAt,
|
|
156
|
+
operation: "rename",
|
|
157
|
+
noteId: args.oldNoteId,
|
|
158
|
+
nextNoteId: args.nextNoteId,
|
|
159
|
+
}).join("\n")}\n${formatPatch(patch)}`;
|
|
160
|
+
}
|
|
161
|
+
async function writePatch(workspaceRoot, patchId, createdAt, text) {
|
|
162
|
+
const relPath = patchRelPath(patchId, createdAt);
|
|
163
|
+
const abs = workspacePath(workspaceRoot, relPath);
|
|
164
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
165
|
+
await writeFile(abs, text, "utf8");
|
|
166
|
+
return relPath;
|
|
167
|
+
}
|
|
168
|
+
export async function listAgentNotesLocal(workspaceRoot) {
|
|
169
|
+
const rootAbs = workspacePath(workspaceRoot, AGENT_NOTES_ROOT);
|
|
170
|
+
const rows = await readdir(rootAbs, { withFileTypes: true }).catch(() => []);
|
|
171
|
+
const notes = await Promise.all(rows
|
|
172
|
+
.filter((row) => row.isFile() && row.name.endsWith(".md"))
|
|
173
|
+
.map(async (row) => {
|
|
174
|
+
const abs = path.join(rootAbs, row.name);
|
|
175
|
+
const entry = await stat(abs);
|
|
176
|
+
const id = sanitizeNoteId(row.name);
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
path: noteRelPath(id),
|
|
180
|
+
name: row.name,
|
|
181
|
+
size: entry.size,
|
|
182
|
+
mtimeMs: entry.mtimeMs,
|
|
183
|
+
};
|
|
184
|
+
}));
|
|
185
|
+
notes.sort((a, b) => {
|
|
186
|
+
return b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name);
|
|
187
|
+
});
|
|
188
|
+
return notes;
|
|
189
|
+
}
|
|
190
|
+
export async function getAgentNoteLocal(workspaceRoot, noteId) {
|
|
191
|
+
const id = sanitizeNoteId(noteId);
|
|
192
|
+
const content = await readTextIfExists(workspacePath(workspaceRoot, noteRelPath(id)));
|
|
193
|
+
return {
|
|
194
|
+
id,
|
|
195
|
+
path: noteRelPath(id),
|
|
196
|
+
name: id,
|
|
197
|
+
content,
|
|
198
|
+
totalSize: Buffer.byteLength(content, "utf8"),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export async function saveAgentNoteLocal(args) {
|
|
202
|
+
const noteId = sanitizeNoteId(args.noteId);
|
|
203
|
+
const noteAbs = workspacePath(args.workspaceRoot, noteRelPath(noteId));
|
|
204
|
+
const oldText = await readTextIfExists(noteAbs);
|
|
205
|
+
const nextText = args.content.replace(/\r\n/g, "\n");
|
|
206
|
+
if (oldText === nextText) {
|
|
207
|
+
return {
|
|
208
|
+
note: await getAgentNoteLocal(args.workspaceRoot, noteId),
|
|
209
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
210
|
+
patchPath: null,
|
|
211
|
+
patchId: null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const createdAt = nowIso();
|
|
215
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
216
|
+
const patchText = changedPatch({ noteId, oldText, nextText, patchId, createdAt, operation: "content" });
|
|
217
|
+
const patchPath = await writePatch(args.workspaceRoot, patchId, createdAt, patchText);
|
|
218
|
+
await mkdir(path.dirname(noteAbs), { recursive: true });
|
|
219
|
+
await writeFile(noteAbs, nextText, "utf8");
|
|
220
|
+
return {
|
|
221
|
+
note: await getAgentNoteLocal(args.workspaceRoot, noteId),
|
|
222
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
223
|
+
patchPath,
|
|
224
|
+
patchId,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export async function createAgentNoteLocal(workspaceRoot, name) {
|
|
228
|
+
const createdAt = nowIso();
|
|
229
|
+
const noteId = sanitizeNoteId(name);
|
|
230
|
+
const noteAbs = workspacePath(workspaceRoot, noteRelPath(noteId));
|
|
231
|
+
try {
|
|
232
|
+
await stat(noteAbs);
|
|
233
|
+
throw new Error("A note with that filename already exists");
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
241
|
+
const patchText = changedPatch({ noteId, oldText: "", nextText: "", patchId, createdAt, operation: "create" });
|
|
242
|
+
const patchPath = await writePatch(workspaceRoot, patchId, createdAt, patchText);
|
|
243
|
+
await mkdir(path.dirname(noteAbs), { recursive: true });
|
|
244
|
+
await writeFile(noteAbs, "", "utf8");
|
|
245
|
+
return {
|
|
246
|
+
note: await getAgentNoteLocal(workspaceRoot, noteId),
|
|
247
|
+
notes: await listAgentNotesLocal(workspaceRoot),
|
|
248
|
+
patchPath,
|
|
249
|
+
patchId,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
export async function renameAgentNoteLocal(args) {
|
|
253
|
+
const currentId = sanitizeNoteId(args.noteId);
|
|
254
|
+
const nextId = sanitizeNoteId(args.name);
|
|
255
|
+
if (currentId === nextId) {
|
|
256
|
+
return {
|
|
257
|
+
note: await getAgentNoteLocal(args.workspaceRoot, currentId),
|
|
258
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
259
|
+
patchPath: null,
|
|
260
|
+
patchId: null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const nextAbs = workspacePath(args.workspaceRoot, noteRelPath(nextId));
|
|
264
|
+
try {
|
|
265
|
+
await stat(nextAbs);
|
|
266
|
+
throw new Error("A note with that filename already exists");
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const currentAbs = workspacePath(args.workspaceRoot, noteRelPath(currentId));
|
|
274
|
+
const createdAt = nowIso();
|
|
275
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
276
|
+
const patchPath = await writePatch(args.workspaceRoot, patchId, createdAt, renamePatch({ oldNoteId: currentId, nextNoteId: nextId, patchId, createdAt }));
|
|
277
|
+
await mkdir(path.dirname(nextAbs), { recursive: true });
|
|
278
|
+
await rename(currentAbs, nextAbs);
|
|
279
|
+
return {
|
|
280
|
+
note: await getAgentNoteLocal(args.workspaceRoot, nextId),
|
|
281
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
282
|
+
patchPath,
|
|
283
|
+
patchId,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
export async function deleteAgentNoteLocal(args) {
|
|
287
|
+
const noteId = sanitizeNoteId(args.noteId);
|
|
288
|
+
const noteAbs = workspacePath(args.workspaceRoot, noteRelPath(noteId));
|
|
289
|
+
const oldText = await readTextIfExists(noteAbs);
|
|
290
|
+
const createdAt = nowIso();
|
|
291
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
292
|
+
const patchPath = await writePatch(args.workspaceRoot, patchId, createdAt, changedPatch({ noteId, oldText, nextText: "", patchId, createdAt, operation: "delete" }));
|
|
293
|
+
await rm(noteAbs, { force: true });
|
|
294
|
+
const notes = await listAgentNotesLocal(args.workspaceRoot);
|
|
295
|
+
return {
|
|
296
|
+
note: notes[0] ? await getAgentNoteLocal(args.workspaceRoot, notes[0].id) : null,
|
|
297
|
+
notes,
|
|
298
|
+
patchPath,
|
|
299
|
+
patchId,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export function agentNotesCapabilitiesLocal() {
|
|
303
|
+
return {
|
|
304
|
+
storage: "agent-workspace",
|
|
305
|
+
notesRoot: AGENT_NOTES_ROOT,
|
|
306
|
+
patchFormat: "git-diff",
|
|
307
|
+
patchesRoot: PATCHES_ROOT,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
import { agentNotesCapabilitiesLocal, createAgentNoteLocal, deleteAgentNoteLocal, getAgentNoteLocal, listAgentNotesLocal, renameAgentNoteLocal, saveAgentNoteLocal, } from "./agent-notes-local.js";
|
|
3
|
+
const notesRpcCodec = StringCodec();
|
|
4
|
+
function parseAction(value) {
|
|
5
|
+
if (value === "capabilities" ||
|
|
6
|
+
value === "list" ||
|
|
7
|
+
value === "get" ||
|
|
8
|
+
value === "create" ||
|
|
9
|
+
value === "save" ||
|
|
10
|
+
value === "rename" ||
|
|
11
|
+
value === "delete") {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
throw new Error("unsupported notes action");
|
|
15
|
+
}
|
|
16
|
+
function stringValue(value) {
|
|
17
|
+
return typeof value === "string" ? value : "";
|
|
18
|
+
}
|
|
19
|
+
async function executeNotesRpc(workspaceRoot, request) {
|
|
20
|
+
const action = parseAction(request.action);
|
|
21
|
+
if (action === "capabilities") {
|
|
22
|
+
return { ok: true, action, capabilities: agentNotesCapabilitiesLocal() };
|
|
23
|
+
}
|
|
24
|
+
if (action === "list") {
|
|
25
|
+
return { ok: true, action, notes: await listAgentNotesLocal(workspaceRoot) };
|
|
26
|
+
}
|
|
27
|
+
if (action === "get") {
|
|
28
|
+
const notes = await listAgentNotesLocal(workspaceRoot);
|
|
29
|
+
const noteId = stringValue(request.noteId) || notes[0]?.id || "";
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
action,
|
|
33
|
+
note: noteId ? await getAgentNoteLocal(workspaceRoot, noteId) : null,
|
|
34
|
+
notes,
|
|
35
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (action === "create") {
|
|
39
|
+
const name = stringValue(request.name).trim();
|
|
40
|
+
if (!name) {
|
|
41
|
+
throw new Error("name is required");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
action,
|
|
46
|
+
...(await createAgentNoteLocal(workspaceRoot, name)),
|
|
47
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (action === "save") {
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
action,
|
|
54
|
+
...(await saveAgentNoteLocal({
|
|
55
|
+
workspaceRoot,
|
|
56
|
+
noteId: stringValue(request.noteId),
|
|
57
|
+
content: stringValue(request.content),
|
|
58
|
+
})),
|
|
59
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (action === "rename") {
|
|
63
|
+
const name = stringValue(request.name).trim();
|
|
64
|
+
if (!name) {
|
|
65
|
+
throw new Error("name is required");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
action,
|
|
70
|
+
...(await renameAgentNoteLocal({
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
noteId: stringValue(request.noteId),
|
|
73
|
+
name,
|
|
74
|
+
})),
|
|
75
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
action,
|
|
81
|
+
...(await deleteAgentNoteLocal({
|
|
82
|
+
workspaceRoot,
|
|
83
|
+
noteId: stringValue(request.noteId),
|
|
84
|
+
})),
|
|
85
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function handleNotesRpcMessage(args) {
|
|
89
|
+
let payload = {};
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(notesRpcCodec.decode(args.msg.data));
|
|
92
|
+
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
93
|
+
throw new Error("agent id mismatch");
|
|
94
|
+
}
|
|
95
|
+
args.msg.respond(notesRpcCodec.encode(JSON.stringify(await executeNotesRpc(args.workspaceRoot, payload))));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
99
|
+
args.msg.respond(notesRpcCodec.encode(JSON.stringify({
|
|
100
|
+
ok: false,
|
|
101
|
+
action: typeof payload.action === "string" ? payload.action : "",
|
|
102
|
+
error: message,
|
|
103
|
+
})));
|
|
104
|
+
args.onError(`notes rpc failed action=${typeof payload.action === "string" ? payload.action : "unknown"} error=${message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -22,6 +22,9 @@ export function buildAgentSkillRpcSubject(userId, agentId) {
|
|
|
22
22
|
export function buildAgentFsRpcSubject(userId, agentId) {
|
|
23
23
|
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
24
24
|
}
|
|
25
|
+
export function buildAgentNotesRpcSubject(userId, agentId) {
|
|
26
|
+
return `doer.agent.notes.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
27
|
+
}
|
|
25
28
|
export function buildAgentDaemonRpcSubject(userId, agentId) {
|
|
26
29
|
return `doer.agent.daemon.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
27
30
|
}
|
|
@@ -168,7 +171,10 @@ export function writeRpcStream(requestId, stream, chunk) {
|
|
|
168
171
|
}
|
|
169
172
|
function resolveLogTimeZone() {
|
|
170
173
|
const configured = process.env.DOER_AGENT_LOG_TIMEZONE?.trim() || process.env.TZ?.trim();
|
|
171
|
-
|
|
174
|
+
if (configured && configured.length > 0) {
|
|
175
|
+
return configured;
|
|
176
|
+
}
|
|
177
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
172
178
|
}
|
|
173
179
|
function resolveTimeZoneOffsetString(date, timeZone) {
|
|
174
180
|
try {
|
|
@@ -207,6 +213,7 @@ export function formatLocalTimestamp(date = new Date()) {
|
|
|
207
213
|
minute: "2-digit",
|
|
208
214
|
second: "2-digit",
|
|
209
215
|
hour12: false,
|
|
216
|
+
hourCycle: "h23",
|
|
210
217
|
}).formatToParts(date);
|
|
211
218
|
const pick = (type) => {
|
|
212
219
|
return parts.find((part) => part.type === type)?.value || "00";
|
package/dist/agent.js
CHANGED
|
@@ -5,6 +5,8 @@ import { StringCodec } from "nats";
|
|
|
5
5
|
import { buildAgentSettingsEnvPatch, readAgentSettingsConfig, } from "./agent-settings.js";
|
|
6
6
|
import { handleFsRpcMessage } from "./agent-fs-rpc.js";
|
|
7
7
|
import { handleGitRpcMessage } from "./agent-git-rpc.js";
|
|
8
|
+
import { handleNotesRpcMessage } from "./agent-notes-rpc.js";
|
|
9
|
+
import { ensureBundledDoerSkills } from "./agent-bundled-skills.js";
|
|
8
10
|
import { subscribeToCodexAppRpc } from "./agent-codex-app-rpc.js";
|
|
9
11
|
import { createCodexAppServerManager } from "./codex-app-server-manager.js";
|
|
10
12
|
import { subscribeToDaemonRpc } from "./agent-daemon-rpc.js";
|
|
@@ -15,7 +17,7 @@ import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
|
|
|
15
17
|
import { subscribeToMaintenanceRpc } from "./agent-maintenance-rpc.js";
|
|
16
18
|
import { subscribeToHttpProxyRpc } from "./agent-http-proxy-rpc.js";
|
|
17
19
|
import { sendSignalToTaskProcess } from "./agent-task-execution.js";
|
|
18
|
-
import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
|
|
20
|
+
import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentNotesRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
|
|
19
21
|
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
20
22
|
import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
|
|
21
23
|
import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
|
|
@@ -23,6 +25,7 @@ const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
|
|
|
23
25
|
const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
24
26
|
const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
|
|
25
27
|
const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
|
|
28
|
+
const BUNDLED_SKILLS_ROOT = path.join(AGENT_PROJECT_DIR, "runtime", "skills");
|
|
26
29
|
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
27
30
|
const HEARTBEAT_FAILURE_THRESHOLD = 3;
|
|
28
31
|
const codexAppEventCodec = StringCodec();
|
|
@@ -108,6 +111,25 @@ function subscribeToFsRpc(args) {
|
|
|
108
111
|
});
|
|
109
112
|
writeAgentInfo(`fs rpc subscribed subject=${subject}`);
|
|
110
113
|
}
|
|
114
|
+
function subscribeToNotesRpc(args) {
|
|
115
|
+
const subject = buildAgentNotesRpcSubject(args.userId, args.agentId);
|
|
116
|
+
args.jetstream.nc.subscribe(subject, {
|
|
117
|
+
callback: (error, msg) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
writeAgentError(`notes rpc subscription error: ${message}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
void handleNotesRpcMessage({
|
|
124
|
+
msg,
|
|
125
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
126
|
+
agentId: args.agentId,
|
|
127
|
+
onError: writeAgentError,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
writeAgentInfo(`notes rpc subscribed subject=${subject}`);
|
|
132
|
+
}
|
|
111
133
|
function formatCodexAppNotificationParams(params) {
|
|
112
134
|
if (params === undefined) {
|
|
113
135
|
return "undefined";
|
|
@@ -249,6 +271,12 @@ async function main() {
|
|
|
249
271
|
jetstream.nc.publish(buildAgentCodexAppEventsSubject(userId, initialAgentId), codexAppEventCodec.encode(JSON.stringify(event)));
|
|
250
272
|
},
|
|
251
273
|
});
|
|
274
|
+
void ensureBundledDoerSkills({
|
|
275
|
+
bundledSkillsRoot: BUNDLED_SKILLS_ROOT,
|
|
276
|
+
codexHome: runtimeEnvHelpers.resolveCodexHomePath(),
|
|
277
|
+
onInfo: writeAgentInfo,
|
|
278
|
+
onError: writeAgentError,
|
|
279
|
+
});
|
|
252
280
|
subscribeToFsRpc({
|
|
253
281
|
jetstream,
|
|
254
282
|
serverBaseUrl,
|
|
@@ -256,6 +284,11 @@ async function main() {
|
|
|
256
284
|
agentId: initialAgentId,
|
|
257
285
|
agentToken,
|
|
258
286
|
});
|
|
287
|
+
subscribeToNotesRpc({
|
|
288
|
+
jetstream,
|
|
289
|
+
userId,
|
|
290
|
+
agentId: initialAgentId,
|
|
291
|
+
});
|
|
259
292
|
subscribeToDaemonRpc({
|
|
260
293
|
nc: jetstream.nc,
|
|
261
294
|
subject: buildAgentDaemonRpcSubject(userId, initialAgentId),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -6,6 +7,7 @@ import * as z from "zod/v4";
|
|
|
6
7
|
import { deleteAgentDaemonLocal, listAgentDaemonsLocal, readAgentDaemonLogsLocal, restartAgentDaemonLocal, startAgentDaemonLocal, stopAgentDaemonLocal, } from "./agent-daemon-rpc.js";
|
|
7
8
|
import { readAgentSettingsConfig } from "./agent-settings.js";
|
|
8
9
|
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
10
|
+
import { agentNotesCapabilitiesLocal, createAgentNoteLocal, deleteAgentNoteLocal, getAgentNoteLocal, listAgentNotesLocal, renameAgentNoteLocal, saveAgentNoteLocal, } from "./agent-notes-local.js";
|
|
9
11
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const AGENT_PROJECT_DIR = path.join(MODULE_DIR, "..");
|
|
11
13
|
function parseWorkspaceRoot(argv) {
|
|
@@ -17,6 +19,23 @@ function parseWorkspaceRoot(argv) {
|
|
|
17
19
|
function formatJson(value) {
|
|
18
20
|
return JSON.stringify(value, null, 2);
|
|
19
21
|
}
|
|
22
|
+
function runSearch(command, args, cwd) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
child.stdout.setEncoding("utf8");
|
|
28
|
+
child.stderr.setEncoding("utf8");
|
|
29
|
+
child.stdout.on("data", (chunk) => {
|
|
30
|
+
stdout += chunk;
|
|
31
|
+
});
|
|
32
|
+
child.stderr.on("data", (chunk) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
});
|
|
35
|
+
child.once("error", reject);
|
|
36
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
20
39
|
async function main() {
|
|
21
40
|
const workspaceRoot = parseWorkspaceRoot(process.argv.slice(2));
|
|
22
41
|
const runtimeEnvHelpers = createRuntimeEnvHelpers({
|
|
@@ -156,6 +175,112 @@ async function main() {
|
|
|
156
175
|
structuredContent: logs,
|
|
157
176
|
};
|
|
158
177
|
});
|
|
178
|
+
server.registerTool("notes_list", {
|
|
179
|
+
description: "List Doer note files in .doer-agent/notes.",
|
|
180
|
+
inputSchema: {},
|
|
181
|
+
}, async () => {
|
|
182
|
+
const notes = await listAgentNotesLocal(workspaceRoot);
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: formatJson({ notes, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
185
|
+
structuredContent: { notes, capabilities: agentNotesCapabilitiesLocal() },
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
server.registerTool("notes_read", {
|
|
189
|
+
description: "Read a Doer note file from .doer-agent/notes.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
noteId: z.string().optional().describe("Note filename. Defaults to the first note when omitted."),
|
|
192
|
+
},
|
|
193
|
+
}, async ({ noteId }) => {
|
|
194
|
+
const notes = await listAgentNotesLocal(workspaceRoot);
|
|
195
|
+
const targetNoteId = noteId?.trim() || notes[0]?.id || "";
|
|
196
|
+
const note = targetNoteId ? await getAgentNoteLocal(workspaceRoot, targetNoteId) : null;
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: formatJson({ note, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
199
|
+
structuredContent: { note, capabilities: agentNotesCapabilitiesLocal() },
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
server.registerTool("notes_create", {
|
|
203
|
+
description: "Create an empty Doer note and record a patch entry.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
name: z.string().min(1).describe("Note filename. .md is appended if omitted."),
|
|
206
|
+
},
|
|
207
|
+
}, async ({ name }) => {
|
|
208
|
+
const result = await createAgentNoteLocal(workspaceRoot, name);
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
211
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
server.registerTool("notes_save", {
|
|
215
|
+
description: "Save a Doer note and record a git-diff patch entry for the content change.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
noteId: z.string().min(1).describe("Note filename."),
|
|
218
|
+
content: z.string().describe("Full note content to save."),
|
|
219
|
+
},
|
|
220
|
+
}, async ({ noteId, content }) => {
|
|
221
|
+
const result = await saveAgentNoteLocal({ workspaceRoot, noteId, content });
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
224
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
server.registerTool("notes_rename", {
|
|
228
|
+
description: "Rename a Doer note and record a git-diff rename patch entry.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
noteId: z.string().describe("Current note filename."),
|
|
231
|
+
name: z.string().min(1).describe("New filename. .md is appended if omitted."),
|
|
232
|
+
},
|
|
233
|
+
}, async ({ noteId, name }) => {
|
|
234
|
+
const result = await renameAgentNoteLocal({ workspaceRoot, noteId, name });
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
237
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
server.registerTool("notes_delete", {
|
|
241
|
+
description: "Delete a Doer note and record a git-diff deletion patch entry.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
noteId: z.string().describe("Note filename to delete."),
|
|
244
|
+
},
|
|
245
|
+
}, async ({ noteId }) => {
|
|
246
|
+
const result = await deleteAgentNoteLocal({ workspaceRoot, noteId });
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
249
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
server.registerTool("notes_search", {
|
|
253
|
+
description: "Search Doer notes with ripgrep under .doer-agent/notes. Patches are excluded by default.",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
query: z.string().min(1).describe("Text or regex pattern to search for."),
|
|
256
|
+
includePatches: z.boolean().optional().describe("Search patch history too."),
|
|
257
|
+
fixedStrings: z.boolean().optional().describe("Treat query as a literal string."),
|
|
258
|
+
limit: z.number().int().min(1).max(200).optional().describe("Maximum matching lines to return."),
|
|
259
|
+
},
|
|
260
|
+
}, async ({ query, includePatches, fixedStrings, limit }) => {
|
|
261
|
+
const args = [
|
|
262
|
+
"--line-number",
|
|
263
|
+
"--column",
|
|
264
|
+
"--no-heading",
|
|
265
|
+
"--color=never",
|
|
266
|
+
fixedStrings === false ? null : "--fixed-strings",
|
|
267
|
+
"--glob",
|
|
268
|
+
"*.md",
|
|
269
|
+
includePatches ? "--glob" : null,
|
|
270
|
+
includePatches ? "*.patch" : null,
|
|
271
|
+
query,
|
|
272
|
+
".doer-agent/notes",
|
|
273
|
+
].filter((item) => Boolean(item));
|
|
274
|
+
const result = await runSearch("rg", args, workspaceRoot);
|
|
275
|
+
if (result.code !== 0 && result.code !== 1) {
|
|
276
|
+
throw new Error(result.stderr || "notes search failed");
|
|
277
|
+
}
|
|
278
|
+
const lines = result.stdout.split("\n").filter(Boolean).slice(0, limit ?? 50);
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: formatJson({ matches: lines, truncated: result.stdout.split("\n").filter(Boolean).length > lines.length }) }],
|
|
281
|
+
structuredContent: { matches: lines, truncated: result.stdout.split("\n").filter(Boolean).length > lines.length },
|
|
282
|
+
};
|
|
283
|
+
});
|
|
159
284
|
const transport = new StdioServerTransport();
|
|
160
285
|
await server.connect(transport);
|
|
161
286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doer-agent",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Reverse-polling agent runtime for doer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/agent.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"@openai/codex": "^0.133.0",
|
|
29
29
|
"@openai/codex-sdk": "^0.133.0",
|
|
30
|
+
"diff": "^9.0.0",
|
|
30
31
|
"nats": "^2.29.3",
|
|
31
32
|
"tar": "^7.5.15"
|
|
32
33
|
},
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doer-agent-notes
|
|
3
|
+
description: Use when working with Doer agent text notes, memo files, note patch history, or note CRUD/search tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Doer Agent Notes
|
|
7
|
+
|
|
8
|
+
Use the Doer notes tools for note CRUD whenever they are available. Do not edit `.doer-agent/notes/*.md` directly when a notes tool can perform the same operation, because note tools also record patch history.
|
|
9
|
+
|
|
10
|
+
## Storage
|
|
11
|
+
|
|
12
|
+
Current note files live directly under `.doer-agent/notes` as Markdown files.
|
|
13
|
+
|
|
14
|
+
- Notes: `.doer-agent/notes/{note-id}.md`
|
|
15
|
+
- There is no built-in default note filename. Users choose the filename when creating a note.
|
|
16
|
+
- `note-id` is the filename. If the extension is omitted, normalize it back to `.md`.
|
|
17
|
+
- Note content is free-form Markdown text.
|
|
18
|
+
- Titles, tags, and frontmatter are optional; do not require them for storage.
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
Prefer these tools when present:
|
|
23
|
+
|
|
24
|
+
- `notes_list`
|
|
25
|
+
- `notes_read`
|
|
26
|
+
- `notes_create`
|
|
27
|
+
- `notes_save`
|
|
28
|
+
- `notes_rename`
|
|
29
|
+
- `notes_delete`
|
|
30
|
+
- `notes_search`
|
|
31
|
+
|
|
32
|
+
## Patch History
|
|
33
|
+
|
|
34
|
+
Note change history is append-only and stored under `.doer-agent/notes/patches/YYYY/MM/*.patch`.
|
|
35
|
+
|
|
36
|
+
Patch files use git diff format with Doer metadata comment lines before the diff body.
|
|
37
|
+
|
|
38
|
+
For content changes, include:
|
|
39
|
+
|
|
40
|
+
- `# doer-note-patch-id`
|
|
41
|
+
- `# created-at`
|
|
42
|
+
- `# operation: content`
|
|
43
|
+
- `# note-id`
|
|
44
|
+
- `# note-path`
|
|
45
|
+
- `# base-sha256`
|
|
46
|
+
- `# next-sha256`
|
|
47
|
+
|
|
48
|
+
For filename changes, include:
|
|
49
|
+
|
|
50
|
+
- `# doer-note-patch-id`
|
|
51
|
+
- `# created-at`
|
|
52
|
+
- `# operation: rename`
|
|
53
|
+
- `# note-id`
|
|
54
|
+
- `# note-path`
|
|
55
|
+
- `# next-note-id`
|
|
56
|
+
- `# next-note-path`
|
|
57
|
+
|
|
58
|
+
For deletions, include:
|
|
59
|
+
|
|
60
|
+
- `# doer-note-patch-id`
|
|
61
|
+
- `# created-at`
|
|
62
|
+
- `# operation: delete`
|
|
63
|
+
- `# note-id`
|
|
64
|
+
- `# note-path`
|
|
65
|
+
- `# base-sha256`
|
|
66
|
+
- `# next-sha256`
|
|
67
|
+
|
|
68
|
+
## Principles
|
|
69
|
+
|
|
70
|
+
- The note source of truth is human-readable `.md` files.
|
|
71
|
+
- Change history is append-only `.patch` files.
|
|
72
|
+
- Search indexes, vector databases, and other derived stores are not source of truth.
|
|
73
|
+
- Derived data must be reproducible from `.md` and `.patch` files.
|