chapterhouse 0.13.0 → 0.14.0
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/api/route-coverage.test.js +1 -3
- package/dist/api/server.js +0 -2
- package/dist/api/server.test.js +0 -281
- package/dist/config.js +3 -85
- package/dist/config.test.js +5 -123
- package/dist/copilot/agents.js +25 -13
- package/dist/copilot/agents.test.js +10 -11
- package/dist/copilot/memory-coordinator.js +12 -227
- package/dist/copilot/memory-coordinator.test.js +31 -250
- package/dist/copilot/orchestrator.js +8 -66
- package/dist/copilot/orchestrator.test.js +9 -467
- package/dist/copilot/skills.js +15 -1
- package/dist/copilot/system-message.js +9 -15
- package/dist/copilot/system-message.test.js +9 -22
- package/dist/copilot/tools/index.js +3 -3
- package/dist/copilot/tools-deps.js +1 -1
- package/dist/copilot/tools.agent.test.js +6 -0
- package/dist/copilot/tools.inventory.test.js +1 -14
- package/dist/daemon.js +7 -9
- package/dist/memory/assets.js +33 -0
- package/dist/memory/domains.js +58 -0
- package/dist/memory/domains.test.js +47 -0
- package/dist/memory/git.js +66 -0
- package/dist/memory/git.test.js +32 -0
- package/dist/memory/history.js +19 -0
- package/dist/memory/hottier.js +32 -0
- package/dist/memory/hottier.test.js +33 -0
- package/dist/memory/index.js +5 -13
- package/dist/memory/instructions.js +17 -0
- package/dist/memory/manager.js +84 -0
- package/dist/memory/markdown.js +78 -0
- package/dist/memory/markdown.test.js +42 -0
- package/dist/memory/mutex.js +18 -0
- package/dist/memory/path-guard.js +26 -0
- package/dist/memory/path-guard.test.js +27 -0
- package/dist/memory/paths.js +12 -0
- package/dist/memory/reconcile.js +75 -0
- package/dist/memory/reconcile.test.js +50 -0
- package/dist/memory/scaffold.js +37 -0
- package/dist/memory/scaffold.test.js +52 -0
- package/dist/memory/tools/commit-wrapper.js +32 -0
- package/dist/memory/tools/domains.js +73 -0
- package/dist/memory/tools/domains.test.js +66 -0
- package/dist/memory/tools/git.js +52 -0
- package/dist/memory/tools/index.js +25 -0
- package/dist/memory/tools/read.js +101 -0
- package/dist/memory/tools/read.test.js +69 -0
- package/dist/memory/tools/search.js +103 -0
- package/dist/memory/tools/search.test.js +63 -0
- package/dist/memory/tools/sessions.js +45 -0
- package/dist/memory/tools/sessions.test.js +74 -0
- package/dist/memory/tools/shared.js +7 -0
- package/dist/memory/tools/write.js +116 -0
- package/dist/memory/tools/write.test.js +107 -0
- package/dist/memory/walk.js +39 -0
- package/dist/store/repositories/sessions.js +40 -0
- package/dist/wiki/consolidation.js +3 -31
- package/dist/wiki/consolidation.test.js +0 -19
- package/dist/wiki/frontmatter.js +18 -6
- package/dist/wiki/frontmatter.test.js +40 -0
- package/package.json +1 -1
- package/skills/system/evolve/SKILL.md +131 -0
- package/skills/system/foresight/SKILL.md +116 -0
- package/skills/system/history/SKILL.md +58 -0
- package/skills/system/housekeeping/SKILL.md +185 -0
- package/skills/system/reflect/SKILL.md +214 -0
- package/skills/system/scenario/SKILL.md +198 -0
- package/skills/system/setup/SKILL.md +113 -0
- package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
- package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
- package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
- package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/dist/api/routes/memory.js +0 -475
- package/dist/api/routes/memory.test.js +0 -108
- package/dist/copilot/tools/memory.js +0 -678
- package/dist/copilot/tools.memory.test.js +0 -590
- package/dist/memory/action-items.js +0 -100
- package/dist/memory/action-items.test.js +0 -83
- package/dist/memory/active-scope.js +0 -78
- package/dist/memory/active-scope.test.js +0 -80
- package/dist/memory/checkpoint-prompt.js +0 -71
- package/dist/memory/checkpoint.js +0 -274
- package/dist/memory/checkpoint.test.js +0 -275
- package/dist/memory/decisions.js +0 -54
- package/dist/memory/decisions.test.js +0 -92
- package/dist/memory/entities.js +0 -70
- package/dist/memory/entities.test.js +0 -65
- package/dist/memory/eot.js +0 -459
- package/dist/memory/eot.test.js +0 -949
- package/dist/memory/hooks.js +0 -149
- package/dist/memory/hooks.test.js +0 -325
- package/dist/memory/hot-tier.js +0 -283
- package/dist/memory/hot-tier.test.js +0 -275
- package/dist/memory/housekeeping-scheduler.js +0 -187
- package/dist/memory/housekeeping-scheduler.test.js +0 -236
- package/dist/memory/housekeeping.js +0 -497
- package/dist/memory/housekeeping.test.js +0 -410
- package/dist/memory/inbox.js +0 -83
- package/dist/memory/inbox.test.js +0 -178
- package/dist/memory/migration.js +0 -244
- package/dist/memory/migration.test.js +0 -108
- package/dist/memory/observations.js +0 -46
- package/dist/memory/observations.test.js +0 -86
- package/dist/memory/recall.js +0 -269
- package/dist/memory/recall.test.js +0 -265
- package/dist/memory/reflect.js +0 -273
- package/dist/memory/reflect.test.js +0 -256
- package/dist/memory/scope-lock.js +0 -26
- package/dist/memory/scope-lock.test.js +0 -118
- package/dist/memory/scopes.js +0 -89
- package/dist/memory/scopes.test.js +0 -176
- package/dist/memory/tiering.js +0 -223
- package/dist/memory/tiering.test.js +0 -323
- package/dist/memory/types.js +0 -2
- package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Write tools: cog_write, cog_edit, cog_append, cog_move. Each mutating tool
|
|
2
|
+
// runs through withMemoryWrite (write-lock + auto-commit).
|
|
3
|
+
// Ported from chgo's tools_write.go.
|
|
4
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
7
|
+
import { dirname } from "path";
|
|
8
|
+
import { resolveMemoryPath } from "../path-guard.js";
|
|
9
|
+
import { hasL0Header } from "../markdown.js";
|
|
10
|
+
import { withMemoryWrite } from "./commit-wrapper.js";
|
|
11
|
+
import { toolError } from "./shared.js";
|
|
12
|
+
export function createWriteTools(manager) {
|
|
13
|
+
const root = manager.paths.memoryRoot;
|
|
14
|
+
return [
|
|
15
|
+
defineTool("cog_write", {
|
|
16
|
+
description: "Create or overwrite a memory file. A .md file must begin with an " +
|
|
17
|
+
"<!-- L0: summary --> line within the first few lines.",
|
|
18
|
+
parameters: z.object({
|
|
19
|
+
path: z.string().describe("File path relative to the memory root"),
|
|
20
|
+
content: z.string().describe("Full file content"),
|
|
21
|
+
}),
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
const abs = resolveMemoryPath(root, args.path);
|
|
25
|
+
if (abs.endsWith(".md") && !hasL0Header(args.content)) {
|
|
26
|
+
return "Error: a .md memory file must begin with an <!-- L0: summary --> line";
|
|
27
|
+
}
|
|
28
|
+
return await withMemoryWrite(manager, `memory: write ${args.path}`, () => {
|
|
29
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
30
|
+
writeFileSync(abs, args.content, "utf8");
|
|
31
|
+
return `wrote ${args.path}`;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return toolError(err);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
defineTool("cog_edit", {
|
|
40
|
+
description: "Replace an exact, unique string in a memory file (edit in place).",
|
|
41
|
+
parameters: z.object({
|
|
42
|
+
path: z.string().describe("File path"),
|
|
43
|
+
old: z.string().describe("Exact text to replace — must occur exactly once"),
|
|
44
|
+
new: z.string().describe("Replacement text"),
|
|
45
|
+
}),
|
|
46
|
+
handler: async (args) => {
|
|
47
|
+
try {
|
|
48
|
+
const abs = resolveMemoryPath(root, args.path);
|
|
49
|
+
const data = readFileSync(abs, "utf8");
|
|
50
|
+
const occurrences = data.split(args.old).length - 1;
|
|
51
|
+
if (occurrences === 0) {
|
|
52
|
+
return `Error: old string not found in ${args.path}`;
|
|
53
|
+
}
|
|
54
|
+
if (occurrences > 1) {
|
|
55
|
+
return `Error: old string is not unique in ${args.path} — include more context`;
|
|
56
|
+
}
|
|
57
|
+
return await withMemoryWrite(manager, `memory: edit ${args.path}`, () => {
|
|
58
|
+
writeFileSync(abs, data.replace(args.old, args.new), "utf8");
|
|
59
|
+
return `edited ${args.path}`;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return toolError(err);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
defineTool("cog_append", {
|
|
68
|
+
description: "Append a block of text to a memory file (for append-only files like observations).",
|
|
69
|
+
parameters: z.object({
|
|
70
|
+
path: z.string().describe("File path"),
|
|
71
|
+
content: z.string().describe("Text to append"),
|
|
72
|
+
}),
|
|
73
|
+
handler: async (args) => {
|
|
74
|
+
try {
|
|
75
|
+
const abs = resolveMemoryPath(root, args.path);
|
|
76
|
+
const existing = readFileSync(abs, "utf8");
|
|
77
|
+
let next = existing;
|
|
78
|
+
if (next !== "" && !next.endsWith("\n"))
|
|
79
|
+
next += "\n";
|
|
80
|
+
next += args.content;
|
|
81
|
+
if (!next.endsWith("\n"))
|
|
82
|
+
next += "\n";
|
|
83
|
+
return await withMemoryWrite(manager, `memory: append ${args.path}`, () => {
|
|
84
|
+
writeFileSync(abs, next, "utf8");
|
|
85
|
+
return `appended to ${args.path}`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return toolError(err);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
defineTool("cog_move", {
|
|
94
|
+
description: "Move or rename a memory file (used for glacier archival).",
|
|
95
|
+
parameters: z.object({
|
|
96
|
+
from: z.string().describe("Source path"),
|
|
97
|
+
to: z.string().describe("Destination path"),
|
|
98
|
+
}),
|
|
99
|
+
handler: async (args) => {
|
|
100
|
+
try {
|
|
101
|
+
const from = resolveMemoryPath(root, args.from);
|
|
102
|
+
const to = resolveMemoryPath(root, args.to);
|
|
103
|
+
return await withMemoryWrite(manager, `memory: move ${args.from} -> ${args.to}`, () => {
|
|
104
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
105
|
+
renameSync(from, to);
|
|
106
|
+
return `moved ${args.from} -> ${args.to}`;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return toolError(err);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { gitAvailable, gitLog } from "../git.js";
|
|
6
|
+
import { MemoryManager } from "../manager.js";
|
|
7
|
+
import { createMemoryTools } from "./index.js";
|
|
8
|
+
const sandbox = join(process.cwd(), ".test-work", `mem-tools-write-${process.pid}`);
|
|
9
|
+
function findTool(tools, name) {
|
|
10
|
+
const found = tools.find((t) => t.name === name);
|
|
11
|
+
assert.ok(found, `tool ${name} not found`);
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
async function call(tool, args) {
|
|
15
|
+
const result = await tool.handler(args, {
|
|
16
|
+
sessionId: "t",
|
|
17
|
+
toolCallId: "t",
|
|
18
|
+
toolName: tool.name,
|
|
19
|
+
arguments: args,
|
|
20
|
+
});
|
|
21
|
+
return String(result);
|
|
22
|
+
}
|
|
23
|
+
async function setup() {
|
|
24
|
+
const mgr = new MemoryManager(sandbox);
|
|
25
|
+
await mgr.ensureReady();
|
|
26
|
+
return { tools: createMemoryTools(mgr), root: mgr.paths.memoryRoot };
|
|
27
|
+
}
|
|
28
|
+
test.beforeEach(() => {
|
|
29
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
30
|
+
mkdirSync(sandbox, { recursive: true });
|
|
31
|
+
});
|
|
32
|
+
test.after(() => rmSync(sandbox, { recursive: true, force: true }));
|
|
33
|
+
test("cog_write rejects a .md file with no L0 header", async () => {
|
|
34
|
+
if (!gitAvailable())
|
|
35
|
+
return;
|
|
36
|
+
const { tools } = await setup();
|
|
37
|
+
const out = await call(findTool(tools, "cog_write"), {
|
|
38
|
+
path: "personal/note.md",
|
|
39
|
+
content: "# No L0 here\nbody",
|
|
40
|
+
});
|
|
41
|
+
assert.match(out, /<!-- L0: summary -->/);
|
|
42
|
+
});
|
|
43
|
+
test("cog_write accepts a .md file with an L0 header and commits it", async () => {
|
|
44
|
+
if (!gitAvailable())
|
|
45
|
+
return;
|
|
46
|
+
const { tools, root } = await setup();
|
|
47
|
+
const before = (await gitLog(root, 50)).trim().split("\n").length;
|
|
48
|
+
const out = await call(findTool(tools, "cog_write"), {
|
|
49
|
+
path: "personal/note.md",
|
|
50
|
+
content: "<!-- L0: a note -->\n# Note\nbody",
|
|
51
|
+
});
|
|
52
|
+
assert.match(out, /wrote personal\/note\.md/);
|
|
53
|
+
assert.ok(existsSync(join(root, "personal/note.md")));
|
|
54
|
+
const log = await gitLog(root, 50);
|
|
55
|
+
assert.ok(log.includes("memory: write personal/note.md"));
|
|
56
|
+
assert.equal(log.trim().split("\n").length, before + 1, "exactly one new commit");
|
|
57
|
+
});
|
|
58
|
+
test("cog_edit errors on missing and non-unique matches, succeeds when unique", async () => {
|
|
59
|
+
if (!gitAvailable())
|
|
60
|
+
return;
|
|
61
|
+
const { tools } = await setup();
|
|
62
|
+
await call(findTool(tools, "cog_write"), {
|
|
63
|
+
path: "personal/note.md",
|
|
64
|
+
content: "<!-- L0: a note -->\nalpha\nbeta\nalpha",
|
|
65
|
+
});
|
|
66
|
+
assert.match(await call(findTool(tools, "cog_edit"), { path: "personal/note.md", old: "zeta", new: "x" }), /not found/);
|
|
67
|
+
assert.match(await call(findTool(tools, "cog_edit"), { path: "personal/note.md", old: "alpha", new: "x" }), /not unique/);
|
|
68
|
+
assert.match(await call(findTool(tools, "cog_edit"), { path: "personal/note.md", old: "beta", new: "BETA" }), /edited/);
|
|
69
|
+
});
|
|
70
|
+
test("cog_append normalizes newlines", async () => {
|
|
71
|
+
if (!gitAvailable())
|
|
72
|
+
return;
|
|
73
|
+
const { tools, root } = await setup();
|
|
74
|
+
await call(findTool(tools, "cog_write"), {
|
|
75
|
+
path: "personal/observations.md",
|
|
76
|
+
content: "<!-- L0: obs -->\nfirst line",
|
|
77
|
+
});
|
|
78
|
+
await call(findTool(tools, "cog_append"), { path: "personal/observations.md", content: "second line" });
|
|
79
|
+
assert.equal(readFileSync(join(root, "personal/observations.md"), "utf8"), "<!-- L0: obs -->\nfirst line\nsecond line\n");
|
|
80
|
+
});
|
|
81
|
+
test("cog_move relocates a file", async () => {
|
|
82
|
+
if (!gitAvailable())
|
|
83
|
+
return;
|
|
84
|
+
const { tools, root } = await setup();
|
|
85
|
+
await call(findTool(tools, "cog_write"), {
|
|
86
|
+
path: "personal/old.md",
|
|
87
|
+
content: "<!-- L0: x -->\nbody",
|
|
88
|
+
});
|
|
89
|
+
const out = await call(findTool(tools, "cog_move"), {
|
|
90
|
+
from: "personal/old.md",
|
|
91
|
+
to: "glacier/personal/old.md",
|
|
92
|
+
});
|
|
93
|
+
assert.match(out, /moved/);
|
|
94
|
+
assert.ok(!existsSync(join(root, "personal/old.md")));
|
|
95
|
+
assert.ok(existsSync(join(root, "glacier/personal/old.md")));
|
|
96
|
+
});
|
|
97
|
+
test("cog_write rejects a path escaping the memory root", async () => {
|
|
98
|
+
if (!gitAvailable())
|
|
99
|
+
return;
|
|
100
|
+
const { tools } = await setup();
|
|
101
|
+
const out = await call(findTool(tools, "cog_write"), {
|
|
102
|
+
path: "../escape.md",
|
|
103
|
+
content: "<!-- L0: x -->\nbody",
|
|
104
|
+
});
|
|
105
|
+
assert.match(out, /escapes the memory root/);
|
|
106
|
+
});
|
|
107
|
+
//# sourceMappingURL=write.test.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Walks the markdown files of the memory tree (or one domain subtree),
|
|
2
|
+
// skipping the .git directory. Ported from chgo's tools_read.go walkMemoryMarkdown.
|
|
3
|
+
import { readdirSync } from "fs";
|
|
4
|
+
import { join, relative, sep } from "path";
|
|
5
|
+
import { resolveMemoryPath } from "./path-guard.js";
|
|
6
|
+
/**
|
|
7
|
+
* Calls `fn(rel, abs)` for every `.md` file under the memory root, or under a
|
|
8
|
+
* domain subdirectory when `domain` is given. `rel` is slash-separated and
|
|
9
|
+
* relative to the memory root.
|
|
10
|
+
*/
|
|
11
|
+
export function walkMemoryMarkdown(memoryRoot, domain, fn) {
|
|
12
|
+
let base = memoryRoot;
|
|
13
|
+
if (domain && domain.trim() !== "") {
|
|
14
|
+
base = resolveMemoryPath(memoryRoot, domain);
|
|
15
|
+
}
|
|
16
|
+
const walk = (dir) => {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return; // directory may not exist yet
|
|
23
|
+
}
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const full = join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
if (entry.name === ".git")
|
|
28
|
+
continue;
|
|
29
|
+
walk(full);
|
|
30
|
+
}
|
|
31
|
+
else if (entry.name.endsWith(".md")) {
|
|
32
|
+
const rel = relative(memoryRoot, full).split(sep).join("/");
|
|
33
|
+
fn(rel, full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
walk(base);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=walk.js.map
|
|
@@ -229,6 +229,46 @@ export function getSessionMessages(sessionKey, limit, options = {}) {
|
|
|
229
229
|
return message;
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Group conversation_log rows into sessions for the cog_sessions tool. Rows are
|
|
234
|
+
* grouped by (session_key, run_id); `agent_completion` rows map to role
|
|
235
|
+
* "assistant"; empty-content rows are skipped. When `since` (an ISO-8601
|
|
236
|
+
* string) is given, only later turns are kept. Sessions are sorted
|
|
237
|
+
* newest-first; `limit` (0 = no cap) bounds the count.
|
|
238
|
+
*
|
|
239
|
+
* Note: conversation_log is pruned to the last 1000 rows, so only recent
|
|
240
|
+
* history is visible — adequate for the reflect skill's cursor model.
|
|
241
|
+
*/
|
|
242
|
+
export function getConversationSessions(since, limit = 0) {
|
|
243
|
+
const db = getDb();
|
|
244
|
+
const rows = db.prepare(`SELECT role, content, ts, session_key, run_id, agent_slug FROM conversation_log
|
|
245
|
+
WHERE role IN ('user', 'assistant', 'agent_completion')
|
|
246
|
+
ORDER BY session_key, run_id, id ASC`).all();
|
|
247
|
+
const sinceIso = since ? normalizeSqliteTsToIso(since) : undefined;
|
|
248
|
+
const byKey = new Map();
|
|
249
|
+
for (const r of rows) {
|
|
250
|
+
if (!r.content || r.content.trim() === "")
|
|
251
|
+
continue;
|
|
252
|
+
const at = normalizeSqliteTsToIso(r.ts);
|
|
253
|
+
if (sinceIso && at <= sinceIso)
|
|
254
|
+
continue;
|
|
255
|
+
const key = `${r.session_key}|${r.run_id ?? ""}`;
|
|
256
|
+
let session = byKey.get(key);
|
|
257
|
+
if (!session) {
|
|
258
|
+
session = { sessionKey: r.session_key, runId: r.run_id, startedAt: at, turns: [] };
|
|
259
|
+
byKey.set(key, session);
|
|
260
|
+
}
|
|
261
|
+
session.turns.push({
|
|
262
|
+
role: r.role === "agent_completion" ? "assistant" : r.role,
|
|
263
|
+
content: r.content,
|
|
264
|
+
ts: at,
|
|
265
|
+
agentSlug: r.agent_slug,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const sessions = Array.from(byKey.values()).filter((s) => s.turns.length > 0);
|
|
269
|
+
sessions.sort((a, b) => (a.startedAt < b.startedAt ? 1 : a.startedAt > b.startedAt ? -1 : 0));
|
|
270
|
+
return limit > 0 ? sessions.slice(0, limit) : sessions;
|
|
271
|
+
}
|
|
232
272
|
/**
|
|
233
273
|
* Append one event to agent_task_events and return the new event.
|
|
234
274
|
* Uses a transaction so seq is monotonically incremented.
|
|
@@ -3,8 +3,6 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
4
4
|
import { dirname, join, basename } from "node:path";
|
|
5
5
|
import { config } from "../config.js";
|
|
6
|
-
import { recordActionItem } from "../memory/action-items.js";
|
|
7
|
-
import { getScope } from "../memory/scopes.js";
|
|
8
6
|
import { getChapterhouseHome, resolveWikiRelativePath } from "../paths.js";
|
|
9
7
|
import { childLogger } from "../util/logger.js";
|
|
10
8
|
import { deletePage, listPages, readPage, writePage } from "./fs.js";
|
|
@@ -77,13 +75,14 @@ export async function runConsolidationWithDeps(db, partialDeps = {}) {
|
|
|
77
75
|
await deps.commitWikiChanges(runAt);
|
|
78
76
|
return result;
|
|
79
77
|
}
|
|
80
|
-
function createDefaultDeps(
|
|
78
|
+
function createDefaultDeps(_db) {
|
|
81
79
|
return {
|
|
82
80
|
now: () => new Date(),
|
|
83
81
|
synthesizeTruth: synthesizeTruthWithCopilot,
|
|
84
82
|
rebuildIndex: rebuildWikiIndex,
|
|
85
83
|
commitWikiChanges: async (runAt) => commitWikiChanges(runAt),
|
|
86
|
-
|
|
84
|
+
// Wiki consolidation no longer records action items into agent memory.
|
|
85
|
+
createActionItem: () => false,
|
|
87
86
|
truthRewriteBudget: config.pkbTruthRewriteBudget,
|
|
88
87
|
};
|
|
89
88
|
}
|
|
@@ -466,33 +465,6 @@ function notifyStaleSessions(db, deps, runAt) {
|
|
|
466
465
|
}
|
|
467
466
|
return created;
|
|
468
467
|
}
|
|
469
|
-
function createStaleSessionActionItem(db, input) {
|
|
470
|
-
const globalScope = getScope("global");
|
|
471
|
-
if (!globalScope) {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
const existing = recordDuplicateActionItemCheck(db, globalScope.id, input.title);
|
|
475
|
-
if (existing) {
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
recordActionItem({
|
|
479
|
-
scope_id: globalScope.id,
|
|
480
|
-
title: input.title,
|
|
481
|
-
detail: input.detail,
|
|
482
|
-
source: input.source,
|
|
483
|
-
tier: "warm",
|
|
484
|
-
});
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
|
-
function recordDuplicateActionItemCheck(db, scopeId, title) {
|
|
488
|
-
const row = db.prepare(`
|
|
489
|
-
SELECT 1
|
|
490
|
-
FROM mem_action_items
|
|
491
|
-
WHERE scope_id = ? AND title = ? AND status IN ('open', 'snoozed')
|
|
492
|
-
LIMIT 1
|
|
493
|
-
`).get(scopeId, title);
|
|
494
|
-
return Boolean(row);
|
|
495
|
-
}
|
|
496
468
|
async function synthesizeTruthWithCopilot(input) {
|
|
497
469
|
const token = config.copilotAuthToken;
|
|
498
470
|
if (!token) {
|
|
@@ -118,25 +118,6 @@ test("runConsolidation removes orphaned wiki_links rows", async () => {
|
|
|
118
118
|
assert.equal(result.linksRepaired, 1);
|
|
119
119
|
assert.equal(orphanCount.c, 0);
|
|
120
120
|
});
|
|
121
|
-
test("runConsolidation creates an action item for research sessions inactive for 7+ days", async () => {
|
|
122
|
-
const { dbModule, wikiFs, consolidation, memory } = await loadModules();
|
|
123
|
-
const db = dbModule.getDb();
|
|
124
|
-
wikiFs.ensureWikiStructure();
|
|
125
|
-
const globalScope = memory.getScope("global");
|
|
126
|
-
assert.ok(globalScope);
|
|
127
|
-
db.prepare(`
|
|
128
|
-
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
129
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
130
|
-
`).run("source-1", "url", "https://example.test/1", "Research note", "2026-05-01T10:00:00.000Z", "sources/source-1.md", "content", "[]", "active", "session-123", "Compiler research");
|
|
131
|
-
const result = await consolidation.runConsolidationWithDeps(db, {
|
|
132
|
-
now: () => new Date("2026-05-14T22:30:03.086Z"),
|
|
133
|
-
synthesizeTruth: async () => "unused",
|
|
134
|
-
commitWikiChanges: async () => false,
|
|
135
|
-
});
|
|
136
|
-
const actionItems = memory.listActionItems({ scope_id: globalScope.id, includeArchived: true });
|
|
137
|
-
assert.equal(result.staleSessionsNotified, 1);
|
|
138
|
-
assert.equal(actionItems.some((item) => item.title.includes("Research session 'Compiler research' has been inactive for 7+ days")), true);
|
|
139
|
-
});
|
|
140
121
|
test("runConsolidation caps truth rewrites before exceeding the LLM budget", async () => {
|
|
141
122
|
const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
|
|
142
123
|
const db = dbModule.getDb();
|
package/dist/wiki/frontmatter.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { normalizeWikiPath } from "./path-utils.js";
|
|
2
2
|
import { DEFAULT_SCHEMA, load } from "js-yaml";
|
|
3
3
|
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
4
|
-
|
|
4
|
+
// Markdown formatting that must not appear in a one-line plain-text summary.
|
|
5
|
+
// Detects *actual* formatting — paired or line-anchored — not lone characters:
|
|
6
|
+
// a bare underscore/asterisk inside a word (e.g. `ask_user_question`) is plain
|
|
7
|
+
// text, not emphasis, and must not be rejected.
|
|
8
|
+
const SUMMARY_MARKDOWN_RE = /(\*\*[^*]+\*\*|__[^_]+__|\*[^\s*][^*]*\*|(?<![A-Za-z0-9])_[^\s_][^_]*_(?![A-Za-z0-9])|`[^`]+`|~~[^~]+~~|^\s*#+\s|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|^\s*>)/m;
|
|
5
9
|
const FRONTMATTER_TEMPLATE = `---\ntitle: <title>\nsummary: <plain-text one-line summary, max 200 chars>\nupdated: YYYY-MM-DD\ntags: []\nrelated: []\n---`;
|
|
6
10
|
const PROJECT_RULES_HARD_DEFAULTS = {
|
|
7
11
|
auto_pr: true,
|
|
@@ -159,14 +163,22 @@ export function validateWikiFrontmatter(content, options = {}) {
|
|
|
159
163
|
}
|
|
160
164
|
else {
|
|
161
165
|
const summary = parsed.summary.trim();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const markdownMatch = summary.match(SUMMARY_MARKDOWN_RE);
|
|
167
|
+
let reason;
|
|
168
|
+
if (summary.length > 200) {
|
|
169
|
+
reason = `invalid 'summary' — it is ${summary.length} characters; the maximum is 200`;
|
|
170
|
+
}
|
|
171
|
+
else if (summary.includes("\n") || summary.includes("\r")) {
|
|
172
|
+
reason = "invalid 'summary' — it must be a single line";
|
|
173
|
+
}
|
|
174
|
+
else if (markdownMatch) {
|
|
175
|
+
reason = `invalid 'summary' — it must be plain text, but contains markdown formatting (${JSON.stringify(markdownMatch[0])})`;
|
|
176
|
+
}
|
|
177
|
+
if (reason) {
|
|
166
178
|
errors.push({
|
|
167
179
|
rule: "invalid-summary",
|
|
168
180
|
field: "summary",
|
|
169
|
-
message: formatFrontmatterMessage(
|
|
181
|
+
message: formatFrontmatterMessage(reason),
|
|
170
182
|
});
|
|
171
183
|
}
|
|
172
184
|
}
|
|
@@ -293,4 +293,44 @@ test("validateAndBackfillFrontmatter backfilled last_updated is valid ISO timest
|
|
|
293
293
|
const ts = match[1].trim();
|
|
294
294
|
assert.doesNotThrow(() => new Date(ts).toISOString(), `${ts} should be valid ISO timestamp`);
|
|
295
295
|
});
|
|
296
|
+
// ── Issue #458: summary markdown detection must not flag plain text ──────────
|
|
297
|
+
test("validateWikiFrontmatter accepts a plain-text summary containing technical identifiers", async () => {
|
|
298
|
+
const { validateWikiFrontmatter } = await loadFrontmatterModule();
|
|
299
|
+
const result = validateWikiFrontmatter(`---
|
|
300
|
+
title: Ask User Question
|
|
301
|
+
summary: Design notes for the ask_user_question tool, stored under ~/.chapterhouse
|
|
302
|
+
updated: 2026-05-19
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
# Ask User Question
|
|
306
|
+
`);
|
|
307
|
+
assert.equal(result.valid, true, JSON.stringify(result.errors));
|
|
308
|
+
});
|
|
309
|
+
test("validateWikiFrontmatter still rejects genuine markdown formatting in summaries", async () => {
|
|
310
|
+
const { validateWikiFrontmatter } = await loadFrontmatterModule();
|
|
311
|
+
const markdownSummaries = [
|
|
312
|
+
"**bold text**",
|
|
313
|
+
"__bold text__",
|
|
314
|
+
"an *italic* word",
|
|
315
|
+
"an _italic_ word",
|
|
316
|
+
"a `code` span",
|
|
317
|
+
"a ~~struck~~ phrase",
|
|
318
|
+
"[a link](https://example.com)",
|
|
319
|
+
];
|
|
320
|
+
for (const summary of markdownSummaries) {
|
|
321
|
+
const result = validateWikiFrontmatter(`---\ntitle: T\nsummary: ${summary}\nupdated: 2026-05-19\n---\n\n# T\n`);
|
|
322
|
+
assert.equal(result.valid, false, `expected "${summary}" to be rejected`);
|
|
323
|
+
assert.equal(result.errors[0]?.rule, "invalid-summary", `"${summary}" should fail invalid-summary`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
test("validateWikiFrontmatter invalid-summary message identifies the specific problem", async () => {
|
|
327
|
+
const { validateWikiFrontmatter } = await loadFrontmatterModule();
|
|
328
|
+
const markdown = validateWikiFrontmatter(`---\ntitle: T\nsummary: a \`code\` span\nupdated: 2026-05-19\n---\n\n# T\n`);
|
|
329
|
+
assert.equal(markdown.valid, false);
|
|
330
|
+
assert.match(markdown.errors[0].message, /markdown formatting/i);
|
|
331
|
+
assert.match(markdown.errors[0].message, /`code`/);
|
|
332
|
+
const tooLong = validateWikiFrontmatter(`---\ntitle: T\nsummary: ${"x".repeat(201)}\nupdated: 2026-05-19\n---\n\n# T\n`);
|
|
333
|
+
assert.equal(tooLong.valid, false);
|
|
334
|
+
assert.match(tooLong.errors[0].message, /200/);
|
|
335
|
+
});
|
|
296
336
|
//# sourceMappingURL=frontmatter.test.js.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: evolve
|
|
3
|
+
description: Use for a systems-level audit of the memory architecture's rules and tiers. Trigger when the user says evolve, system audit, audit yourself, or check your architecture.
|
|
4
|
+
---
|
|
5
|
+
**Tool enforcement:** All reads and writes to `memory/` MUST use cog_* tools (`cog_read`, `cog_write`, `cog_edit`, `cog_append`, `cog_move`, `cog_tree`, `cog_l0_scan`, `cog_l1_outline`, `cog_search`, `cog_stats`, `cog_domains`, `cog_sessions`, `cog_git`). **Do NOT use bash, cat, echo, find, or any shell command to read or write memory files.** If a cog_* tool call fails, note the error in self-observations and fall back to bash only for that specific operation.
|
|
6
|
+
|
|
7
|
+
**This is NOT /reflect.** Reflect = "what did I learn from interactions?" Evolve = "are the rules and architecture working?" **Evolve never touches memory content — it changes the rules that govern how content moves.**
|
|
8
|
+
|
|
9
|
+
## Domain
|
|
10
|
+
|
|
11
|
+
Systems architecture — process rules, skill design, tier effectiveness, pipeline health.
|
|
12
|
+
|
|
13
|
+
## Memory Files
|
|
14
|
+
|
|
15
|
+
Read FIRST — this is your continuity:
|
|
16
|
+
- `memory/cog-meta/evolve-log.md` — your run log
|
|
17
|
+
- `memory/cog-meta/evolve-observations.md` — architectural issues spotted
|
|
18
|
+
|
|
19
|
+
Architecture reference:
|
|
20
|
+
- `skills/system/housekeeping/SKILL.md` — housekeeping rules (read via `cog_read`)
|
|
21
|
+
- `skills/system/reflect/SKILL.md` — reflect rules (read via `cog_read`)
|
|
22
|
+
- The injected memory operating instructions — the platform equivalent of cog's `CLAUDE.md`. They are already in your context (injected into the system prompt); review them there, no `cog_read` needed.
|
|
23
|
+
|
|
24
|
+
Measure (don't edit content):
|
|
25
|
+
- `memory/hot-memory.md`
|
|
26
|
+
- `memory/cog-meta/patterns.md`
|
|
27
|
+
- Any domain satellite pattern files (e.g. `work/*/patterns.md`)
|
|
28
|
+
|
|
29
|
+
## Orientation (run FIRST, before any file reads)
|
|
30
|
+
|
|
31
|
+
Use these tool calls to see exactly what changed since the last run:
|
|
32
|
+
|
|
33
|
+
1. **What did housekeeping and reflect change recently?** — Call `cog_git` with op `diff`, `ref: "HEAD~1"`, and `paths: ["memory/"]` for a stat-style summary of recent changes across the memory tree.
|
|
34
|
+
2. **Detailed diff of architectural files** — Call `cog_git` with op `diff`, `ref: "HEAD~1"`, and `paths: ["memory/cog-meta/patterns.md", "memory/hot-memory.md"]` to see exactly what changed in the files you care about.
|
|
35
|
+
3. **What changed across recent runs?** — Call `cog_git` with op `log` to review the recent commit history. Each pipeline run commits its changes, so the log shows which runs touched memory and when — use it to scope which files to audit beyond the most recent diff.
|
|
36
|
+
4. **Current prompt-weight components** — Call `cog_stats` on `memory/hot-memory.md`, `memory/cog-meta/patterns.md`, and `memory/cog-meta/briefing-bridge.md` to get current file sizes without opening those files.
|
|
37
|
+
|
|
38
|
+
Use the git diffs to understand what housekeeping/reflect actually did, instead of re-reading entire files.
|
|
39
|
+
|
|
40
|
+
## Process
|
|
41
|
+
|
|
42
|
+
### 1. Architecture Review
|
|
43
|
+
|
|
44
|
+
Evaluate the structural design:
|
|
45
|
+
|
|
46
|
+
- **Tier design** — are the tiers (hot-memory → patterns → observations → glacier) well-defined?
|
|
47
|
+
- **Condensation pipeline** — is the flow working? Where does it leak or stall?
|
|
48
|
+
- **File naming and organization** — any files in wrong domains? Orphaned files?
|
|
49
|
+
- **Skill boundaries** — are housekeeping/reflect/evolve boundaries clean? Any drift?
|
|
50
|
+
|
|
51
|
+
### 2. Process Effectiveness Audit
|
|
52
|
+
|
|
53
|
+
Review the output of recent housekeeping and reflect runs by reading their skill files via `cog_read`:
|
|
54
|
+
|
|
55
|
+
**Housekeeping rules check** — read `skills/system/housekeeping/SKILL.md`:
|
|
56
|
+
- Did pruning priority order work? Or did it trim wrong things?
|
|
57
|
+
- Are glacier thresholds (50 obs, 10 action items) right?
|
|
58
|
+
- Is the 50-line hot-memory cap appropriate?
|
|
59
|
+
- Is entity format enforcement catching violations?
|
|
60
|
+
|
|
61
|
+
**Reflect rules check** — read `skills/system/reflect/SKILL.md`:
|
|
62
|
+
- Did condensation produce useful patterns, or noise?
|
|
63
|
+
- Did thread candidate detection work?
|
|
64
|
+
- Is reflect staying in its lane?
|
|
65
|
+
- Are patterns routing to the right file (core vs satellite)?
|
|
66
|
+
|
|
67
|
+
**Scorecard metrics** — measure and record in evolve-log:
|
|
68
|
+
- Core `patterns.md`: line count / 70, byte size / 5.5KB (target: ≤1.0)
|
|
69
|
+
- Satellite pattern files: list each with line count (soft cap: 30)
|
|
70
|
+
- Entity compression ratio: `(total entity lines across all files) / (total ### entries)` (target: ≤3.0)
|
|
71
|
+
- Hot-memory line counts vs caps
|
|
72
|
+
|
|
73
|
+
### 3. Rule Change Proposals
|
|
74
|
+
|
|
75
|
+
Based on findings, propose concrete rule changes. Don't fix content — fix the rules.
|
|
76
|
+
|
|
77
|
+
For each proposal:
|
|
78
|
+
- What problem does it solve?
|
|
79
|
+
- What evidence supports it?
|
|
80
|
+
- What's the risk?
|
|
81
|
+
- Is this a rule change (apply directly) or architecture change (propose for user review)?
|
|
82
|
+
|
|
83
|
+
**Apply low-risk rule changes directly** to the relevant skill files using `cog_edit`. Propose architecture changes for user review.
|
|
84
|
+
|
|
85
|
+
### 4. Route Content Issues
|
|
86
|
+
|
|
87
|
+
When you spot content problems during your audit, **don't fix them and don't defer them for yourself**. Route them explicitly.
|
|
88
|
+
|
|
89
|
+
Format in debrief:
|
|
90
|
+
```
|
|
91
|
+
→ housekeeping: entities.md at 290 lines, needs glacier pass
|
|
92
|
+
→ reflect: hot-memory missing thread link for X
|
|
93
|
+
→ reflect: patterns.md has stale snapshot data from Feb
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If the same content issue keeps appearing across runs, that's a **rule problem** — propose a rule change so housekeeping/reflect catch it themselves.
|
|
97
|
+
|
|
98
|
+
### 5. Generate Scorecard
|
|
99
|
+
|
|
100
|
+
Use `cog_write` to overwrite `memory/cog-meta/scorecard.md` with current metrics:
|
|
101
|
+
- Core patterns.md: line count / 70, byte size / 5.5KB (target: ≤1.0)
|
|
102
|
+
- Satellite pattern files: list each with line count (soft cap: 30)
|
|
103
|
+
- Entity compression ratio: `(total entity lines across all files) / (total ### entries)` — target ≤3.0
|
|
104
|
+
- Hot-memory line counts vs caps
|
|
105
|
+
- Briefing bridge SSOT compliance (% of lines with [[source]] links)
|
|
106
|
+
|
|
107
|
+
### 6. Write Observations & Update Log
|
|
108
|
+
|
|
109
|
+
**Observations** — Use `cog_append` to append to `memory/cog-meta/evolve-observations.md`:
|
|
110
|
+
- Format: `- YYYY-MM-DD [tag]: observation`
|
|
111
|
+
- Tags: bloat, staleness, redundancy, gap, architecture, opportunity, rule-drift, process-health
|
|
112
|
+
|
|
113
|
+
**Evolve Log** — Use `cog_append` to append to `memory/cog-meta/evolve-log.md`:
|
|
114
|
+
- Run number, process effectiveness findings, rule changes applied or proposed, deferred items
|
|
115
|
+
- Content issues routed (→ housekeeping / → reflect)
|
|
116
|
+
- Update "Next Run Priorities" section at top via `cog_edit`. **Only architecture/design items — never content work.**
|
|
117
|
+
|
|
118
|
+
### 7. Debrief
|
|
119
|
+
|
|
120
|
+
Concise summary:
|
|
121
|
+
- *Process health* — did housekeeping/reflect follow their rules?
|
|
122
|
+
- *Rule changes* — applied or proposed, with rationale
|
|
123
|
+
- *Routed issues* — content problems sent to housekeeping/reflect
|
|
124
|
+
- *Architecture notes* — structural observations
|
|
125
|
+
- *Next evolve* — top 3 architecture priorities
|
|
126
|
+
|
|
127
|
+
Keep it actionable. Numbers over narrative.
|
|
128
|
+
|
|
129
|
+
## Activation
|
|
130
|
+
|
|
131
|
+
Read `memory/cog-meta/evolve-log.md` and `memory/cog-meta/evolve-observations.md` FIRST for continuity. Then audit the system. You are the architect — you design the rules, you don't play by them.
|