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.
Files changed (116) hide show
  1. package/dist/api/route-coverage.test.js +1 -3
  2. package/dist/api/server.js +0 -2
  3. package/dist/api/server.test.js +0 -281
  4. package/dist/config.js +3 -85
  5. package/dist/config.test.js +5 -123
  6. package/dist/copilot/agents.js +13 -10
  7. package/dist/copilot/agents.test.js +10 -11
  8. package/dist/copilot/memory-coordinator.js +12 -227
  9. package/dist/copilot/memory-coordinator.test.js +31 -250
  10. package/dist/copilot/orchestrator.js +8 -66
  11. package/dist/copilot/orchestrator.test.js +9 -467
  12. package/dist/copilot/skills.js +15 -1
  13. package/dist/copilot/system-message.js +9 -15
  14. package/dist/copilot/system-message.test.js +9 -22
  15. package/dist/copilot/tools/index.js +3 -3
  16. package/dist/copilot/tools-deps.js +1 -1
  17. package/dist/copilot/tools.agent.test.js +6 -0
  18. package/dist/copilot/tools.inventory.test.js +1 -14
  19. package/dist/daemon.js +7 -9
  20. package/dist/memory/assets.js +33 -0
  21. package/dist/memory/domains.js +58 -0
  22. package/dist/memory/domains.test.js +47 -0
  23. package/dist/memory/git.js +66 -0
  24. package/dist/memory/git.test.js +32 -0
  25. package/dist/memory/history.js +19 -0
  26. package/dist/memory/hottier.js +32 -0
  27. package/dist/memory/hottier.test.js +33 -0
  28. package/dist/memory/index.js +5 -13
  29. package/dist/memory/instructions.js +17 -0
  30. package/dist/memory/manager.js +84 -0
  31. package/dist/memory/markdown.js +78 -0
  32. package/dist/memory/markdown.test.js +42 -0
  33. package/dist/memory/mutex.js +18 -0
  34. package/dist/memory/path-guard.js +26 -0
  35. package/dist/memory/path-guard.test.js +27 -0
  36. package/dist/memory/paths.js +12 -0
  37. package/dist/memory/reconcile.js +75 -0
  38. package/dist/memory/reconcile.test.js +50 -0
  39. package/dist/memory/scaffold.js +37 -0
  40. package/dist/memory/scaffold.test.js +52 -0
  41. package/dist/memory/tools/commit-wrapper.js +32 -0
  42. package/dist/memory/tools/domains.js +73 -0
  43. package/dist/memory/tools/domains.test.js +66 -0
  44. package/dist/memory/tools/git.js +52 -0
  45. package/dist/memory/tools/index.js +25 -0
  46. package/dist/memory/tools/read.js +101 -0
  47. package/dist/memory/tools/read.test.js +69 -0
  48. package/dist/memory/tools/search.js +103 -0
  49. package/dist/memory/tools/search.test.js +63 -0
  50. package/dist/memory/tools/sessions.js +45 -0
  51. package/dist/memory/tools/sessions.test.js +74 -0
  52. package/dist/memory/tools/shared.js +7 -0
  53. package/dist/memory/tools/write.js +116 -0
  54. package/dist/memory/tools/write.test.js +107 -0
  55. package/dist/memory/walk.js +39 -0
  56. package/dist/store/repositories/sessions.js +40 -0
  57. package/dist/wiki/consolidation.js +3 -31
  58. package/dist/wiki/consolidation.test.js +0 -19
  59. package/package.json +1 -1
  60. package/skills/system/evolve/SKILL.md +131 -0
  61. package/skills/system/foresight/SKILL.md +116 -0
  62. package/skills/system/history/SKILL.md +58 -0
  63. package/skills/system/housekeeping/SKILL.md +185 -0
  64. package/skills/system/reflect/SKILL.md +214 -0
  65. package/skills/system/scenario/SKILL.md +198 -0
  66. package/skills/system/setup/SKILL.md +113 -0
  67. package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
  68. package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
  69. package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
  70. package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
  71. package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
  72. package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
  73. package/web/dist/index.html +1 -1
  74. package/dist/api/routes/memory.js +0 -475
  75. package/dist/api/routes/memory.test.js +0 -108
  76. package/dist/copilot/tools/memory.js +0 -678
  77. package/dist/copilot/tools.memory.test.js +0 -590
  78. package/dist/memory/action-items.js +0 -100
  79. package/dist/memory/action-items.test.js +0 -83
  80. package/dist/memory/active-scope.js +0 -78
  81. package/dist/memory/active-scope.test.js +0 -80
  82. package/dist/memory/checkpoint-prompt.js +0 -71
  83. package/dist/memory/checkpoint.js +0 -274
  84. package/dist/memory/checkpoint.test.js +0 -275
  85. package/dist/memory/decisions.js +0 -54
  86. package/dist/memory/decisions.test.js +0 -92
  87. package/dist/memory/entities.js +0 -70
  88. package/dist/memory/entities.test.js +0 -65
  89. package/dist/memory/eot.js +0 -459
  90. package/dist/memory/eot.test.js +0 -949
  91. package/dist/memory/hooks.js +0 -149
  92. package/dist/memory/hooks.test.js +0 -325
  93. package/dist/memory/hot-tier.js +0 -283
  94. package/dist/memory/hot-tier.test.js +0 -275
  95. package/dist/memory/housekeeping-scheduler.js +0 -187
  96. package/dist/memory/housekeeping-scheduler.test.js +0 -236
  97. package/dist/memory/housekeeping.js +0 -497
  98. package/dist/memory/housekeeping.test.js +0 -410
  99. package/dist/memory/inbox.js +0 -83
  100. package/dist/memory/inbox.test.js +0 -178
  101. package/dist/memory/migration.js +0 -244
  102. package/dist/memory/migration.test.js +0 -108
  103. package/dist/memory/observations.js +0 -46
  104. package/dist/memory/observations.test.js +0 -86
  105. package/dist/memory/recall.js +0 -269
  106. package/dist/memory/recall.test.js +0 -265
  107. package/dist/memory/reflect.js +0 -273
  108. package/dist/memory/reflect.test.js +0 -256
  109. package/dist/memory/scope-lock.js +0 -26
  110. package/dist/memory/scope-lock.test.js +0 -118
  111. package/dist/memory/scopes.js +0 -89
  112. package/dist/memory/scopes.test.js +0 -176
  113. package/dist/memory/tiering.js +0 -223
  114. package/dist/memory/tiering.test.js +0 -323
  115. package/dist/memory/types.js +0 -2
  116. package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
