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
|
@@ -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
|
package/dist/memory/entities.js
DELETED
|
@@ -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
|
package/dist/memory/eot.js
DELETED
|
@@ -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
|