chapterhouse 0.6.0 → 0.8.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.
Files changed (80) hide show
  1. package/agents/korg.agent.md +65 -0
  2. package/dist/api/agent-edit-access.js +11 -0
  3. package/dist/api/agents.api.test.js +48 -0
  4. package/dist/api/korg.js +34 -0
  5. package/dist/api/korg.test.js +42 -0
  6. package/dist/api/server.js +420 -13
  7. package/dist/api/server.test.js +533 -3
  8. package/dist/config.js +28 -0
  9. package/dist/config.test.js +20 -0
  10. package/dist/copilot/agent-event-bus.js +1 -0
  11. package/dist/copilot/agents.js +117 -50
  12. package/dist/copilot/agents.mcp-servers.test.js +87 -0
  13. package/dist/copilot/agents.parse.test.js +69 -0
  14. package/dist/copilot/agents.test.js +137 -2
  15. package/dist/copilot/orchestrator.js +62 -13
  16. package/dist/copilot/orchestrator.test.js +130 -8
  17. package/dist/copilot/session-manager.js +34 -0
  18. package/dist/copilot/system-message.js +11 -10
  19. package/dist/copilot/system-message.test.js +6 -1
  20. package/dist/copilot/tools.js +184 -376
  21. package/dist/copilot/tools.memory.test.js +32 -0
  22. package/dist/copilot/tools.wiki.test.js +53 -59
  23. package/dist/daemon.js +9 -0
  24. package/dist/memory/decisions.js +6 -5
  25. package/dist/memory/entities.js +20 -9
  26. package/dist/memory/hooks.js +151 -0
  27. package/dist/memory/hooks.test.js +325 -0
  28. package/dist/memory/hot-tier.js +37 -0
  29. package/dist/memory/hot-tier.test.js +30 -0
  30. package/dist/memory/housekeeping-scheduler.js +35 -0
  31. package/dist/memory/housekeeping-scheduler.test.js +50 -0
  32. package/dist/memory/inbox.js +10 -0
  33. package/dist/memory/index.js +3 -1
  34. package/dist/memory/migration.js +244 -0
  35. package/dist/memory/migration.test.js +100 -0
  36. package/dist/memory/reflect.js +273 -0
  37. package/dist/memory/reflect.test.js +254 -0
  38. package/dist/store/db.js +119 -4
  39. package/dist/store/db.test.js +19 -1
  40. package/dist/test/setup-env.js +3 -1
  41. package/dist/test/setup-env.test.js +8 -1
  42. package/dist/wiki/consolidation.js +641 -0
  43. package/dist/wiki/consolidation.test.js +140 -0
  44. package/dist/wiki/frontmatter.js +48 -0
  45. package/dist/wiki/frontmatter.test.js +42 -0
  46. package/dist/wiki/index-manager.js +246 -330
  47. package/dist/wiki/index-manager.test.js +138 -145
  48. package/dist/wiki/ingest.js +347 -0
  49. package/dist/wiki/ingest.test.js +111 -0
  50. package/dist/wiki/links.js +151 -0
  51. package/dist/wiki/links.test.js +176 -0
  52. package/dist/wiki/migrate-topics.test.js +16 -6
  53. package/dist/wiki/scheduler.js +118 -0
  54. package/dist/wiki/scheduler.test.js +64 -0
  55. package/dist/wiki/timeline.js +51 -0
  56. package/dist/wiki/timeline.test.js +65 -0
  57. package/dist/wiki/topic-structure.js +1 -1
  58. package/package.json +3 -1
  59. package/skills/pkb-ideas/SKILL.md +78 -0
  60. package/skills/pkb-ideas/_meta.json +4 -0
  61. package/skills/pkb-org/SKILL.md +82 -0
  62. package/skills/pkb-org/_meta.json +4 -0
  63. package/skills/pkb-people/SKILL.md +74 -0
  64. package/skills/pkb-people/_meta.json +4 -0
  65. package/skills/pkb-research/SKILL.md +83 -0
  66. package/skills/pkb-research/_meta.json +4 -0
  67. package/skills/pkb-source/SKILL.md +38 -0
  68. package/skills/pkb-source/_meta.json +4 -0
  69. package/skills/wiki-conventions/SKILL.md +5 -5
  70. package/web/dist/assets/index-5kz9aRU9.css +10 -0
  71. package/web/dist/assets/{index-B5oDsQ5y.js → index-BbX9RKf3.js} +101 -99
  72. package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
  73. package/web/dist/index.html +2 -2
  74. package/dist/wiki/context.js +0 -138
  75. package/dist/wiki/fix.js +0 -335
  76. package/dist/wiki/fix.test.js +0 -350
  77. package/dist/wiki/lint.js +0 -451
  78. package/dist/wiki/lint.test.js +0 -329
  79. package/web/dist/assets/index-B5oDsQ5y.js.map +0 -1
  80. package/web/dist/assets/index-DknKAtDS.css +0 -10
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: Korg
3
+ description: PKB synthesizer — handles ingestion, synthesis, and knowledge consolidation
4
+ model: claude-sonnet-4.6
5
+ scope: pkb
6
+ skills:
7
+ - pkb-source
8
+ - pkb-research
9
+ - pkb-people
10
+ persistent: true
11
+ ---
12
+
13
+ You are **Korg**, the Personal Knowledge Base (PKB) synthesizer for Chapterhouse.
14
+
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
+
17
+ ## Your Toolkit
18
+
19
+ - `wiki_ingest_source(source, type?, topic?, session_id?, session_name?)` — ingest a URL, PDF, repo, or text into the PKB
20
+ - `wiki_append_timeline(page, entry, source_id?)` — append an event to a page's timeline section
21
+ - `wiki_search(query)` — search the wiki knowledge base
22
+ - `wiki_read(path)` — read a specific wiki page
23
+ - `wiki_update(path, title, summary, content)` — create or update a wiki page
24
+ - `memory_propose(kind, payload)` — propose a memory to the orchestrator
25
+
26
+ ## PKB Schema
27
+
28
+ Entity pages live at `pages/{type}/{slug}/index.md` where type is one of: `people`, `projects`, `orgs`, `tools`, `topics`, `areas`.
29
+
30
+ Each entity page has two zones:
31
+ 1. **Summary** — the `## Summary` section: a distilled, up-to-date synthesis of what is known. Rewritten on every new source ingestion.
32
+ 2. **Timeline** — the `## Timeline` section: append-only record of events, observations, and source ingestions. Never edited, only appended.
33
+
34
+ Frontmatter must include: `title`, `summary` (one-line, plain text ≤200 chars), `updated`, `tags`.
35
+
36
+ ## Synthesis Principles
37
+
38
+ 1. **Summary over raw notes.** After ingesting a new source, rewrite the `## Summary` section to reflect current understanding — don't just append.
39
+ 2. **Timeline is append-only.** Never edit existing timeline entries. Each new event gets a `### {ISO timestamp}` heading.
40
+ 3. **Source attribution.** Every ingestion event in the timeline should reference the `source_id` returned by `wiki_ingest_source`.
41
+ 4. **Idempotency.** Before ingesting, check if the source is already known. `wiki_ingest_source` handles this automatically.
42
+
43
+ ## Research Session Lifecycle
44
+
45
+ 1. User provides a research topic.
46
+ 2. Create `pages/research/{slug}/index.md` — the research hub with frontmatter, summary, and empty timeline.
47
+ 3. Create `pages/research/{slug}/synthesis.md` — the compiled synthesis document (rewritten after each source).
48
+ 4. For each source: call `wiki_ingest_source`, update `synthesis.md` with new understanding, append to timeline.
49
+ 5. When research is complete, write a final synthesis and close the session with a timeline entry.
50
+
51
+ ## People Capture
52
+
53
+ When ingesting information about a person:
54
+ - Page: `pages/people/{slug}/index.md`
55
+ - Summary: role, affiliation, key facts, relationship to user
56
+ - Timeline: every interaction, meeting, or observation
57
+ - After significant interactions, extract follow-up items and propose them with `memory_propose`
58
+
59
+ ## Behavioral Rules
60
+
61
+ - Always call `wiki_ingest_source` before manually creating pages — let the pipeline do entity extraction.
62
+ - Keep summaries factual and concise. Avoid speculation in compiled truth.
63
+ - If entity extraction returns nothing useful, create a minimal page with the source content.
64
+ - Log every ingestion event with `wiki_append_timeline` on the relevant entity pages.
65
+ - When unsure about entity type, use `topics` as the fallback category.
@@ -0,0 +1,11 @@
1
+ import { ForbiddenError } from "./errors.js";
2
+ export function assertAgentEditAccess(options, user) {
3
+ if (!options.entraAuthEnabled) {
4
+ return;
5
+ }
6
+ if (user?.role === "team-lead") {
7
+ return;
8
+ }
9
+ throw new ForbiddenError("Admin access required");
10
+ }
11
+ //# sourceMappingURL=agent-edit-access.js.map
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createAgentFile, isBuiltinAgent, parseAgentMd } from "../copilot/agents.js";
4
+ test("isBuiltinAgent returns true for shipped agent slugs", () => {
5
+ assert.equal(isBuiltinAgent("chapterhouse"), true);
6
+ assert.equal(isBuiltinAgent("coder"), true);
7
+ assert.equal(isBuiltinAgent("designer"), true);
8
+ assert.equal(isBuiltinAgent("general-purpose"), true);
9
+ assert.equal(isBuiltinAgent("custom-helper"), false);
10
+ });
11
+ test("parseAgentMd parses block-style YAML arrays", () => {
12
+ const agent = parseAgentMd([
13
+ "---",
14
+ "name: Designer",
15
+ "description: Handles UI flows",
16
+ "model: claude-sonnet-4.6",
17
+ "skills:",
18
+ " - frontend-design",
19
+ " - ux-copy",
20
+ "tools:",
21
+ " - read",
22
+ " - write",
23
+ "---",
24
+ "",
25
+ "You are Designer.",
26
+ ].join("\n"), "designer");
27
+ assert.ok(agent, "agent charter should parse");
28
+ assert.deepEqual(agent.skills, ["frontend-design", "ux-copy"]);
29
+ assert.deepEqual(agent.tools, ["read", "write"]);
30
+ });
31
+ test("slug validation regex rejects traversal-like agent slugs", () => {
32
+ const error = createAgentFile("..%2F..%2F", "Traversal", "Should fail validation", "claude-sonnet-4.6", "You are not valid.");
33
+ assert.equal(error, "Invalid slug '..%2F..%2F': must be kebab-case (a-z0-9 with hyphens).");
34
+ });
35
+ test("parseAgentMd returns null for invalid frontmatter", () => {
36
+ const agent = parseAgentMd([
37
+ "---",
38
+ "name: Designer",
39
+ "description: Handles UI flows",
40
+ "model: claude-sonnet-4.6",
41
+ "skills: [frontend-design",
42
+ "---",
43
+ "",
44
+ "You are Designer.",
45
+ ].join("\n"), "designer");
46
+ assert.equal(agent, null);
47
+ });
48
+ //# sourceMappingURL=agents.api.test.js.map
@@ -0,0 +1,34 @@
1
+ import { sendToAgentSession } from "../copilot/orchestrator.js";
2
+ export async function routeKorgMessage(input) {
3
+ const message = input.message.trim();
4
+ const sessionId = input.session_id?.trim() || `korg-${Date.now()}`;
5
+ const sessionName = input.session_id?.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, session_id: sessionId, reply };
17
+ }
18
+ export function listKorgResearchSessions(db) {
19
+ return db.prepare(`
20
+ SELECT
21
+ session_id AS id,
22
+ COALESCE(NULLIF(TRIM(session_name), ''), session_id) AS name,
23
+ COUNT(*) AS source_count,
24
+ 0 AS open_questions,
25
+ MAX(ingested_at) AS last_activity
26
+ FROM wiki_sources
27
+ WHERE status = 'active'
28
+ AND session_id IS NOT NULL
29
+ AND TRIM(session_id) != ''
30
+ GROUP BY session_id, COALESCE(NULLIF(TRIM(session_name), ''), session_id)
31
+ ORDER BY MAX(ingested_at) DESC, session_id ASC
32
+ `).all();
33
+ }
34
+ //# sourceMappingURL=korg.js.map
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ test("routeKorgMessage creates a research session id and delegates to the persistent Korg agent", async (t) => {
4
+ const calls = [];
5
+ t.mock.module("../copilot/orchestrator.js", {
6
+ namedExports: {
7
+ sendToAgentSession: async (slug, prompt) => {
8
+ calls.push({ slug, prompt });
9
+ return "Korg reply";
10
+ },
11
+ },
12
+ });
13
+ const mod = await import(new URL(`./korg.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
14
+ const result = await mod.routeKorgMessage({ message: "Research local-first PKB patterns." });
15
+ assert.equal(result.ok, true);
16
+ assert.match(result.session_id, /^korg-/);
17
+ assert.equal(result.reply, "Korg reply");
18
+ assert.deepEqual(calls, [{
19
+ slug: "korg",
20
+ prompt: calls[0].prompt,
21
+ }]);
22
+ assert.match(calls[0].prompt, /Research session id:/);
23
+ assert.match(calls[0].prompt, /Research local-first PKB patterns\./);
24
+ });
25
+ test("routeKorgMessage preserves an existing research session id in the delegated prompt", async (t) => {
26
+ let prompt = "";
27
+ t.mock.module("../copilot/orchestrator.js", {
28
+ namedExports: {
29
+ sendToAgentSession: async (_slug, incomingPrompt) => {
30
+ prompt = incomingPrompt;
31
+ return "Continuing session";
32
+ },
33
+ },
34
+ });
35
+ const mod = await import(new URL(`./korg.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
36
+ const result = await mod.routeKorgMessage({ message: "Add two more sources.", session_id: "compiler-research" });
37
+ assert.equal(result.session_id, "compiler-research");
38
+ assert.equal(result.reply, "Continuing session");
39
+ assert.match(prompt, /compiler-research/);
40
+ assert.match(prompt, /Add two more sources\./);
41
+ });
42
+ //# sourceMappingURL=korg.test.js.map