@@ -1,92 +0,0 @@
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 repoRoot = process.cwd();
6
- const sandboxRoot = join(repoRoot, ".test-work", `memory-decisions-${process.pid}`);
7
- const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
8
- process.env.CHAPTERHOUSE_HOME = sandboxRoot;
9
- function resetSandbox() {
10
- mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
11
- rmSync(sandboxRoot, { recursive: true, force: true });
12
- mkdirSync(chapterhouseHome, { recursive: true });
13
- }
14
- async function loadModules() {
15
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
16
- const memoryModule = await import(new URL("./index.js", import.meta.url).href);
17
- return { dbModule, memoryModule };
18
- }
19
- function getFunction(module, name) {
20
- const value = module[name];
21
- assert.equal(typeof value, "function", `expected ${name} to be exported`);
22
- return value;
23
- }
24
- test.beforeEach(async () => {
25
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
26
- dbModule.closeDb();
27
- resetSandbox();
28
- });
29
- test.after(async () => {
30
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
31
- dbModule.closeDb();
32
- rmSync(sandboxRoot, { recursive: true, force: true });
33
- });
34
- test("recordDecision supports CRUD, supersession, and keeps the decision FTS index in sync", async () => {
35
- const { dbModule, memoryModule } = await loadModules();
36
- const db = dbModule.getDb();
37
- const getScope = getFunction(memoryModule, "getScope");
38
- const recordDecision = getFunction(memoryModule, "recordDecision");
39
- const getDecision = getFunction(memoryModule, "getDecision");
40
- const listDecisions = getFunction(memoryModule, "listDecisions");
41
- const supersedeDecision = getFunction(memoryModule, "supersedeDecision");
42
- const chapterhouse = getScope("chapterhouse");
43
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
44
- const original = recordDecision({
45
- scope_id: chapterhouse.id,
46
- title: "Use SQLite for scoped memory",
47
- rationale: "SQLite FTS5 keeps memory recall local, fast, and dependency-free.",
48
- decided_at: "2026-05-13",
49
- });
50
- const replacement = recordDecision({
51
- scope_id: chapterhouse.id,
52
- title: "Keep SQLite for agent memory v1",
53
- rationale: "SQLite remains the only persistence layer for PR 2.",
54
- decided_at: "2026-05-14",
55
- });
56
- assert.equal(listDecisions({ scope_id: chapterhouse.id }).length >= 2, true);
57
- assert.deepEqual(getDecision(original.id)?.title, original.title);
58
- const initialHits = db.prepare(`
59
- SELECT rowid
60
- FROM mem_decisions_fts
61
- WHERE mem_decisions_fts MATCH 'dependency'
62
- `).all();
63
- assert.deepEqual(initialHits, [{ rowid: original.id }]);
64
- db.prepare(`
65
- UPDATE mem_decisions
66
- SET rationale = ?
67
- WHERE id = ?
68
- `).run("FTS snippets should refresh when the rationale changes.", original.id);
69
- const oldHits = db.prepare(`
70
- SELECT rowid
71
- FROM mem_decisions_fts
72
- WHERE mem_decisions_fts MATCH 'dependency'
73
- `).all();
74
- const newHits = db.prepare(`
75
- SELECT rowid
76
- FROM mem_decisions_fts
77
- WHERE mem_decisions_fts MATCH 'snippets'
78
- `).all();
79
- assert.deepEqual(oldHits, []);
80
- assert.deepEqual(newHits, [{ rowid: original.id }]);
81
- const superseded = supersedeDecision(original.id, replacement.id);
82
- assert.equal(superseded.supersededBy, replacement.id);
83
- assert.equal(getDecision(original.id)?.supersededBy, replacement.id);
84
- db.prepare(`DELETE FROM mem_decisions WHERE id = ?`).run(original.id);
85
- const deletedHits = db.prepare(`
86
- SELECT rowid
87
- FROM mem_decisions_fts
88
- WHERE mem_decisions_fts MATCH 'snippets'
89
- `).all();
90
- assert.deepEqual(deletedHits, []);
91
- });
92
- //# sourceMappingURL=decisions.test.js.map
@@ -1,70 +0,0 @@
1
- import { getDb } from "../store/db.js";
2
- function toEntity(row) {
3
- return {
4
- id: row.id,
5
- scopeId: row.scope_id,
6
- slug: row.slug ?? undefined,
7
- kind: row.kind,
8
- name: row.name,
9
- summary: row.summary ?? undefined,
10
- tier: row.tier,
11
- confidence: row.confidence,
12
- createdAt: row.created_at,
13
- updatedAt: row.updated_at,
14
- };
15
- }
16
- export function getEntity(id) {
17
- const row = getDb().prepare(`
18
- SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
19
- FROM mem_entities
20
- WHERE id = ?
21
- `).get(id);
22
- return row ? toEntity(row) : undefined;
23
- }
24
- export function findEntityByName(scopeId, kind, name) {
25
- const row = getDb().prepare(`
26
- SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
27
- FROM mem_entities
28
- WHERE scope_id = ? AND kind = ? AND name = ?
29
- `).get(scopeId, kind, name);
30
- return row ? toEntity(row) : undefined;
31
- }
32
- export function findEntityBySlug(scopeId, slug) {
33
- const row = getDb().prepare(`
34
- SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
35
- FROM mem_entities
36
- WHERE scope_id = ? AND slug = ?
37
- `).get(scopeId, slug);
38
- return row ? toEntity(row) : undefined;
39
- }
40
- export function upsertEntity(input) {
41
- const db = getDb();
42
- const existing = input.slug
43
- ? findEntityBySlug(input.scope_id, input.slug) ?? findEntityByName(input.scope_id, input.kind, input.name)
44
- : findEntityByName(input.scope_id, input.kind, input.name);
45
- if (existing) {
46
- db.prepare(`
47
- UPDATE mem_entities
48
- SET slug = COALESCE(?, slug), summary = ?, tier = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP
49
- WHERE id = ?
50
- `).run(input.slug ?? null, input.summary ?? existing.summary ?? null, input.tier ?? existing.tier, input.confidence ?? existing.confidence, existing.id);
51
- return getEntity(existing.id);
52
- }
53
- const result = db.prepare(`
54
- INSERT INTO mem_entities (scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at)
55
- VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
56
- `).run(input.scope_id, input.slug ?? null, input.kind, input.name, input.summary ?? null, input.tier ?? "warm", input.confidence ?? 1.0);
57
- return getEntity(Number(result.lastInsertRowid));
58
- }
59
- export function listEntities(input = {}) {
60
- const rows = getDb().prepare(`
61
- SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
62
- FROM mem_entities
63
- WHERE (? IS NULL OR scope_id = ?)
64
- AND (? IS NULL OR kind = ?)
65
- ORDER BY updated_at DESC, id DESC
66
- LIMIT ? OFFSET ?
67
- `).all(input.scope_id ?? null, input.scope_id ?? null, input.kind ?? null, input.kind ?? null, input.limit ?? 50, input.offset ?? 0);
68
- return rows.map(toEntity);
69
- }
70
- //# sourceMappingURL=entities.js.map
@@ -1,65 +0,0 @@
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 repoRoot = process.cwd();
6
- const sandboxRoot = join(repoRoot, ".test-work", `memory-entities-${process.pid}`);
7
- const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
8
- process.env.CHAPTERHOUSE_HOME = sandboxRoot;
9
- function resetSandbox() {
10
- mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
11
- rmSync(sandboxRoot, { recursive: true, force: true });
12
- mkdirSync(chapterhouseHome, { recursive: true });
13
- }
14
- async function loadModules() {
15
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
16
- const memoryModule = await import(new URL("./index.js", import.meta.url).href);
17
- return { dbModule, memoryModule };
18
- }
19
- function getFunction(module, name) {
20
- const value = module[name];
21
- assert.equal(typeof value, "function", `expected ${name} to be exported`);
22
- return value;
23
- }
24
- test.beforeEach(async () => {
25
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
26
- dbModule.closeDb();
27
- resetSandbox();
28
- });
29
- test.after(async () => {
30
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
31
- dbModule.closeDb();
32
- rmSync(sandboxRoot, { recursive: true, force: true });
33
- });
34
- test("upsertEntity is idempotent on scope, kind, and name", async () => {
35
- const { dbModule, memoryModule } = await loadModules();
36
- dbModule.getDb();
37
- const getScope = getFunction(memoryModule, "getScope");
38
- const upsertEntity = getFunction(memoryModule, "upsertEntity");
39
- const getEntity = getFunction(memoryModule, "getEntity");
40
- const findEntityByName = getFunction(memoryModule, "findEntityByName");
41
- const listEntities = getFunction(memoryModule, "listEntities");
42
- const chapterhouse = getScope("chapterhouse");
43
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
44
- const first = upsertEntity({
45
- scope_id: chapterhouse.id,
46
- kind: "tool",
47
- name: "better-sqlite3",
48
- summary: "SQLite driver used for memory persistence.",
49
- tier: "warm",
50
- confidence: 0.7,
51
- });
52
- const second = upsertEntity({
53
- scope_id: chapterhouse.id,
54
- kind: "tool",
55
- name: "better-sqlite3",
56
- summary: "Sync SQLite driver used throughout the daemon.",
57
- tier: "hot",
58
- confidence: 0.95,
59
- });
60
- assert.equal(second.id, first.id);
61
- assert.equal(listEntities({ scope_id: chapterhouse.id, kind: "tool" }).length, 1);
62
- assert.equal(findEntityByName(chapterhouse.id, "tool", "better-sqlite3")?.id, first.id);
63
- assert.deepEqual(getEntity(first.id), second);
64
- });
65
- //# sourceMappingURL=entities.test.js.map
@@ -1,459 +0,0 @@
1
- import { config } from "../config.js";
2
- import { getAgent, loadAgents } from "../copilot/agents.js";
3
- import { runOneShotPrompt } from "../copilot/oneshot.js";
4
- import { childLogger } from "../util/logger.js";
5
- import { recordDecision } from "./decisions.js";
6
- import { upsertEntity } from "./entities.js";
7
- import { listPendingMemoryProposalsForTask, resolveInboxItem } from "./inbox.js";
8
- import { recordObservation } from "./observations.js";
9
- import { recordActionItem } from "./action-items.js";
10
- import { getActiveScope } from "./active-scope.js";
11
- import { getScope } from "./scopes.js";
12
- const log = childLogger("memory.eot");
13
- function isEndOfTaskHookEnabled() {
14
- return config.memoryEndOfTaskHookEnabled;
15
- }
16
- function isMemoryAutoAcceptEnabled() {
17
- return config.memoryAutoAcceptEnabled;
18
- }
19
- function isFrictionHookEnabled() {
20
- return config.memoryEndOfTaskFrictionEnabled;
21
- }
22
- function buildReviewerSystemPrompt() {
23
- return [
24
- "You review subagent memory proposals at end-of-task.",
25
- "Decide accept or reject for each proposal id.",
26
- "Optionally extract additional implicit durable memories from the task summary.",
27
- "Return JSON only with keys: decisions, implicit_memories.",
28
- "Each decision must include proposal_id, decision, reason.",
29
- "Supported kinds are observation, decision, entity, and action_item.",
30
- "Entity payloads must include name and entity_kind.",
31
- "Each implicit memory must include kind, scope_slug, payload, and may include confidence/reason.",
32
- ].join("\n");
33
- }
34
- function buildReviewerUserPrompt(finalResult, proposals) {
35
- return JSON.stringify({
36
- final_result: finalResult,
37
- proposals: proposals.map((proposal) => ({
38
- id: proposal.id,
39
- source_agent: proposal.sourceAgent,
40
- payload: proposal.payload,
41
- })),
42
- }, null, 2);
43
- }
44
- function buildFrictionSystemPrompt() {
45
- return [
46
- "You review a completed agent task for tool friction.",
47
- "Tool friction is: missing validation feedback, missing batch capability, silent failures,",
48
- "overly strict input constraints, or tool gaps that caused the agent to work around limitations.",
49
- "If you identify friction, return a JSON array of action items.",
50
- "Each item must have: title (string), detail (string), source (always 'eot:friction').",
51
- "If no friction was found, return an empty array [].",
52
- "Return JSON only. No prose, no wrapping.",
53
- ].join("\n");
54
- }
55
- function buildFrictionUserPrompt(finalResult) {
56
- return JSON.stringify({ final_result: finalResult }, null, 2);
57
- }
58
- function parseEnvelope(raw) {
59
- try {
60
- const parsed = JSON.parse(raw);
61
- if (!parsed || typeof parsed !== "object") {
62
- throw new Error("Invalid memory proposal payload.");
63
- }
64
- if (parsed.kind !== "observation"
65
- && parsed.kind !== "decision"
66
- && parsed.kind !== "entity"
67
- && parsed.kind !== "action_item") {
68
- throw new Error("Invalid proposal kind.");
69
- }
70
- if (!parsed.payload || typeof parsed.payload !== "object") {
71
- throw new Error("Invalid proposal payload.");
72
- }
73
- return {
74
- kind: parsed.kind,
75
- scope_slug: typeof parsed.scope_slug === "string" ? parsed.scope_slug : undefined,
76
- confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
77
- reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
78
- payload: parsed.payload,
79
- };
80
- }
81
- catch (err) {
82
- log.warn({ err }, "malformed memory proposal payload");
83
- return null;
84
- }
85
- }
86
- function parseReviewerResponse(raw) {
87
- try {
88
- const parsed = JSON.parse(raw);
89
- return {
90
- decisions: Array.isArray(parsed.decisions)
91
- ? parsed.decisions.flatMap((entry) => {
92
- if (!entry || typeof entry !== "object") {
93
- return [];
94
- }
95
- const candidate = entry;
96
- if (typeof candidate.proposal_id !== "number") {
97
- return [];
98
- }
99
- if (candidate.decision !== "accept" && candidate.decision !== "reject") {
100
- return [];
101
- }
102
- if (typeof candidate.reason !== "string" || candidate.reason.trim().length === 0) {
103
- return [];
104
- }
105
- return [{
106
- proposal_id: candidate.proposal_id,
107
- decision: candidate.decision,
108
- reason: candidate.reason.trim(),
109
- }];
110
- })
111
- : [],
112
- implicit_memories: Array.isArray(parsed.implicit_memories)
113
- ? parsed.implicit_memories.flatMap((entry) => {
114
- if (!entry || typeof entry !== "object") {
115
- return [];
116
- }
117
- const candidate = entry;
118
- if (candidate.kind !== "observation"
119
- && candidate.kind !== "decision"
120
- && candidate.kind !== "entity"
121
- && candidate.kind !== "action_item") {
122
- return [];
123
- }
124
- if (typeof candidate.scope_slug !== "string" || !candidate.payload || typeof candidate.payload !== "object") {
125
- return [];
126
- }
127
- return [{
128
- kind: candidate.kind,
129
- scope_slug: candidate.scope_slug,
130
- payload: candidate.payload,
131
- confidence: typeof candidate.confidence === "number" ? candidate.confidence : undefined,
132
- reason: typeof candidate.reason === "string" ? candidate.reason : undefined,
133
- }];
134
- })
135
- : [],
136
- };
137
- }
138
- catch (err) {
139
- log.warn({ err }, "malformed reviewer response");
140
- return { decisions: [], implicit_memories: [] };
141
- }
142
- }
143
- function parseFrictionResponse(raw) {
144
- try {
145
- const parsed = JSON.parse(raw);
146
- if (!Array.isArray(parsed)) {
147
- return [];
148
- }
149
- return parsed.flatMap((entry) => {
150
- if (!entry || typeof entry !== "object") {
151
- return [];
152
- }
153
- const candidate = entry;
154
- if (!isNonEmptyString(candidate.title) || typeof candidate.detail !== "string") {
155
- return [];
156
- }
157
- if (candidate.source !== "eot:friction") {
158
- return [];
159
- }
160
- return [{
161
- title: candidate.title.trim(),
162
- detail: candidate.detail,
163
- source: "eot:friction",
164
- }];
165
- }).slice(0, 3);
166
- }
167
- catch {
168
- return [];
169
- }
170
- }
171
- function isNonEmptyString(value) {
172
- return typeof value === "string" && value.trim().length > 0;
173
- }
174
- function isIsoTimestamp(value) {
175
- return !Number.isNaN(Date.parse(value));
176
- }
177
- function validateObservationPayload(payload) {
178
- const observation = payload;
179
- if (!isNonEmptyString(observation.content)) {
180
- return undefined;
181
- }
182
- return {
183
- ...observation,
184
- content: observation.content.trim(),
185
- };
186
- }
187
- function validateActionItemPayload(payload) {
188
- const actionItem = payload;
189
- if (!isNonEmptyString(actionItem.title)) {
190
- throw new Error("Action item proposal payload requires a non-empty title.");
191
- }
192
- if (actionItem.detail !== undefined && typeof actionItem.detail !== "string") {
193
- throw new Error("Action item proposal payload detail must be a string.");
194
- }
195
- if (actionItem.due_at !== undefined && (typeof actionItem.due_at !== "string" || !isIsoTimestamp(actionItem.due_at))) {
196
- throw new Error("Action item proposal payload due_at must be an ISO timestamp.");
197
- }
198
- if (actionItem.entity_id !== undefined && (!Number.isInteger(actionItem.entity_id) || actionItem.entity_id <= 0)) {
199
- throw new Error("Action item proposal payload entity_id must be a positive integer.");
200
- }
201
- if (actionItem.entity_id !== undefined && actionItem.entity_name !== undefined) {
202
- throw new Error("Action item proposal payload must provide either entity_id or entity_name/entity_kind, not both.");
203
- }
204
- const hasEntityName = actionItem.entity_name !== undefined;
205
- const hasEntityKind = actionItem.entity_kind !== undefined;
206
- if (hasEntityName !== hasEntityKind) {
207
- throw new Error("Action item proposal payload entity_name and entity_kind must be provided together.");
208
- }
209
- if (hasEntityName && !isNonEmptyString(actionItem.entity_name)) {
210
- throw new Error("Action item proposal payload entity_name must be non-empty when provided.");
211
- }
212
- if (hasEntityKind && !isNonEmptyString(actionItem.entity_kind)) {
213
- throw new Error("Action item proposal payload entity_kind must be non-empty when provided.");
214
- }
215
- return {
216
- title: actionItem.title.trim(),
217
- detail: actionItem.detail,
218
- due_at: actionItem.due_at,
219
- source: actionItem.source,
220
- entity_id: actionItem.entity_id,
221
- entity_name: actionItem.entity_name?.trim(),
222
- entity_kind: actionItem.entity_kind?.trim(),
223
- };
224
- }
225
- function getBoundScopeSlug(sourceAgent) {
226
- const agent = getAgent(sourceAgent);
227
- return agent?.scope ?? loadAgents().find((entry) => entry.slug === sourceAgent)?.scope;
228
- }
229
- function resolveAcceptedProposalScopeSlug(envelope, proposal) {
230
- if (isNonEmptyString(envelope.scope_slug)) {
231
- return envelope.scope_slug.trim();
232
- }
233
- const boundScope = getBoundScopeSlug(proposal.sourceAgent);
234
- if (boundScope) {
235
- return boundScope;
236
- }
237
- const activeScope = getActiveScope();
238
- if (activeScope) {
239
- return activeScope.slug;
240
- }
241
- if (proposal.scopeId) {
242
- const queuedScope = getScope(proposal.scopeId);
243
- if (queuedScope) {
244
- return queuedScope.slug;
245
- }
246
- }
247
- throw new Error("No memory scope could be resolved for this proposal.");
248
- }
249
- function resolveActiveScopeSlug() {
250
- const activeScope = getActiveScope();
251
- if (activeScope) {
252
- return activeScope.slug;
253
- }
254
- return getScope("chapterhouse")?.slug;
255
- }
256
- function resolveCallLLM(input) {
257
- return input.callLLM ?? (async ({ system, user, model }) => {
258
- const result = await runOneShotPrompt({
259
- client: input.copilotClient,
260
- model,
261
- system,
262
- user,
263
- expectJson: true,
264
- });
265
- return result.content;
266
- });
267
- }
268
- function rememberAcceptedMemory(kind, scopeSlug, payload, source, confidence, sourceAgent) {
269
- const scope = getScope(scopeSlug);
270
- if (!scope) {
271
- throw new Error(`Unknown memory scope '${scopeSlug}'.`);
272
- }
273
- if (kind === "observation") {
274
- const observation = validateObservationPayload(payload);
275
- if (!observation) {
276
- log.warn({ scopeSlug, source, sourceAgent }, "Skipping accepted observation proposal with empty content");
277
- return false;
278
- }
279
- const content = observation.content;
280
- recordObservation({
281
- scope_id: scope.id,
282
- entity_id: observation.entity_id,
283
- content,
284
- source: observation.source ?? source,
285
- confidence,
286
- });
287
- return true;
288
- }
289
- if (kind === "decision") {
290
- const decision = payload;
291
- recordDecision({
292
- scope_id: scope.id,
293
- title: decision.title,
294
- rationale: decision.rationale ?? decision.title,
295
- decided_at: decision.decided_at,
296
- });
297
- return true;
298
- }
299
- if (kind === "action_item") {
300
- const actionItem = validateActionItemPayload(payload);
301
- const entity = actionItem.entity_name && actionItem.entity_kind
302
- ? upsertEntity({
303
- scope_id: scope.id,
304
- kind: actionItem.entity_kind,
305
- name: actionItem.entity_name,
306
- confidence,
307
- })
308
- : undefined;
309
- recordActionItem({
310
- scope_id: scope.id,
311
- entity_id: entity?.id ?? actionItem.entity_id,
312
- title: actionItem.title,
313
- detail: actionItem.detail,
314
- due_at: actionItem.due_at,
315
- source: sourceAgent ? `subagent_proposal:${sourceAgent}` : actionItem.source ?? source,
316
- });
317
- return true;
318
- }
319
- const entity = payload;
320
- const entityKind = entity.entity_kind ?? entity.kind;
321
- if (!entityKind) {
322
- throw new Error("Entity proposal payload requires entity_kind.");
323
- }
324
- upsertEntity({
325
- scope_id: scope.id,
326
- kind: entityKind,
327
- name: entity.name,
328
- summary: entity.summary,
329
- confidence,
330
- });
331
- return true;
332
- }
333
- export async function runEndOfTaskMemoryHook(input) {
334
- const autoAcceptEnabled = isMemoryAutoAcceptEnabled();
335
- const summary = {
336
- task_id: input.taskId,
337
- proposals_total: 0,
338
- accepted: 0,
339
- rejected: 0,
340
- implicit_extracted: 0,
341
- auto_accept: autoAcceptEnabled,
342
- };
343
- if (!isEndOfTaskHookEnabled()) {
344
- return summary;
345
- }
346
- const proposals = listPendingMemoryProposalsForTask(input.taskId);
347
- summary.proposals_total = proposals.length;
348
- const callLLM = resolveCallLLM(input);
349
- const review = parseReviewerResponse(await callLLM({
350
- system: buildReviewerSystemPrompt(),
351
- user: buildReviewerUserPrompt(input.finalResult, proposals),
352
- model: input.model ?? config.copilotModel,
353
- }));
354
- const proposalsById = new Map(proposals.map((proposal) => [proposal.id, proposal]));
355
- const reviewedProposalIds = new Set();
356
- for (const decision of review.decisions) {
357
- const proposal = proposalsById.get(decision.proposal_id);
358
- if (!proposal) {
359
- continue;
360
- }
361
- reviewedProposalIds.add(proposal.id);
362
- if (decision.decision === "accept") {
363
- if (!autoAcceptEnabled) {
364
- summary.accepted++;
365
- }
366
- else {
367
- const envelope = parseEnvelope(proposal.payload);
368
- if (!envelope) {
369
- resolveInboxItem(proposal.id, "rejected", "Malformed memory proposal payload.");
370
- summary.rejected++;
371
- continue;
372
- }
373
- try {
374
- const accepted = rememberAcceptedMemory(envelope.kind, resolveAcceptedProposalScopeSlug(envelope, proposal), envelope.payload, `agent:${proposal.sourceAgent}`, envelope.confidence, proposal.sourceAgent);
375
- if (!accepted) {
376
- resolveInboxItem(proposal.id, "accepted", decision.reason);
377
- summary.accepted++;
378
- continue;
379
- }
380
- resolveInboxItem(proposal.id, "accepted", decision.reason);
381
- summary.accepted++;
382
- }
383
- catch (err) {
384
- const reason = err instanceof Error ? err.message : String(err);
385
- resolveInboxItem(proposal.id, "rejected", reason);
386
- summary.rejected++;
387
- }
388
- }
389
- continue;
390
- }
391
- summary.rejected++;
392
- if (autoAcceptEnabled) {
393
- resolveInboxItem(proposal.id, "rejected", decision.reason);
394
- }
395
- }
396
- for (const proposal of proposals) {
397
- if (reviewedProposalIds.has(proposal.id)) {
398
- continue;
399
- }
400
- summary.rejected++;
401
- if (autoAcceptEnabled) {
402
- resolveInboxItem(proposal.id, "rejected", "Reviewer did not select this proposal for acceptance.");
403
- }
404
- }
405
- if (autoAcceptEnabled) {
406
- for (const implicitMemory of review.implicit_memories) {
407
- if (rememberAcceptedMemory(implicitMemory.kind, implicitMemory.scope_slug, implicitMemory.payload, "agent:eot", implicitMemory.confidence)) {
408
- summary.implicit_extracted++;
409
- }
410
- }
411
- }
412
- await runFrictionHook({
413
- taskId: input.taskId,
414
- finalResult: input.finalResult,
415
- copilotClient: input.copilotClient,
416
- callLLM,
417
- model: input.model,
418
- });
419
- log.info(summary, "memory.eot.processed");
420
- input.onProcessed?.(summary);
421
- return summary;
422
- }
423
- export async function runFrictionHook(input) {
424
- try {
425
- if (!isFrictionHookEnabled()) {
426
- return;
427
- }
428
- if (input.finalResult.trim().length <= 100) {
429
- return;
430
- }
431
- const scopeSlug = resolveActiveScopeSlug();
432
- if (!scopeSlug) {
433
- return;
434
- }
435
- const callLLM = resolveCallLLM(input);
436
- const raw = await callLLM({
437
- system: buildFrictionSystemPrompt(),
438
- user: buildFrictionUserPrompt(input.finalResult),
439
- model: input.model ?? config.copilotModel,
440
- });
441
- const frictionItems = parseFrictionResponse(raw);
442
- for (const item of frictionItems) {
443
- try {
444
- rememberAcceptedMemory("action_item", scopeSlug, {
445
- title: item.title,
446
- detail: item.detail,
447
- source: item.source,
448
- }, "eot:friction");
449
- }
450
- catch (err) {
451
- log.warn({ err, taskId: input.taskId, title: item.title }, "friction hook: failed to record action item");
452
- }
453
- }
454
- }
455
- catch (err) {
456
- log.warn({ err, taskId: input.taskId }, "friction hook failed");
457
- }
458
- }
459
- //# sourceMappingURL=eot.js.map