chapterhouse 0.13.1 → 0.14.1
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 +92 -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/memory-assets/domain-skill.md +38 -0
- package/memory-assets/seed/cog-meta/improvements.md +8 -0
- package/memory-assets/seed/cog-meta/patterns.md +5 -0
- package/memory-assets/seed/cog-meta/reflect-cursor.md +4 -0
- package/memory-assets/seed/cog-meta/scenario-calibration.md +14 -0
- package/memory-assets/seed/cog-meta/self-observations.md +4 -0
- package/memory-assets/seed/domains.yml +19 -0
- package/memory-assets/seed/glacier/index.md +6 -0
- package/memory-assets/seed/hot-memory.md +5 -0
- package/memory-assets/seed/link-index.md +6 -0
- package/memory-assets/system-instructions.md +214 -0
- package/memory-assets/templates/action-items.md +8 -0
- package/memory-assets/templates/entities.md +4 -0
- package/memory-assets/templates/generic.md +2 -0
- package/memory-assets/templates/hot-memory.md +4 -0
- package/memory-assets/templates/observations.md +4 -0
- package/package.json +2 -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
package/web/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=JetBrains+Mono:wght@300;400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
10
|
<title>Chapterhouse</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-aCcfpaLM.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-DmO1TJpc.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { getActiveScope, setActiveScope } from "../../memory/active-scope.js";
|
|
4
|
-
import { recordActionItem } from "../../memory/action-items.js";
|
|
5
|
-
import { recordDecision } from "../../memory/decisions.js";
|
|
6
|
-
import { upsertEntity } from "../../memory/entities.js";
|
|
7
|
-
import { handleGitCommitHook, handlePrMergeHook } from "../../memory/hooks.js";
|
|
8
|
-
import { getInboxItem, listPendingInboxItems, resolveInboxItem } from "../../memory/inbox.js";
|
|
9
|
-
import { recordObservation } from "../../memory/observations.js";
|
|
10
|
-
import { createScope, getScope, listScopes } from "../../memory/scopes.js";
|
|
11
|
-
import { ActiveMemoryScopeSchema, InboxRouteResponseSchema, MemoryEntriesSchema, MemoryHookResponseSchema, MemoryInboxSchema, MemoryRememberResponseSchema, MemoryScopeCreateResponseSchema, MemoryScopeListSchema, SetActiveScopeResponseSchema, } from "../../shared/api-schemas.js";
|
|
12
|
-
import { getDb } from "../../store/db.js";
|
|
13
|
-
import { childLogger } from "../../util/logger.js";
|
|
14
|
-
import { parseRequest } from "../errors.js";
|
|
15
|
-
import { sendJson } from "../send-json.js";
|
|
16
|
-
const log = childLogger("server");
|
|
17
|
-
const requiredString = (message) => z.string({ error: message }).trim().min(1, message);
|
|
18
|
-
const scopeCreateSchema = z.object({
|
|
19
|
-
slug: requiredString("Missing 'slug' in request body")
|
|
20
|
-
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Scope slug must be unique kebab-case"),
|
|
21
|
-
title: requiredString("Missing 'title' in request body"),
|
|
22
|
-
description: z.string().optional(),
|
|
23
|
-
}).strict();
|
|
24
|
-
const setActiveScopeSchema = z.object({
|
|
25
|
-
scope: z.string().nullable(),
|
|
26
|
-
});
|
|
27
|
-
const memoryEntriesQuerySchema = z.object({
|
|
28
|
-
store: z.enum(["observations", "decisions", "entities", "action_items", "patterns"]).optional(),
|
|
29
|
-
kind: z.enum(["observation", "decision", "entity", "action_item", "pattern"]).optional(),
|
|
30
|
-
tier: z.enum(["hot", "warm", "cold"]).optional(),
|
|
31
|
-
cursor: z.string().regex(/^\d+$/, "cursor must be a positive integer").optional(),
|
|
32
|
-
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
33
|
-
});
|
|
34
|
-
const memoryRememberSchema = z.object({
|
|
35
|
-
content: requiredString("Missing 'content' in request body"),
|
|
36
|
-
kind: z.enum(["observation", "decision"]).optional(),
|
|
37
|
-
entity_name: z.string().optional(),
|
|
38
|
-
entity_kind: z.string().optional(),
|
|
39
|
-
title: z.string().optional(),
|
|
40
|
-
decided_at: z.string().optional(),
|
|
41
|
-
tier: z.enum(["hot", "warm", "cold"]).optional(),
|
|
42
|
-
});
|
|
43
|
-
const inboxRouteSchema = z.object({
|
|
44
|
-
action: z.enum(["accept", "reject", "route"]),
|
|
45
|
-
reason: z.string().optional(),
|
|
46
|
-
target_scope: z.string().optional(),
|
|
47
|
-
});
|
|
48
|
-
const inboxRejectSchema = z.object({
|
|
49
|
-
reason: requiredString("Missing 'reason' in request body"),
|
|
50
|
-
});
|
|
51
|
-
const gitCommitHookSchema = z.object({
|
|
52
|
-
message: requiredString("Missing 'message' in request body"),
|
|
53
|
-
stat: z.string().optional(),
|
|
54
|
-
});
|
|
55
|
-
const prMergeHookSchema = z.object({
|
|
56
|
-
number: z.number({ error: "Missing or invalid 'number' in request body" }).int().positive(),
|
|
57
|
-
title: requiredString("Missing 'title' in request body"),
|
|
58
|
-
body: z.string().optional(),
|
|
59
|
-
files_changed: z.array(z.string()).optional(),
|
|
60
|
-
});
|
|
61
|
-
function sendError(res, status, message) {
|
|
62
|
-
res.status(status).type("application/json").send(JSON.stringify({ error: message }));
|
|
63
|
-
}
|
|
64
|
-
function scopeSlugForInboxItem(item) {
|
|
65
|
-
const envelope = JSON.parse(item.payload);
|
|
66
|
-
if (typeof envelope.scope_slug === "string" && envelope.scope_slug.trim()) {
|
|
67
|
-
return envelope.scope_slug;
|
|
68
|
-
}
|
|
69
|
-
if (item.scopeId) {
|
|
70
|
-
const scope = getScope(item.scopeId);
|
|
71
|
-
if (scope) {
|
|
72
|
-
return scope.slug;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const activeScope = getActiveScope();
|
|
76
|
-
if (activeScope) {
|
|
77
|
-
return activeScope.slug;
|
|
78
|
-
}
|
|
79
|
-
const fallback = getScope("chapterhouse");
|
|
80
|
-
if (fallback) {
|
|
81
|
-
return fallback.slug;
|
|
82
|
-
}
|
|
83
|
-
throw new Error("No memory scope could be resolved for this proposal.");
|
|
84
|
-
}
|
|
85
|
-
function rememberAcceptedProposal(kind, scopeSlug, payload, source, confidence, sourceAgent) {
|
|
86
|
-
const scope = getScope(scopeSlug);
|
|
87
|
-
if (!scope) {
|
|
88
|
-
throw new Error(`Unknown memory scope '${scopeSlug}'.`);
|
|
89
|
-
}
|
|
90
|
-
const payloadRecord = payload;
|
|
91
|
-
if (kind === "observation") {
|
|
92
|
-
const content = typeof payloadRecord.content === "string" ? payloadRecord.content.trim() : "";
|
|
93
|
-
if (!content) {
|
|
94
|
-
throw new Error("Observation proposal payload requires content.");
|
|
95
|
-
}
|
|
96
|
-
recordObservation({
|
|
97
|
-
scope_id: scope.id,
|
|
98
|
-
entity_id: typeof payloadRecord.entity_id === "number" ? payloadRecord.entity_id : undefined,
|
|
99
|
-
content,
|
|
100
|
-
source: typeof payloadRecord.source === "string" ? payloadRecord.source : source,
|
|
101
|
-
confidence,
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (kind === "decision") {
|
|
106
|
-
const title = typeof payloadRecord.title === "string" ? payloadRecord.title.trim() : "";
|
|
107
|
-
if (!title) {
|
|
108
|
-
throw new Error("Decision proposal payload requires title.");
|
|
109
|
-
}
|
|
110
|
-
recordDecision({
|
|
111
|
-
scope_id: scope.id,
|
|
112
|
-
title,
|
|
113
|
-
rationale: typeof payloadRecord.rationale === "string" ? payloadRecord.rationale : title,
|
|
114
|
-
decided_at: typeof payloadRecord.decided_at === "string" ? payloadRecord.decided_at : undefined,
|
|
115
|
-
});
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (kind === "action_item") {
|
|
119
|
-
const title = typeof payloadRecord.title === "string" ? payloadRecord.title.trim() : "";
|
|
120
|
-
if (!title) {
|
|
121
|
-
throw new Error("Action item proposal payload requires title.");
|
|
122
|
-
}
|
|
123
|
-
const entity = typeof payloadRecord.entity_name === "string" && typeof payloadRecord.entity_kind === "string"
|
|
124
|
-
? upsertEntity({
|
|
125
|
-
scope_id: scope.id,
|
|
126
|
-
kind: payloadRecord.entity_kind,
|
|
127
|
-
name: payloadRecord.entity_name,
|
|
128
|
-
confidence,
|
|
129
|
-
})
|
|
130
|
-
: undefined;
|
|
131
|
-
recordActionItem({
|
|
132
|
-
scope_id: scope.id,
|
|
133
|
-
entity_id: entity?.id ?? (typeof payloadRecord.entity_id === "number" ? payloadRecord.entity_id : undefined),
|
|
134
|
-
title,
|
|
135
|
-
detail: typeof payloadRecord.detail === "string" ? payloadRecord.detail : undefined,
|
|
136
|
-
due_at: typeof payloadRecord.due_at === "string" ? payloadRecord.due_at : undefined,
|
|
137
|
-
source: sourceAgent ? `subagent_proposal:${sourceAgent}` : source,
|
|
138
|
-
});
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const entityKind = typeof payloadRecord.entity_kind === "string" ? payloadRecord.entity_kind :
|
|
142
|
-
typeof payloadRecord.kind === "string" ? payloadRecord.kind :
|
|
143
|
-
"";
|
|
144
|
-
const name = typeof payloadRecord.name === "string" ? payloadRecord.name.trim() : "";
|
|
145
|
-
if (!entityKind || !name) {
|
|
146
|
-
throw new Error("Entity proposal payload requires entity_kind and name.");
|
|
147
|
-
}
|
|
148
|
-
upsertEntity({
|
|
149
|
-
scope_id: scope.id,
|
|
150
|
-
kind: entityKind,
|
|
151
|
-
name,
|
|
152
|
-
summary: typeof payloadRecord.summary === "string" ? payloadRecord.summary : undefined,
|
|
153
|
-
confidence,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function acceptInboxItem(id) {
|
|
157
|
-
const item = getInboxItem(id);
|
|
158
|
-
if (!item) {
|
|
159
|
-
throw Object.assign(new Error(`Inbox item '${id}' not found`), { statusCode: 404 });
|
|
160
|
-
}
|
|
161
|
-
if (item.status !== "pending") {
|
|
162
|
-
throw Object.assign(new Error(`Inbox item '${id}' is already resolved`), { statusCode: 409 });
|
|
163
|
-
}
|
|
164
|
-
const envelope = JSON.parse(item.payload);
|
|
165
|
-
rememberAcceptedProposal(envelope.kind, scopeSlugForInboxItem(item), envelope.payload, `agent:${item.sourceAgent}`, envelope.confidence, item.sourceAgent);
|
|
166
|
-
resolveInboxItem(id, "accepted", "Accepted via web UI");
|
|
167
|
-
}
|
|
168
|
-
function rejectInboxItem(id, reason) {
|
|
169
|
-
const item = getInboxItem(id);
|
|
170
|
-
if (!item) {
|
|
171
|
-
throw Object.assign(new Error(`Inbox item '${id}' not found`), { statusCode: 404 });
|
|
172
|
-
}
|
|
173
|
-
if (item.status !== "pending") {
|
|
174
|
-
throw Object.assign(new Error(`Inbox item '${id}' is already resolved`), { statusCode: 409 });
|
|
175
|
-
}
|
|
176
|
-
resolveInboxItem(id, "rejected", reason);
|
|
177
|
-
}
|
|
178
|
-
function parseInboxId(raw) {
|
|
179
|
-
const id = Number(Array.isArray(raw) ? raw[0] : raw);
|
|
180
|
-
if (!Number.isInteger(id) || id <= 0) {
|
|
181
|
-
throw Object.assign(new Error("Invalid inbox item id"), { statusCode: 400 });
|
|
182
|
-
}
|
|
183
|
-
return id;
|
|
184
|
-
}
|
|
185
|
-
export function createMemoryRouter(options) {
|
|
186
|
-
const { authMiddleware } = options;
|
|
187
|
-
const router = Router();
|
|
188
|
-
router.get("/api/memory/active-scope", (_req, res) => {
|
|
189
|
-
const activeScope = getActiveScope();
|
|
190
|
-
if (!activeScope) {
|
|
191
|
-
sendJson(res, ActiveMemoryScopeSchema, null);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
sendJson(res, ActiveMemoryScopeSchema, {
|
|
195
|
-
slug: activeScope.slug,
|
|
196
|
-
title: activeScope.title,
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
router.post("/api/memory/active-scope", (req, res) => {
|
|
200
|
-
const body = parseRequest(setActiveScopeSchema, req.body ?? {});
|
|
201
|
-
try {
|
|
202
|
-
const scope = setActiveScope(body.scope);
|
|
203
|
-
sendJson(res, SetActiveScopeResponseSchema, { ok: true, scope: scope?.slug ?? null });
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
sendError(res, 404, err instanceof Error ? err.message : String(err));
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
router.get("/api/memory/scopes", (_req, res) => {
|
|
210
|
-
const db = getDb();
|
|
211
|
-
const activeScope = getActiveScope();
|
|
212
|
-
const scopes = listScopes();
|
|
213
|
-
const result = scopes.map((scope) => {
|
|
214
|
-
const counts = {
|
|
215
|
-
observations: db.prepare(`SELECT COUNT(*) AS count FROM mem_observations WHERE scope_id = ?`).get(scope.id).count,
|
|
216
|
-
decisions: db.prepare(`SELECT COUNT(*) AS count FROM mem_decisions WHERE scope_id = ?`).get(scope.id).count,
|
|
217
|
-
entities: db.prepare(`SELECT COUNT(*) AS count FROM mem_entities WHERE scope_id = ?`).get(scope.id).count,
|
|
218
|
-
action_items: db.prepare(`SELECT COUNT(*) AS count FROM mem_action_items WHERE scope_id = ?`).get(scope.id).count,
|
|
219
|
-
patterns: db.prepare(`SELECT COUNT(*) AS count FROM mem_patterns WHERE scope_id = ?`).get(scope.id).count,
|
|
220
|
-
};
|
|
221
|
-
return {
|
|
222
|
-
slug: scope.slug,
|
|
223
|
-
title: scope.title,
|
|
224
|
-
description: scope.description,
|
|
225
|
-
active: activeScope?.slug === scope.slug,
|
|
226
|
-
counts,
|
|
227
|
-
};
|
|
228
|
-
});
|
|
229
|
-
sendJson(res, MemoryScopeListSchema, { scopes: result });
|
|
230
|
-
});
|
|
231
|
-
router.get("/api/memory/inbox", (_req, res) => {
|
|
232
|
-
const items = listPendingInboxItems();
|
|
233
|
-
const result = items.map((item) => ({
|
|
234
|
-
id: item.id,
|
|
235
|
-
scope_slug: item.scopeId
|
|
236
|
-
? getDb().prepare(`SELECT slug FROM mem_scopes WHERE id = ?`).get(item.scopeId)?.slug ?? null
|
|
237
|
-
: null,
|
|
238
|
-
kind: item.kind,
|
|
239
|
-
payload: item.payload,
|
|
240
|
-
source_agent: item.sourceAgent,
|
|
241
|
-
created_at: item.createdAt,
|
|
242
|
-
}));
|
|
243
|
-
sendJson(res, MemoryInboxSchema, { items: result, total: result.length });
|
|
244
|
-
});
|
|
245
|
-
router.post("/api/memory/inbox/:id/route", (req, res) => {
|
|
246
|
-
const id = parseInboxId(req.params.id);
|
|
247
|
-
const body = parseRequest(inboxRouteSchema, req.body ?? {});
|
|
248
|
-
const item = getInboxItem(id);
|
|
249
|
-
if (!item) {
|
|
250
|
-
sendError(res, 404, `Inbox item '${id}' not found`);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (item.status !== "pending") {
|
|
254
|
-
sendError(res, 409, `Inbox item '${id}' is already resolved`);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (body.action === "accept") {
|
|
258
|
-
acceptInboxItem(id);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
rejectInboxItem(id, body.reason ?? "Rejected via web UI");
|
|
262
|
-
}
|
|
263
|
-
log.info({ id, action: body.action }, "inbox item routed via web UI");
|
|
264
|
-
sendJson(res, InboxRouteResponseSchema, { ok: true });
|
|
265
|
-
});
|
|
266
|
-
router.post("/api/memory/inbox/:id/accept", (req, res) => {
|
|
267
|
-
try {
|
|
268
|
-
const id = parseInboxId(req.params.id);
|
|
269
|
-
acceptInboxItem(id);
|
|
270
|
-
log.info({ id }, "inbox item accepted via web UI");
|
|
271
|
-
sendJson(res, InboxRouteResponseSchema, { ok: true });
|
|
272
|
-
}
|
|
273
|
-
catch (err) {
|
|
274
|
-
const statusCode = typeof err === "object" && err !== null && "statusCode" in err && typeof err.statusCode === "number"
|
|
275
|
-
? err.statusCode
|
|
276
|
-
: 400;
|
|
277
|
-
sendError(res, statusCode, err instanceof Error ? err.message : String(err));
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
router.post("/api/memory/inbox/:id/reject", (req, res) => {
|
|
281
|
-
const body = parseRequest(inboxRejectSchema, req.body ?? {});
|
|
282
|
-
try {
|
|
283
|
-
const id = parseInboxId(req.params.id);
|
|
284
|
-
rejectInboxItem(id, body.reason);
|
|
285
|
-
log.info({ id }, "inbox item rejected via web UI");
|
|
286
|
-
sendJson(res, InboxRouteResponseSchema, { ok: true });
|
|
287
|
-
}
|
|
288
|
-
catch (err) {
|
|
289
|
-
const statusCode = typeof err === "object" && err !== null && "statusCode" in err && typeof err.statusCode === "number"
|
|
290
|
-
? err.statusCode
|
|
291
|
-
: 400;
|
|
292
|
-
sendError(res, statusCode, err instanceof Error ? err.message : String(err));
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
router.get("/api/memory/:scope", (req, res) => {
|
|
296
|
-
const scopeSlug = String(req.params.scope);
|
|
297
|
-
const scope = getScope(scopeSlug);
|
|
298
|
-
if (!scope) {
|
|
299
|
-
sendError(res, 404, `Memory scope '${scopeSlug}' not found`);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
const query = parseRequest(memoryEntriesQuerySchema, req.query);
|
|
303
|
-
const store = query.store ?? (query.kind === "observation" ? "observations" :
|
|
304
|
-
query.kind === "decision" ? "decisions" :
|
|
305
|
-
query.kind === "entity" ? "entities" :
|
|
306
|
-
query.kind === "action_item" ? "action_items" :
|
|
307
|
-
query.kind === "pattern" ? "patterns" :
|
|
308
|
-
"observations");
|
|
309
|
-
const tier = query.tier;
|
|
310
|
-
const cursor = query.cursor ? Number(query.cursor) : undefined;
|
|
311
|
-
const limit = query.limit ?? 100;
|
|
312
|
-
const db = getDb();
|
|
313
|
-
let entries;
|
|
314
|
-
let total;
|
|
315
|
-
let nextCursor;
|
|
316
|
-
if (store === "observations") {
|
|
317
|
-
const cursorClause = cursor ? "AND id < ?" : "";
|
|
318
|
-
const params = tier ? [scope.id, tier] : [scope.id];
|
|
319
|
-
const pageParams = cursor ? [...params, cursor, limit + 1] : [...params, limit + 1];
|
|
320
|
-
if (tier) {
|
|
321
|
-
entries = db.prepare(`SELECT id, content, source, tier, entity_id, created_at FROM mem_observations WHERE scope_id = ? AND tier = ? AND archived_at IS NULL ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
322
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_observations WHERE scope_id = ? AND tier = ? AND archived_at IS NULL`).get(scope.id, tier).n;
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
entries = db.prepare(`SELECT id, content, source, tier, entity_id, created_at FROM mem_observations WHERE scope_id = ? AND archived_at IS NULL ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
326
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_observations WHERE scope_id = ? AND archived_at IS NULL`).get(scope.id).n;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
else if (store === "decisions") {
|
|
330
|
-
const cursorClause = cursor ? "AND id < ?" : "";
|
|
331
|
-
const params = tier ? [scope.id, tier] : [scope.id];
|
|
332
|
-
const pageParams = cursor ? [...params, cursor, limit + 1] : [...params, limit + 1];
|
|
333
|
-
if (tier) {
|
|
334
|
-
entries = db.prepare(`SELECT id, title, rationale, tier, entity_id, decided_at, created_at FROM mem_decisions WHERE scope_id = ? AND tier = ? AND archived_at IS NULL ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
335
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_decisions WHERE scope_id = ? AND tier = ? AND archived_at IS NULL`).get(scope.id, tier).n;
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
entries = db.prepare(`SELECT id, title, rationale, tier, entity_id, decided_at, created_at FROM mem_decisions WHERE scope_id = ? AND archived_at IS NULL ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
339
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_decisions WHERE scope_id = ? AND archived_at IS NULL`).get(scope.id).n;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
else if (store === "entities") {
|
|
343
|
-
const cursorClause = cursor ? "AND id < ?" : "";
|
|
344
|
-
const params = tier ? [scope.id, tier] : [scope.id];
|
|
345
|
-
const pageParams = cursor ? [...params, cursor, limit + 1] : [...params, limit + 1];
|
|
346
|
-
if (tier) {
|
|
347
|
-
entries = db.prepare(`SELECT id, kind, name, summary, tier, created_at, updated_at FROM mem_entities WHERE scope_id = ? AND tier = ? ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
348
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_entities WHERE scope_id = ? AND tier = ?`).get(scope.id, tier).n;
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
entries = db.prepare(`SELECT id, kind, name, summary, tier, created_at, updated_at FROM mem_entities WHERE scope_id = ? ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
352
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_entities WHERE scope_id = ?`).get(scope.id).n;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
else if (store === "action_items") {
|
|
356
|
-
const cursorClause = cursor ? "AND id < ?" : "";
|
|
357
|
-
const params = tier ? [scope.id, tier] : [scope.id];
|
|
358
|
-
const pageParams = cursor ? [...params, cursor, limit + 1] : [...params, limit + 1];
|
|
359
|
-
if (tier) {
|
|
360
|
-
entries = db.prepare(`SELECT id, title, detail, status, tier, due_at, entity_id, created_at FROM mem_action_items WHERE scope_id = ? AND tier = ? ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
361
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_action_items WHERE scope_id = ? AND tier = ?`).get(scope.id, tier).n;
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
entries = db.prepare(`SELECT id, title, detail, status, tier, due_at, entity_id, created_at FROM mem_action_items WHERE scope_id = ? ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
365
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_action_items WHERE scope_id = ?`).get(scope.id).n;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
const cursorClause = cursor ? "AND id < ?" : "";
|
|
370
|
-
const params = tier ? [scope.id, tier] : [scope.id];
|
|
371
|
-
const pageParams = cursor ? [...params, cursor, limit + 1] : [...params, limit + 1];
|
|
372
|
-
if (tier) {
|
|
373
|
-
entries = db.prepare(`SELECT id, title, summary, source_observation_ids, confidence, tier, created_at, last_updated FROM mem_patterns WHERE scope_id = ? AND tier = ? ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
374
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_patterns WHERE scope_id = ? AND tier = ?`).get(scope.id, tier).n;
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
entries = db.prepare(`SELECT id, title, summary, source_observation_ids, confidence, tier, created_at, last_updated FROM mem_patterns WHERE scope_id = ? ${cursorClause} ORDER BY id DESC LIMIT ?`).all(...pageParams);
|
|
378
|
-
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_patterns WHERE scope_id = ?`).get(scope.id).n;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (entries.length > limit) {
|
|
382
|
-
const extra = entries.pop();
|
|
383
|
-
const last = entries[entries.length - 1];
|
|
384
|
-
nextCursor = String(last?.id ?? extra?.id ?? "");
|
|
385
|
-
}
|
|
386
|
-
sendJson(res, MemoryEntriesSchema, { entries: entries, total, ...(nextCursor ? { nextCursor } : {}) });
|
|
387
|
-
});
|
|
388
|
-
router.post("/api/memory/:scope/remember", (req, res) => {
|
|
389
|
-
const scopeSlug = String(req.params.scope);
|
|
390
|
-
const scope = getScope(scopeSlug);
|
|
391
|
-
if (!scope) {
|
|
392
|
-
sendError(res, 404, `Memory scope '${scopeSlug}' not found`);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const body = parseRequest(memoryRememberSchema, req.body ?? {});
|
|
396
|
-
if (body.entity_name && !body.entity_kind) {
|
|
397
|
-
sendError(res, 400, "entity_kind is required when entity_name is provided");
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const kind = body.kind ?? "observation";
|
|
401
|
-
const entity = body.entity_name
|
|
402
|
-
? upsertEntity({
|
|
403
|
-
scope_id: scope.id,
|
|
404
|
-
kind: body.entity_kind,
|
|
405
|
-
name: body.entity_name,
|
|
406
|
-
tier: body.tier ?? "warm",
|
|
407
|
-
})
|
|
408
|
-
: undefined;
|
|
409
|
-
if (kind === "decision") {
|
|
410
|
-
if (!body.title) {
|
|
411
|
-
sendError(res, 400, "title is required when kind='decision'");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const decision = recordDecision({
|
|
415
|
-
scope_id: scope.id,
|
|
416
|
-
entity_id: entity?.id,
|
|
417
|
-
title: body.title,
|
|
418
|
-
rationale: body.content,
|
|
419
|
-
decided_at: body.decided_at ?? new Date().toISOString().slice(0, 10),
|
|
420
|
-
tier: body.tier ?? "warm",
|
|
421
|
-
});
|
|
422
|
-
log.info({ id: decision.id, scope: scopeSlug, kind }, "memory written via web UI");
|
|
423
|
-
sendJson(res, MemoryRememberResponseSchema, { ok: true, id: String(decision.id) });
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const observation = recordObservation({
|
|
427
|
-
scope_id: scope.id,
|
|
428
|
-
entity_id: entity?.id,
|
|
429
|
-
content: body.content,
|
|
430
|
-
source: "agent:web-ui",
|
|
431
|
-
tier: body.tier ?? "warm",
|
|
432
|
-
});
|
|
433
|
-
log.info({ id: observation.id, scope: scopeSlug, kind }, "memory written via web UI");
|
|
434
|
-
sendJson(res, MemoryRememberResponseSchema, { ok: true, id: String(observation.id) });
|
|
435
|
-
});
|
|
436
|
-
router.post("/api/memory/hooks/git-commit", authMiddleware, (req, res, next) => {
|
|
437
|
-
const body = parseRequest(gitCommitHookSchema, req.body ?? {});
|
|
438
|
-
handleGitCommitHook({ message: body.message, stat: body.stat })
|
|
439
|
-
.then((result) => sendJson(res, MemoryHookResponseSchema, { ok: true, observation_id: result.observation_id }))
|
|
440
|
-
.catch(next);
|
|
441
|
-
});
|
|
442
|
-
router.post("/api/memory/hooks/pr-merge", authMiddleware, (req, res, next) => {
|
|
443
|
-
const body = parseRequest(prMergeHookSchema, req.body ?? {});
|
|
444
|
-
handlePrMergeHook({
|
|
445
|
-
number: body.number,
|
|
446
|
-
title: body.title,
|
|
447
|
-
body: body.body,
|
|
448
|
-
files_changed: body.files_changed,
|
|
449
|
-
})
|
|
450
|
-
.then((result) => sendJson(res, MemoryHookResponseSchema, { ok: true, observation_id: result.observation_id }))
|
|
451
|
-
.catch(next);
|
|
452
|
-
});
|
|
453
|
-
router.post("/api/scopes", (req, res) => {
|
|
454
|
-
const body = parseRequest(scopeCreateSchema, req.body ?? {});
|
|
455
|
-
if (getScope(body.slug)) {
|
|
456
|
-
sendError(res, 409, `Memory scope '${body.slug}' already exists`);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const scope = createScope({
|
|
460
|
-
slug: body.slug,
|
|
461
|
-
title: body.title,
|
|
462
|
-
description: body.description ?? "",
|
|
463
|
-
keywords: [body.slug],
|
|
464
|
-
});
|
|
465
|
-
res.status(201);
|
|
466
|
-
sendJson(res, MemoryScopeCreateResponseSchema, {
|
|
467
|
-
slug: scope.slug,
|
|
468
|
-
title: scope.title,
|
|
469
|
-
description: scope.description,
|
|
470
|
-
active: scope.active,
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
return router;
|
|
474
|
-
}
|
|
475
|
-
//# sourceMappingURL=memory.js.map
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import express from "express";
|
|
3
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
const repoRoot = process.cwd();
|
|
7
|
-
const sandboxRoot = join(repoRoot, ".test-work", `api-memory-${process.pid}`);
|
|
8
|
-
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
9
|
-
async function withMemoryApi(run) {
|
|
10
|
-
const { createMemoryRouter } = await import(new URL(`./memory.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
11
|
-
const authMiddleware = (_req, _res, next) => next();
|
|
12
|
-
const app = express();
|
|
13
|
-
app.use(express.json());
|
|
14
|
-
app.use(createMemoryRouter({ authMiddleware }));
|
|
15
|
-
const server = app.listen(0, "127.0.0.1");
|
|
16
|
-
await new Promise((resolve) => server.once("listening", resolve));
|
|
17
|
-
const address = server.address();
|
|
18
|
-
assert.ok(address && typeof address === "object");
|
|
19
|
-
try {
|
|
20
|
-
await run(`http://127.0.0.1:${address.port}`);
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
await new Promise((resolve, reject) => {
|
|
24
|
-
server.close((error) => (error ? reject(error) : resolve()));
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
async function loadDbModule() {
|
|
29
|
-
return await import(new URL(`../../store/db.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
30
|
-
}
|
|
31
|
-
test.beforeEach(async () => {
|
|
32
|
-
const dbModule = await loadDbModule();
|
|
33
|
-
dbModule.closeDb();
|
|
34
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
35
|
-
mkdirSync(sandboxRoot, { recursive: true });
|
|
36
|
-
});
|
|
37
|
-
test.after(async () => {
|
|
38
|
-
const dbModule = await loadDbModule();
|
|
39
|
-
dbModule.closeDb();
|
|
40
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
41
|
-
});
|
|
42
|
-
test("memory API lists pattern entries and accepts PRD kind filters", async () => {
|
|
43
|
-
const { getDb } = await loadDbModule();
|
|
44
|
-
const db = getDb();
|
|
45
|
-
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
46
|
-
db.prepare(`
|
|
47
|
-
INSERT INTO mem_entities (scope_id, kind, name, summary, tier, created_at, updated_at)
|
|
48
|
-
VALUES (?, 'project', 'Memory API Test Entity', 'Team AI assistant', 'hot', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
49
|
-
`).run(scope.id);
|
|
50
|
-
db.prepare(`
|
|
51
|
-
INSERT INTO mem_patterns (scope_id, title, summary, source_observation_ids, confidence, tier, created_at, last_updated)
|
|
52
|
-
VALUES (?, 'Agents prefer scoped memory', 'Repeated observations show agents should stay scoped.', '[1,2,3]', 0.82, 'warm', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
53
|
-
`).run(scope.id);
|
|
54
|
-
await withMemoryApi(async (baseUrl) => {
|
|
55
|
-
const entityResponse = await fetch(`${baseUrl}/api/memory/chapterhouse?kind=entity&tier=hot`);
|
|
56
|
-
assert.equal(entityResponse.status, 200);
|
|
57
|
-
const entityPayload = await entityResponse.json();
|
|
58
|
-
assert.equal(entityPayload.total, 1);
|
|
59
|
-
assert.equal(entityPayload.entries[0]?.name, "Memory API Test Entity");
|
|
60
|
-
const patternResponse = await fetch(`${baseUrl}/api/memory/chapterhouse?kind=pattern`);
|
|
61
|
-
assert.equal(patternResponse.status, 200);
|
|
62
|
-
const patternPayload = await patternResponse.json();
|
|
63
|
-
assert.equal(patternPayload.total, 1);
|
|
64
|
-
assert.equal(patternPayload.entries[0]?.title, "Agents prefer scoped memory");
|
|
65
|
-
assert.equal(patternPayload.entries[0]?.summary, "Repeated observations show agents should stay scoped.");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
test("memory API accepts and rejects inbox proposals through explicit endpoints", async () => {
|
|
69
|
-
const { getDb } = await loadDbModule();
|
|
70
|
-
const db = getDb();
|
|
71
|
-
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
72
|
-
const first = db.prepare(`
|
|
73
|
-
INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, status, created_at)
|
|
74
|
-
VALUES (?, 'memory_proposal', '{"kind":"observation","payload":{"content":"keep"}}', 'coder', 'pending', CURRENT_TIMESTAMP)
|
|
75
|
-
`).run(scope.id).lastInsertRowid;
|
|
76
|
-
const second = db.prepare(`
|
|
77
|
-
INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, status, created_at)
|
|
78
|
-
VALUES (?, 'memory_proposal', '{"kind":"observation","payload":{"content":"drop"}}', 'coder', 'pending', CURRENT_TIMESTAMP)
|
|
79
|
-
`).run(scope.id).lastInsertRowid;
|
|
80
|
-
await withMemoryApi(async (baseUrl) => {
|
|
81
|
-
const acceptResponse = await fetch(`${baseUrl}/api/memory/inbox/${first}/accept`, { method: "POST" });
|
|
82
|
-
assert.equal(acceptResponse.status, 200);
|
|
83
|
-
assert.deepEqual(await acceptResponse.json(), { ok: true });
|
|
84
|
-
const rejectResponse = await fetch(`${baseUrl}/api/memory/inbox/${second}/reject`, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: { "content-type": "application/json" },
|
|
87
|
-
body: JSON.stringify({ reason: "Not durable enough" }),
|
|
88
|
-
});
|
|
89
|
-
assert.equal(rejectResponse.status, 200);
|
|
90
|
-
assert.deepEqual(await rejectResponse.json(), { ok: true });
|
|
91
|
-
});
|
|
92
|
-
const rows = db.prepare(`
|
|
93
|
-
SELECT id, status, resolution_reason
|
|
94
|
-
FROM mem_inbox
|
|
95
|
-
ORDER BY id
|
|
96
|
-
`).all();
|
|
97
|
-
const acceptedObservation = db.prepare(`
|
|
98
|
-
SELECT content, source
|
|
99
|
-
FROM mem_observations
|
|
100
|
-
WHERE content = 'keep'
|
|
101
|
-
`).get();
|
|
102
|
-
assert.deepEqual(rows, [
|
|
103
|
-
{ id: Number(first), status: "accepted", resolution_reason: "Accepted via web UI" },
|
|
104
|
-
{ id: Number(second), status: "rejected", resolution_reason: "Not durable enough" },
|
|
105
|
-
]);
|
|
106
|
-
assert.deepEqual(acceptedObservation, { content: "keep", source: "agent:coder" });
|
|
107
|
-
});
|
|
108
|
-
//# sourceMappingURL=memory.test.js.map
|