chapterhouse 0.13.1 → 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 +13 -10
- 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/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,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, 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-domains-${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), mgr };
|
|
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_domains returns the manifest", async () => {
|
|
34
|
+
if (!gitAvailable())
|
|
35
|
+
return;
|
|
36
|
+
const { tools } = await setup();
|
|
37
|
+
const out = await call(findTool(tools, "cog_domains"), {});
|
|
38
|
+
assert.ok(out.includes("id: personal"));
|
|
39
|
+
assert.ok(out.includes("id: cog-meta"));
|
|
40
|
+
});
|
|
41
|
+
test("cog_domain_create appends a domain, scaffolds it, and commits", async () => {
|
|
42
|
+
if (!gitAvailable())
|
|
43
|
+
return;
|
|
44
|
+
const { tools, mgr } = await setup();
|
|
45
|
+
const out = await call(findTool(tools, "cog_domain_create"), {
|
|
46
|
+
id: "acme",
|
|
47
|
+
path: "work/acme",
|
|
48
|
+
type: "work",
|
|
49
|
+
label: "Acme work",
|
|
50
|
+
triggers: ["acme"],
|
|
51
|
+
});
|
|
52
|
+
assert.match(out, /created domain "acme"/);
|
|
53
|
+
assert.ok(existsSync(join(mgr.paths.memoryRoot, "work/acme/hot-memory.md")));
|
|
54
|
+
assert.ok(existsSync(join(mgr.paths.skillsDomains, "acme", "SKILL.md")));
|
|
55
|
+
assert.ok((await gitLog(mgr.paths.memoryRoot, 50)).includes("feat: add domain acme"));
|
|
56
|
+
const manifest = await call(findTool(tools, "cog_domains"), {});
|
|
57
|
+
assert.ok(manifest.includes("id: acme"));
|
|
58
|
+
});
|
|
59
|
+
test("cog_domain_create rejects a duplicate id and an invalid type", async () => {
|
|
60
|
+
if (!gitAvailable())
|
|
61
|
+
return;
|
|
62
|
+
const { tools } = await setup();
|
|
63
|
+
assert.match(await call(findTool(tools, "cog_domain_create"), { id: "personal", path: "personal", type: "personal", label: "dup" }), /already exists/);
|
|
64
|
+
assert.match(await call(findTool(tools, "cog_domain_create"), { id: "x", path: "x", type: "bogus", label: "bad" }), /invalid type/);
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=domains.test.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Git tool: cog_git — runs git operations on the memory repository.
|
|
2
|
+
// Ported from chgo's tools_git.go.
|
|
3
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { gitCommitAll, gitDiff, gitLog, gitStatus } from "../git.js";
|
|
6
|
+
import { withMemoryLock } from "./commit-wrapper.js";
|
|
7
|
+
import { toolError } from "./shared.js";
|
|
8
|
+
export function createGitTools(manager) {
|
|
9
|
+
const root = manager.paths.memoryRoot;
|
|
10
|
+
return [
|
|
11
|
+
defineTool("cog_git", {
|
|
12
|
+
description: "Run a git operation on the memory repository. op is one of: status, " +
|
|
13
|
+
"diff (optional ref and paths), log (optional limit), commit (requires message).",
|
|
14
|
+
parameters: z.object({
|
|
15
|
+
op: z.enum(["status", "diff", "log", "commit"]).describe("status | diff | log | commit"),
|
|
16
|
+
ref: z.string().optional().describe("Optional (diff): a git ref such as HEAD~1"),
|
|
17
|
+
paths: z.array(z.string()).optional().describe("Optional (diff): paths to scope the diff"),
|
|
18
|
+
limit: z.number().int().optional().describe("Optional (log): number of commits, default 15"),
|
|
19
|
+
message: z.string().optional().describe("Required (commit): the commit message"),
|
|
20
|
+
}),
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
try {
|
|
23
|
+
switch (args.op) {
|
|
24
|
+
case "status": {
|
|
25
|
+
const status = await gitStatus(root);
|
|
26
|
+
return status.trim() === "" ? "(clean)" : status;
|
|
27
|
+
}
|
|
28
|
+
case "diff": {
|
|
29
|
+
const diff = await gitDiff(root, args.ref, args.paths);
|
|
30
|
+
return diff.trim() === "" ? "(no diff)" : diff;
|
|
31
|
+
}
|
|
32
|
+
case "log":
|
|
33
|
+
return await gitLog(root, args.limit ?? 15);
|
|
34
|
+
case "commit": {
|
|
35
|
+
if (!args.message)
|
|
36
|
+
return "Error: commit requires a message";
|
|
37
|
+
const message = args.message;
|
|
38
|
+
await withMemoryLock(manager, () => gitCommitAll(root, message));
|
|
39
|
+
return `committed: ${message}`;
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
return `Error: unknown op`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return toolError(err);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Assembles the complete cog_* tool surface. Every name carries the cog_
|
|
2
|
+
// prefix so it cannot collide with an SDK built-in. Ported from chgo's tools.go.
|
|
3
|
+
import { createReadTools } from "./read.js";
|
|
4
|
+
import { createWriteTools } from "./write.js";
|
|
5
|
+
import { createSearchTools } from "./search.js";
|
|
6
|
+
import { createGitTools } from "./git.js";
|
|
7
|
+
import { createDomainTools } from "./domains.js";
|
|
8
|
+
import { createSessionsTool } from "./sessions.js";
|
|
9
|
+
/**
|
|
10
|
+
* Returns the 14 memory tools. When memory is unavailable (git missing, or not
|
|
11
|
+
* scaffolded) it returns an empty list so the daemon and agents keep working.
|
|
12
|
+
*/
|
|
13
|
+
export function createMemoryTools(manager) {
|
|
14
|
+
if (!manager || !manager.isReady())
|
|
15
|
+
return [];
|
|
16
|
+
return [
|
|
17
|
+
...createReadTools(manager),
|
|
18
|
+
...createWriteTools(manager),
|
|
19
|
+
...createSearchTools(manager),
|
|
20
|
+
...createGitTools(manager),
|
|
21
|
+
...createDomainTools(manager),
|
|
22
|
+
createSessionsTool(manager),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Read tools: cog_read, cog_l0_scan, cog_l1_outline, cog_tree.
|
|
2
|
+
// Ported from chgo's tools_read.go.
|
|
3
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { resolveMemoryPath } from "../path-guard.js";
|
|
7
|
+
import { walkMemoryMarkdown } from "../walk.js";
|
|
8
|
+
import { extractL0, extractLines, extractSection } from "../markdown.js";
|
|
9
|
+
import { toolError } from "./shared.js";
|
|
10
|
+
export function createReadTools(manager) {
|
|
11
|
+
const root = manager.paths.memoryRoot;
|
|
12
|
+
return [
|
|
13
|
+
defineTool("cog_read", {
|
|
14
|
+
description: "Read a memory file. Optionally extract one section by heading text, or a 1-based " +
|
|
15
|
+
"line range. Paths are relative to the memory root; a leading 'memory/' is accepted.",
|
|
16
|
+
parameters: z.object({
|
|
17
|
+
path: z.string().describe("File path, e.g. personal/hot-memory.md"),
|
|
18
|
+
section: z.string().optional().describe("Optional: heading text to extract, e.g. 'Current State'"),
|
|
19
|
+
start: z.number().int().optional().describe("Optional: 1-based start line"),
|
|
20
|
+
end: z.number().int().optional().describe("Optional: 1-based end line, inclusive"),
|
|
21
|
+
}),
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
const abs = resolveMemoryPath(root, args.path);
|
|
25
|
+
const content = readFileSync(abs, "utf8");
|
|
26
|
+
if (args.section)
|
|
27
|
+
return extractSection(content, args.section);
|
|
28
|
+
const start = args.start ?? 0;
|
|
29
|
+
const end = args.end ?? 0;
|
|
30
|
+
if (start > 0 || end > 0)
|
|
31
|
+
return extractLines(content, start, end);
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return toolError(err);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
defineTool("cog_l0_scan", {
|
|
40
|
+
description: "Scan one-line <!-- L0: --> summaries across the memory tree (the L0 retrieval tier). " +
|
|
41
|
+
"Optionally scope to a domain path.",
|
|
42
|
+
parameters: z.object({
|
|
43
|
+
domain: z.string().optional().describe("Optional: a domain path to scope the scan, e.g. personal"),
|
|
44
|
+
}),
|
|
45
|
+
handler: async (args) => {
|
|
46
|
+
try {
|
|
47
|
+
const lines = [];
|
|
48
|
+
walkMemoryMarkdown(root, args.domain, (rel, abs) => {
|
|
49
|
+
const l0 = extractL0(readFileSync(abs, "utf8"));
|
|
50
|
+
lines.push(`${rel} — ${l0 || "(no L0)"}`);
|
|
51
|
+
});
|
|
52
|
+
return lines.length === 0 ? "(no memory files)" : lines.join("\n") + "\n";
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return toolError(err);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
defineTool("cog_l1_outline", {
|
|
60
|
+
description: "List a memory file's section headings with line numbers (the L1 retrieval tier).",
|
|
61
|
+
parameters: z.object({
|
|
62
|
+
path: z.string().describe("File path"),
|
|
63
|
+
}),
|
|
64
|
+
handler: async (args) => {
|
|
65
|
+
try {
|
|
66
|
+
const abs = resolveMemoryPath(root, args.path);
|
|
67
|
+
const data = readFileSync(abs, "utf8");
|
|
68
|
+
const out = [];
|
|
69
|
+
data.split("\n").forEach((line, i) => {
|
|
70
|
+
const t = line.trim();
|
|
71
|
+
if (t.startsWith("#"))
|
|
72
|
+
out.push(`${i + 1}: ${t}`);
|
|
73
|
+
});
|
|
74
|
+
return out.length === 0 ? "(no headings)" : out.join("\n") + "\n";
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return toolError(err);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
defineTool("cog_tree", {
|
|
82
|
+
description: "List memory files. Optionally scope to a domain path.",
|
|
83
|
+
parameters: z.object({
|
|
84
|
+
domain: z.string().optional().describe("Optional: a domain path, e.g. work/acme"),
|
|
85
|
+
}),
|
|
86
|
+
handler: async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
const files = [];
|
|
89
|
+
walkMemoryMarkdown(root, args.domain, (rel) => {
|
|
90
|
+
files.push(rel);
|
|
91
|
+
});
|
|
92
|
+
return files.length === 0 ? "(no memory files)" : files.join("\n") + "\n";
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return toolError(err);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { gitAvailable } 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-read-${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
|
+
const tools = createMemoryTools(mgr);
|
|
27
|
+
await call(findTool(tools, "cog_write"), {
|
|
28
|
+
path: "personal/notes.md",
|
|
29
|
+
content: "<!-- L0: my notes -->\n# Notes\n## Current State\nstate body\n## Next\nnext body",
|
|
30
|
+
});
|
|
31
|
+
return tools;
|
|
32
|
+
}
|
|
33
|
+
test.beforeEach(() => {
|
|
34
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
35
|
+
mkdirSync(sandbox, { recursive: true });
|
|
36
|
+
});
|
|
37
|
+
test.after(() => rmSync(sandbox, { recursive: true, force: true }));
|
|
38
|
+
test("cog_read returns full content, a section, and a line range", async () => {
|
|
39
|
+
if (!gitAvailable())
|
|
40
|
+
return;
|
|
41
|
+
const tools = await setup();
|
|
42
|
+
const read = findTool(tools, "cog_read");
|
|
43
|
+
assert.match(await call(read, { path: "personal/notes.md" }), /state body[\s\S]*next body/);
|
|
44
|
+
assert.equal(await call(read, { path: "personal/notes.md", section: "current state" }), "## Current State\nstate body");
|
|
45
|
+
assert.equal(await call(read, { path: "personal/notes.md", start: 1, end: 2 }), "<!-- L0: my notes -->\n# Notes");
|
|
46
|
+
});
|
|
47
|
+
test("cog_l0_scan reports L0 summaries", async () => {
|
|
48
|
+
if (!gitAvailable())
|
|
49
|
+
return;
|
|
50
|
+
const tools = await setup();
|
|
51
|
+
const out = await call(findTool(tools, "cog_l0_scan"), { domain: "personal" });
|
|
52
|
+
assert.ok(out.includes("personal/notes.md — my notes"));
|
|
53
|
+
});
|
|
54
|
+
test("cog_l1_outline lists headings with line numbers", async () => {
|
|
55
|
+
if (!gitAvailable())
|
|
56
|
+
return;
|
|
57
|
+
const tools = await setup();
|
|
58
|
+
const out = await call(findTool(tools, "cog_l1_outline"), { path: "personal/notes.md" });
|
|
59
|
+
assert.ok(out.includes("# Notes"));
|
|
60
|
+
assert.ok(out.includes("## Current State"));
|
|
61
|
+
});
|
|
62
|
+
test("cog_tree lists memory files", async () => {
|
|
63
|
+
if (!gitAvailable())
|
|
64
|
+
return;
|
|
65
|
+
const tools = await setup();
|
|
66
|
+
const out = await call(findTool(tools, "cog_tree"), { domain: "personal" });
|
|
67
|
+
assert.ok(out.includes("personal/notes.md"));
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=read.test.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Search tools: cog_search, cog_stats. Ported from chgo's tools_search.go.
|
|
2
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readFileSync, statSync } from "fs";
|
|
5
|
+
import { resolveMemoryPath } from "../path-guard.js";
|
|
6
|
+
import { walkMemoryMarkdown } from "../walk.js";
|
|
7
|
+
import { toolError } from "./shared.js";
|
|
8
|
+
const SEARCH_MATCH_LIMIT = 200;
|
|
9
|
+
export function createSearchTools(manager) {
|
|
10
|
+
const root = manager.paths.memoryRoot;
|
|
11
|
+
return [
|
|
12
|
+
defineTool("cog_search", {
|
|
13
|
+
description: "Search memory files for a regular expression. Returns matching lines as " +
|
|
14
|
+
"path:line: text. Optionally scope to a domain path.",
|
|
15
|
+
parameters: z.object({
|
|
16
|
+
pattern: z.string().describe("A regular expression"),
|
|
17
|
+
domain: z.string().optional().describe("Optional: a domain path to scope the search"),
|
|
18
|
+
}),
|
|
19
|
+
handler: async (args) => {
|
|
20
|
+
try {
|
|
21
|
+
let re;
|
|
22
|
+
try {
|
|
23
|
+
re = new RegExp(args.pattern);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return `Error: invalid pattern: ${err.message}`;
|
|
27
|
+
}
|
|
28
|
+
const out = [];
|
|
29
|
+
let matches = 0;
|
|
30
|
+
walkMemoryMarkdown(root, args.domain, (rel, abs) => {
|
|
31
|
+
if (matches >= SEARCH_MATCH_LIMIT)
|
|
32
|
+
return;
|
|
33
|
+
const lines = readFileSync(abs, "utf8").split("\n");
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
if (matches >= SEARCH_MATCH_LIMIT)
|
|
36
|
+
break;
|
|
37
|
+
if (re.test(lines[i])) {
|
|
38
|
+
out.push(`${rel}:${i + 1}: ${lines[i].trim()}`);
|
|
39
|
+
matches++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (matches === 0)
|
|
44
|
+
return "(no matches)";
|
|
45
|
+
if (matches >= SEARCH_MATCH_LIMIT) {
|
|
46
|
+
out.push(`(truncated at ${SEARCH_MATCH_LIMIT} matches)`);
|
|
47
|
+
}
|
|
48
|
+
return out.join("\n") + "\n";
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return toolError(err);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
defineTool("cog_stats", {
|
|
56
|
+
description: "Report line count, byte size, list-entry count, and completed-entry count " +
|
|
57
|
+
"for memory files — used for archival and cap checks.",
|
|
58
|
+
parameters: z.object({
|
|
59
|
+
paths: z.array(z.string()).describe("Memory file paths to measure"),
|
|
60
|
+
}),
|
|
61
|
+
handler: async (args) => {
|
|
62
|
+
try {
|
|
63
|
+
if (args.paths.length === 0)
|
|
64
|
+
return "Error: paths is required";
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const rel of args.paths) {
|
|
67
|
+
let abs;
|
|
68
|
+
try {
|
|
69
|
+
abs = resolveMemoryPath(root, rel);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return toolError(err);
|
|
73
|
+
}
|
|
74
|
+
let data;
|
|
75
|
+
try {
|
|
76
|
+
data = readFileSync(abs, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
out.push(`${rel} — error: ${err.message}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const lines = data.split("\n");
|
|
83
|
+
let entries = 0;
|
|
84
|
+
let done = 0;
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.startsWith("- "))
|
|
87
|
+
entries++;
|
|
88
|
+
if (line.startsWith("- [x]") || line.startsWith("- [X]"))
|
|
89
|
+
done++;
|
|
90
|
+
}
|
|
91
|
+
const bytes = statSync(abs).size;
|
|
92
|
+
out.push(`${rel} — lines: ${lines.length}, bytes: ${bytes}, entries: ${entries}, done: ${done}`);
|
|
93
|
+
}
|
|
94
|
+
return out.join("\n") + "\n";
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return toolError(err);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { gitAvailable } 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-search-${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
|
+
const tools = createMemoryTools(mgr);
|
|
27
|
+
await call(findTool(tools, "cog_write"), {
|
|
28
|
+
path: "personal/observations.md",
|
|
29
|
+
content: "<!-- L0: obs -->\n- [x] shipped the release\n- [ ] write the changelog\n- ran the tests",
|
|
30
|
+
});
|
|
31
|
+
return tools;
|
|
32
|
+
}
|
|
33
|
+
test.beforeEach(() => {
|
|
34
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
35
|
+
mkdirSync(sandbox, { recursive: true });
|
|
36
|
+
});
|
|
37
|
+
test.after(() => rmSync(sandbox, { recursive: true, force: true }));
|
|
38
|
+
test("cog_search returns matching lines and reports no matches", async () => {
|
|
39
|
+
if (!gitAvailable())
|
|
40
|
+
return;
|
|
41
|
+
const tools = await setup();
|
|
42
|
+
const search = findTool(tools, "cog_search");
|
|
43
|
+
const hit = await call(search, { pattern: "changelog", domain: "personal" });
|
|
44
|
+
assert.ok(hit.includes("personal/observations.md:"));
|
|
45
|
+
assert.ok(hit.includes("write the changelog"));
|
|
46
|
+
assert.equal(await call(search, { pattern: "no-such-text", domain: "personal" }), "(no matches)");
|
|
47
|
+
});
|
|
48
|
+
test("cog_search reports an invalid pattern", async () => {
|
|
49
|
+
if (!gitAvailable())
|
|
50
|
+
return;
|
|
51
|
+
const tools = await setup();
|
|
52
|
+
assert.match(await call(findTool(tools, "cog_search"), { pattern: "[unclosed" }), /invalid pattern/);
|
|
53
|
+
});
|
|
54
|
+
test("cog_stats counts lines, entries, and completed entries", async () => {
|
|
55
|
+
if (!gitAvailable())
|
|
56
|
+
return;
|
|
57
|
+
const tools = await setup();
|
|
58
|
+
const out = await call(findTool(tools, "cog_stats"), { paths: ["personal/observations.md"] });
|
|
59
|
+
assert.ok(out.includes("personal/observations.md"));
|
|
60
|
+
assert.ok(out.includes("entries: 3"));
|
|
61
|
+
assert.ok(out.includes("done: 1"));
|
|
62
|
+
});
|
|
63
|
+
//# sourceMappingURL=search.test.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Sessions tool: cog_sessions — returns past conversation history for the
|
|
2
|
+
// reflect/history skills. Ported from chgo's history.go toolSessions.
|
|
3
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { toolError } from "./shared.js";
|
|
6
|
+
export function createSessionsTool(manager) {
|
|
7
|
+
return defineTool("cog_sessions", {
|
|
8
|
+
description: "Return past conversation history (the Chapterhouse equivalent of session " +
|
|
9
|
+
"transcripts). With no 'since', returns the most recent sessions. Note: " +
|
|
10
|
+
"history is bounded to recent conversations — advance the reflect cursor each run.",
|
|
11
|
+
parameters: z.object({
|
|
12
|
+
since: z.string().optional().describe("Optional: ISO-8601 timestamp; only turns after it are returned"),
|
|
13
|
+
limit: z.number().int().optional().describe("Optional: maximum number of sessions"),
|
|
14
|
+
}),
|
|
15
|
+
handler: async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
const source = manager.getSessionSource();
|
|
18
|
+
let since;
|
|
19
|
+
const raw = args.since?.trim();
|
|
20
|
+
if (raw && raw !== "never")
|
|
21
|
+
since = raw;
|
|
22
|
+
let limit = args.limit ?? 0;
|
|
23
|
+
if (!since && limit === 0) {
|
|
24
|
+
limit = 3; // cog: when the cursor is "never", read the recent 3 sessions
|
|
25
|
+
}
|
|
26
|
+
const sessions = await source.sessionHistory(since, limit);
|
|
27
|
+
if (sessions.length === 0)
|
|
28
|
+
return "(no sessions)";
|
|
29
|
+
let out = "";
|
|
30
|
+
for (const session of sessions) {
|
|
31
|
+
out += `Session ${session.conversationId} (${session.agentSlug}, started ${session.startedAt}):\n`;
|
|
32
|
+
for (const turn of session.turns) {
|
|
33
|
+
out += ` [${turn.role}] ${turn.text}\n`;
|
|
34
|
+
}
|
|
35
|
+
out += "\n";
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return toolError(err);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
const sandbox = join(process.cwd(), ".test-work", `mem-tools-sessions-${process.pid}`);
|
|
6
|
+
process.env.CHAPTERHOUSE_HOME = sandbox;
|
|
7
|
+
import { closeDb, getDb } from "../../store/connection.js";
|
|
8
|
+
import { getConversationSessions, logConversation } from "../../store/repositories/sessions.js";
|
|
9
|
+
import { gitAvailable } from "../git.js";
|
|
10
|
+
import { MemoryManager } from "../manager.js";
|
|
11
|
+
import { createMemoryTools } from "./index.js";
|
|
12
|
+
function findTool(tools, name) {
|
|
13
|
+
const found = tools.find((t) => t.name === name);
|
|
14
|
+
assert.ok(found, `tool ${name} not found`);
|
|
15
|
+
return found;
|
|
16
|
+
}
|
|
17
|
+
async function call(tool, args) {
|
|
18
|
+
const result = await tool.handler(args, {
|
|
19
|
+
sessionId: "t",
|
|
20
|
+
toolCallId: "t",
|
|
21
|
+
toolName: tool.name,
|
|
22
|
+
arguments: args,
|
|
23
|
+
});
|
|
24
|
+
return String(result);
|
|
25
|
+
}
|
|
26
|
+
function seedConversations() {
|
|
27
|
+
getDb();
|
|
28
|
+
logConversation("user", "hello there", "web", "session-one");
|
|
29
|
+
logConversation("assistant", "hi back", "web", "session-one", { agentSlug: "chapterhouse" });
|
|
30
|
+
logConversation("user", "a question", "web", "session-two");
|
|
31
|
+
logConversation("agent_completion", "the answer", "web", "session-two", { agentSlug: "coder" });
|
|
32
|
+
logConversation("system", "ignored noise", "web", "session-two");
|
|
33
|
+
}
|
|
34
|
+
test.beforeEach(() => {
|
|
35
|
+
closeDb();
|
|
36
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
37
|
+
mkdirSync(sandbox, { recursive: true });
|
|
38
|
+
});
|
|
39
|
+
test.after(() => {
|
|
40
|
+
closeDb();
|
|
41
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
test("getConversationSessions groups turns and maps agent_completion to assistant", () => {
|
|
44
|
+
seedConversations();
|
|
45
|
+
const sessions = getConversationSessions();
|
|
46
|
+
assert.equal(sessions.length, 2);
|
|
47
|
+
const one = sessions.find((s) => s.sessionKey === "session-one");
|
|
48
|
+
const two = sessions.find((s) => s.sessionKey === "session-two");
|
|
49
|
+
assert.ok(one && two);
|
|
50
|
+
assert.deepEqual(one.turns.map((t) => t.role), ["user", "assistant"]);
|
|
51
|
+
assert.deepEqual(two.turns.map((t) => [t.role, t.content]), [
|
|
52
|
+
["user", "a question"],
|
|
53
|
+
["assistant", "the answer"], // agent_completion mapped, system row dropped
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
test("getConversationSessions honors the since filter and limit", () => {
|
|
57
|
+
seedConversations();
|
|
58
|
+
assert.equal(getConversationSessions("2999-01-01T00:00:00Z").length, 0);
|
|
59
|
+
assert.equal(getConversationSessions("2000-01-01T00:00:00Z").length, 2);
|
|
60
|
+
assert.equal(getConversationSessions(undefined, 1).length, 1);
|
|
61
|
+
});
|
|
62
|
+
test("cog_sessions formats recent conversation history", async () => {
|
|
63
|
+
if (!gitAvailable())
|
|
64
|
+
return;
|
|
65
|
+
seedConversations();
|
|
66
|
+
const mgr = new MemoryManager(sandbox);
|
|
67
|
+
await mgr.ensureReady();
|
|
68
|
+
const tools = createMemoryTools(mgr);
|
|
69
|
+
const out = await call(findTool(tools, "cog_sessions"), {});
|
|
70
|
+
assert.ok(out.includes("Session session-one"));
|
|
71
|
+
assert.ok(out.includes("[user] hello there"));
|
|
72
|
+
assert.ok(out.includes("[assistant] the answer"));
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=sessions.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Shared helpers for the memory tools.
|
|
2
|
+
/** Formats an error as a readable, model-facing string. */
|
|
3
|
+
export function toolError(err) {
|
|
4
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5
|
+
return `Error: ${message}`;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=shared.js.map
|