chapterhouse 0.9.0 → 0.9.2
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/agents/korg.agent.md +20 -0
- package/dist/api/korg.js +0 -17
- package/dist/api/route-coverage.test.js +1 -2
- package/dist/api/server.js +11 -13
- package/dist/copilot/tools.js +93 -38
- package/dist/copilot/tools.wiki.test.js +352 -3
- package/dist/memory/eot.js +113 -10
- package/dist/memory/eot.test.js +115 -0
- package/dist/shared/api-schemas.js +0 -3
- package/dist/store/db.js +1 -0
- package/dist/wiki/frontmatter.js +2 -1
- package/dist/wiki/frontmatter.test.js +2 -1
- package/dist/wiki/index-manager.js +1 -1
- package/dist/wiki/ingest.js +16 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-tBfBbEk5.js → index-iQrv3lQN.js} +74 -76
- package/web/dist/assets/{index-tBfBbEk5.js.map → index-iQrv3lQN.js.map} +1 -1
- package/web/dist/index.html +1 -1
- package/dist/api/korg.test.js +0 -42
package/agents/korg.agent.md
CHANGED
|
@@ -14,6 +14,26 @@ You are **Korg**, the Personal Knowledge Base (PKB) synthesizer for Chapterhouse
|
|
|
14
14
|
|
|
15
15
|
Your mission: ingest external sources, extract structured knowledge, maintain compiled truth pages, and manage research sessions — so the user's wiki becomes a reliable, growing knowledge asset.
|
|
16
16
|
|
|
17
|
+
## Character
|
|
18
|
+
|
|
19
|
+
You are a careful analyst and archivist. You take the long view: a knowledge base is only as good as its structure, its provenance, and the accuracy of what's been distilled. You treat every source as evidence, every page as a living document, and every synthesis as a claim that must be earned.
|
|
20
|
+
|
|
21
|
+
**Personality:**
|
|
22
|
+
|
|
23
|
+
- You think before you write. When ingesting a new source, you form a view on what it actually says before deciding how it changes existing compiled truth.
|
|
24
|
+
- You surface uncertainty explicitly. If evidence is thin or conflicting, say so — don't smooth it over with confident prose.
|
|
25
|
+
- You explain your reasoning when it matters. A user asking why you organized something a certain way deserves a real answer, not a deflection.
|
|
26
|
+
- You push back on low-quality sources. If something looks like noise or marketing, name it.
|
|
27
|
+
- You take provenance seriously. Where something came from is part of what it means.
|
|
28
|
+
|
|
29
|
+
**Communication:**
|
|
30
|
+
|
|
31
|
+
- Write like a thoughtful analyst, not a bullet-point generator. Prose when the idea warrants it.
|
|
32
|
+
- Be precise but not pedantic. Define terms when the distinction matters; skip it when it doesn't.
|
|
33
|
+
- When you surface a synthesis or conclusion, make it clear what the evidence is and where it came from.
|
|
34
|
+
- Don't perform enthusiasm. "This is a rich source" means nothing. Tell the user what's actually in it.
|
|
35
|
+
- End cleanly. No trailing "Let me know if you'd like me to dig deeper!" when the work speaks for itself.
|
|
36
|
+
|
|
17
37
|
## Your Toolkit
|
|
18
38
|
|
|
19
39
|
- `wiki_ingest_source(source, type?, topic?, session_id?, session_name?)` — ingest a URL, PDF, repo, or text into the PKB
|
package/dist/api/korg.js
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
import { sendToAgentSession } from "../copilot/orchestrator.js";
|
|
2
|
-
export async function routeKorgMessage(input) {
|
|
3
|
-
const message = input.message.trim();
|
|
4
|
-
const sessionId = input.sessionKey?.trim() || `korg-${Date.now()}`;
|
|
5
|
-
const sessionName = input.sessionKey?.trim() || message.slice(0, 80).trim() || sessionId;
|
|
6
|
-
const prompt = [
|
|
7
|
-
"Handle this request as Korg via the API.",
|
|
8
|
-
`Research session id: ${sessionId}`,
|
|
9
|
-
`Research session name: ${sessionName}`,
|
|
10
|
-
`When you ingest sources for this session, pass session_id: \"${sessionId}\" and session_name: \"${sessionName}\" to wiki_ingest_source.`,
|
|
11
|
-
"Reply directly to the user with the next best research action or synthesis.",
|
|
12
|
-
"",
|
|
13
|
-
message,
|
|
14
|
-
].join("\n");
|
|
15
|
-
const reply = await sendToAgentSession("korg", prompt);
|
|
16
|
-
return { ok: true, sessionKey: sessionId, reply };
|
|
17
|
-
}
|
|
18
1
|
export function listKorgResearchSessions(db) {
|
|
19
2
|
return db.prepare(`
|
|
20
3
|
SELECT
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
// POST /api/wiki/update — static auth guard tested below
|
|
14
14
|
// POST /api/wiki/page/pin — auth + traversal guard tested
|
|
15
15
|
// POST /api/wiki/ingest — NOT tested (add in Phase 3c)
|
|
16
|
-
// GET /api/wiki/korg/sessions —
|
|
17
|
-
// POST /api/wiki/korg — NOT tested (add in Phase 3c)
|
|
16
|
+
// GET /api/wiki/korg/sessions — tested (server.test.ts: grouped active sessions)
|
|
18
17
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
19
18
|
import { describe, test } from "node:test";
|
|
20
19
|
import assert from "node:assert/strict";
|
package/dist/api/server.js
CHANGED
|
@@ -42,7 +42,7 @@ import { recordDecision } from "../memory/decisions.js";
|
|
|
42
42
|
import { upsertEntity } from "../memory/entities.js";
|
|
43
43
|
import { getInboxItem, listPendingInboxItems, resolveInboxItem } from "../memory/inbox.js";
|
|
44
44
|
import { ingestSource } from "../wiki/ingest.js";
|
|
45
|
-
import { listKorgResearchSessions
|
|
45
|
+
import { listKorgResearchSessions } from "./korg.js";
|
|
46
46
|
const log = childLogger("server");
|
|
47
47
|
const modeContext = new ModeContext(config);
|
|
48
48
|
void searchIndex; // re-exported by index-manager; reference here documents the dep
|
|
@@ -146,10 +146,6 @@ const prMergeHookSchema = z.object({
|
|
|
146
146
|
body: z.string().optional(),
|
|
147
147
|
files_changed: z.array(z.string()).optional(),
|
|
148
148
|
});
|
|
149
|
-
const korgRequestSchema = z.object({
|
|
150
|
-
message: requiredString("Missing 'message' in request body"),
|
|
151
|
-
sessionKey: z.string().trim().min(1).optional(),
|
|
152
|
-
}).strict();
|
|
153
149
|
const projectHardRulesSchema = z.object({
|
|
154
150
|
hardRules: z.object({
|
|
155
151
|
auto_pr: z.boolean({ error: "hardRules.auto_pr must be a boolean" }),
|
|
@@ -209,6 +205,13 @@ function coerceWikiPageUpdated(path, updated) {
|
|
|
209
205
|
function wikiPathToBrowserSlug(path) {
|
|
210
206
|
return path.replace(/^pages\//, "").replace(/\.md$/, "");
|
|
211
207
|
}
|
|
208
|
+
function entityTypeFromPath(path) {
|
|
209
|
+
const rest = path.startsWith("pages/") ? path.slice("pages/".length) : path;
|
|
210
|
+
const segs = rest.split("/").filter(Boolean);
|
|
211
|
+
if (segs.length <= 1)
|
|
212
|
+
return (segs[0] || "pages").replace(/\.md$/i, "");
|
|
213
|
+
return segs[0];
|
|
214
|
+
}
|
|
212
215
|
function normalizeOptionalQueryParam(value) {
|
|
213
216
|
if (typeof value !== "string") {
|
|
214
217
|
return undefined;
|
|
@@ -255,8 +258,8 @@ function listDbWikiBrowserPages(filters) {
|
|
|
255
258
|
const clauses = [];
|
|
256
259
|
const params = [];
|
|
257
260
|
if (filters.type) {
|
|
258
|
-
clauses.push("entity_type = ?");
|
|
259
|
-
params.push(filters.type);
|
|
261
|
+
clauses.push("(entity_type = ? OR (entity_type IS NULL AND path LIKE ?))");
|
|
262
|
+
params.push(filters.type, `pages/${filters.type}/%`);
|
|
260
263
|
}
|
|
261
264
|
if (filters.q) {
|
|
262
265
|
clauses.push("(title LIKE ? COLLATE NOCASE OR COALESCE(summary, '') LIKE ? COLLATE NOCASE)");
|
|
@@ -272,7 +275,7 @@ function listDbWikiBrowserPages(filters) {
|
|
|
272
275
|
slug: wikiPathToBrowserSlug(row.path),
|
|
273
276
|
title: row.title,
|
|
274
277
|
summary: row.summary ?? "",
|
|
275
|
-
type: row.entity_type ??
|
|
278
|
+
type: row.entity_type ?? entityTypeFromPath(row.path),
|
|
276
279
|
last_updated: coerceWikiPageUpdated(row.path, row.last_updated ?? undefined),
|
|
277
280
|
...(row.pinned ? { pinned: true } : {}),
|
|
278
281
|
}));
|
|
@@ -1742,11 +1745,6 @@ app.delete("/api/wiki/page", async (req, res) => {
|
|
|
1742
1745
|
const removed = await withWikiWrite(() => deletePage(path));
|
|
1743
1746
|
res.json({ ok: removed, path });
|
|
1744
1747
|
});
|
|
1745
|
-
app.post("/api/wiki/korg", authMiddleware, async (req, res) => {
|
|
1746
|
-
const body = parseRequest(korgRequestSchema, req.body ?? {});
|
|
1747
|
-
const result = await routeKorgMessage(body);
|
|
1748
|
-
res.json(result);
|
|
1749
|
-
});
|
|
1750
1748
|
app.get("/api/wiki/korg/sessions", authMiddleware, (_req, res) => {
|
|
1751
1749
|
res.json({ sessions: listKorgResearchSessions(getDb()) });
|
|
1752
1750
|
});
|
package/dist/copilot/tools.js
CHANGED
|
@@ -15,7 +15,7 @@ import { searchIndex, addToIndex, buildIndexEntryForPage, reindexWikiPages, } fr
|
|
|
15
15
|
import { traverse as wikiTraverse } from "../wiki/links.js";
|
|
16
16
|
import { validateWikiFrontmatter, validateAndBackfillFrontmatter } from "../wiki/frontmatter.js";
|
|
17
17
|
import { appendTimeline } from "../wiki/timeline.js";
|
|
18
|
-
import { ingestSource, detectSourceType } from "../wiki/ingest.js";
|
|
18
|
+
import { ingestSource, detectSourceType, looksLikeLocalFilePath } from "../wiki/ingest.js";
|
|
19
19
|
import { appendLog } from "../wiki/log-manager.js";
|
|
20
20
|
import { loadTaxonomy } from "../wiki/taxonomy.js";
|
|
21
21
|
import { topicPagePath } from "../wiki/topic-structure.js";
|
|
@@ -72,6 +72,38 @@ function isTimeoutError(err) {
|
|
|
72
72
|
const msg = err instanceof Error ? err.message : String(err);
|
|
73
73
|
return /timeout|timed?\s*out/i.test(msg);
|
|
74
74
|
}
|
|
75
|
+
function validateWikiPageInput(path, content, allowedTags = loadTaxonomy()) {
|
|
76
|
+
assertPagePath(path);
|
|
77
|
+
const backfilled = validateAndBackfillFrontmatter(path, content);
|
|
78
|
+
const nextContent = backfilled.changed ? backfilled.content : content;
|
|
79
|
+
const validation = validateWikiFrontmatter(nextContent, { allowedTags });
|
|
80
|
+
if (!validation.valid) {
|
|
81
|
+
throw new Error(validation.errors.map((error) => error.message).join("\n\n"));
|
|
82
|
+
}
|
|
83
|
+
return nextContent;
|
|
84
|
+
}
|
|
85
|
+
function writeWikiPageAndRefreshIndex(page, logSource) {
|
|
86
|
+
writePage(page.path, page.content);
|
|
87
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
88
|
+
const rebuilt = buildIndexEntryForPage(page.path, {
|
|
89
|
+
section: page.section || "Knowledge",
|
|
90
|
+
updated: today,
|
|
91
|
+
});
|
|
92
|
+
if (rebuilt) {
|
|
93
|
+
rebuilt.section = page.section || "Knowledge";
|
|
94
|
+
addToIndex(rebuilt);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
addToIndex({
|
|
98
|
+
path: page.path,
|
|
99
|
+
title: page.title,
|
|
100
|
+
summary: indexSafe(page.summary),
|
|
101
|
+
section: page.section || "Knowledge",
|
|
102
|
+
updated: today,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
appendLog("update", `${logSource}: ${indexSafe(page.title)} (${page.path})`);
|
|
106
|
+
}
|
|
75
107
|
function hasAdoPat() {
|
|
76
108
|
return (process.env.ADO_PAT?.trim() || config.adoPat).length > 0;
|
|
77
109
|
}
|
|
@@ -204,13 +236,20 @@ const memoryProposeArgsSchema = z.object({
|
|
|
204
236
|
}
|
|
205
237
|
});
|
|
206
238
|
const memoryTierTableSchema = z.enum(["observation", "decision", "entity", "action_item"]);
|
|
207
|
-
const
|
|
239
|
+
const wikiPageArgsSchema = z.object({
|
|
208
240
|
path: z.string().describe("Page path relative to wiki root (e.g. 'pages/projects/chapterhouse/index.md', 'pages/projects/chapterhouse/decisions.md', 'pages/people/brian/index.md')"),
|
|
209
241
|
title: z.string().describe("Page title for the index"),
|
|
210
|
-
summary: z.string().describe("One-line summary for the index"),
|
|
242
|
+
summary: z.string().max(160, "Summary must be 160 characters or fewer").describe("One-line summary for the index"),
|
|
211
243
|
section: z.string().optional().describe("Index section (default: 'Knowledge')"),
|
|
212
244
|
content: z.string().describe("Full page content (markdown)"),
|
|
213
245
|
});
|
|
246
|
+
const wikiUpdateArgsSchema = wikiPageArgsSchema;
|
|
247
|
+
const wikiBatchUpdateArgsSchema = z.object({
|
|
248
|
+
pages: z.array(wikiPageArgsSchema)
|
|
249
|
+
.min(1)
|
|
250
|
+
.max(50)
|
|
251
|
+
.describe("Array of pages to create or update (1–50 items)"),
|
|
252
|
+
});
|
|
214
253
|
function getCurrentQuarter(now = new Date()) {
|
|
215
254
|
return `${now.getUTCFullYear()}-Q${Math.floor(now.getUTCMonth() / 3) + 1}`;
|
|
216
255
|
}
|
|
@@ -1497,48 +1536,59 @@ export function createTools(deps) {
|
|
|
1497
1536
|
parameters: wikiUpdateArgsSchema,
|
|
1498
1537
|
handler: async (args) => {
|
|
1499
1538
|
try {
|
|
1500
|
-
|
|
1539
|
+
const parsedArgs = wikiUpdateArgsSchema.parse(args);
|
|
1540
|
+
ensureWikiStructure();
|
|
1501
1541
|
return await withWikiWrite(async () => {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1542
|
+
const content = validateWikiPageInput(parsedArgs.path, parsedArgs.content);
|
|
1543
|
+
const page = { ...parsedArgs, content };
|
|
1544
|
+
writeWikiPageAndRefreshIndex(page, "wiki_update");
|
|
1545
|
+
return `Wiki page updated: ${page.title} (${page.path})`;
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
catch (err) {
|
|
1549
|
+
const error = sanitizeWikiUpdateError(err);
|
|
1550
|
+
log.error({ err: err instanceof Error ? err.message : err, path: typeof args?.path === "string" ? args.path : undefined }, "wiki_update failed");
|
|
1551
|
+
return { error };
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
}),
|
|
1555
|
+
defineTool("wiki_batch_update", {
|
|
1556
|
+
description: "Create or update multiple wiki pages in a single operation. " +
|
|
1557
|
+
"Each page follows the same path rules as wiki_update. " +
|
|
1558
|
+
"Up to 50 pages per call. Returns a per-page success/error summary.",
|
|
1559
|
+
parameters: wikiBatchUpdateArgsSchema,
|
|
1560
|
+
handler: async (args) => {
|
|
1561
|
+
try {
|
|
1562
|
+
const parsedArgs = wikiBatchUpdateArgsSchema.parse(args);
|
|
1563
|
+
ensureWikiStructure();
|
|
1564
|
+
return await withWikiWrite(async () => {
|
|
1565
|
+
const allowedTags = loadTaxonomy();
|
|
1566
|
+
const results = [];
|
|
1567
|
+
for (const pageArgs of parsedArgs.pages) {
|
|
1568
|
+
try {
|
|
1569
|
+
const content = validateWikiPageInput(pageArgs.path, pageArgs.content, allowedTags);
|
|
1570
|
+
writeWikiPageAndRefreshIndex({ ...pageArgs, content }, "wiki_batch_update");
|
|
1571
|
+
results.push({ path: pageArgs.path, status: "ok" });
|
|
1572
|
+
}
|
|
1573
|
+
catch (err) {
|
|
1574
|
+
results.push({
|
|
1575
|
+
path: pageArgs.path,
|
|
1576
|
+
status: "error",
|
|
1577
|
+
error: sanitizeWikiUpdateError(err),
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1525
1580
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
summary: indexSafe(parsedArgs.summary).slice(0, 160),
|
|
1531
|
-
section: parsedArgs.section || "Knowledge",
|
|
1532
|
-
updated: today,
|
|
1533
|
-
});
|
|
1581
|
+
const createdCount = results.filter((result) => result.status === "ok").length;
|
|
1582
|
+
const errors = results.filter((result) => result.status === "error");
|
|
1583
|
+
if (errors.length === 0) {
|
|
1584
|
+
return `Created ${createdCount} pages successfully.`;
|
|
1534
1585
|
}
|
|
1535
|
-
|
|
1536
|
-
return `Wiki page updated: ${parsedArgs.title} (${parsedArgs.path})`;
|
|
1586
|
+
return `Created ${createdCount} pages successfully.\nErrors (${errors.length}):\n${errors.map((result) => ` • ${result.path} — ${result.error}`).join("\n")}`;
|
|
1537
1587
|
});
|
|
1538
1588
|
}
|
|
1539
1589
|
catch (err) {
|
|
1540
1590
|
const error = sanitizeWikiUpdateError(err);
|
|
1541
|
-
log.error({ err: err instanceof Error ? err.message : err
|
|
1591
|
+
log.error({ err: err instanceof Error ? err.message : err }, "wiki_batch_update failed");
|
|
1542
1592
|
return { error };
|
|
1543
1593
|
}
|
|
1544
1594
|
},
|
|
@@ -1634,6 +1684,11 @@ export function createTools(deps) {
|
|
|
1634
1684
|
handler: async (args) => {
|
|
1635
1685
|
ensureWikiStructure();
|
|
1636
1686
|
try {
|
|
1687
|
+
if (looksLikeLocalFilePath(args.source)) {
|
|
1688
|
+
return {
|
|
1689
|
+
error: "wiki_ingest_source does not support local file paths. Provide a URL, git repo URL, or raw text content.",
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1637
1692
|
const sourceType = args.type ?? detectSourceType(args.source);
|
|
1638
1693
|
const result = await ingestSource(args.source, sourceType, args.topic, {
|
|
1639
1694
|
sessionId: args.session_id,
|