chapterhouse 0.9.2 → 0.10.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/README.md +1 -1
- package/dist/api/auth.js +11 -1
- package/dist/api/auth.test.js +29 -0
- package/dist/api/errors.js +23 -0
- package/dist/api/route-coverage.test.js +61 -21
- package/dist/api/routes/agents.js +472 -0
- package/dist/api/routes/memory.js +299 -0
- package/dist/api/routes/projects.js +170 -0
- package/dist/api/routes/sessions.js +347 -0
- package/dist/api/routes/system.js +82 -0
- package/dist/api/routes/wiki.js +455 -0
- package/dist/api/routes/wiki.test.js +49 -0
- package/dist/api/send-json.js +16 -0
- package/dist/api/send-json.test.js +18 -0
- package/dist/api/server-runtime.js +45 -3
- package/dist/api/server.js +34 -1764
- package/dist/api/server.test.js +239 -8
- package/dist/api/sse-hub.js +37 -0
- package/dist/cli.js +1 -1
- package/dist/config.js +151 -58
- package/dist/config.test.js +29 -0
- package/dist/copilot/okr-mapper.js +2 -11
- package/dist/copilot/orchestrator.js +358 -352
- package/dist/copilot/orchestrator.test.js +139 -4
- package/dist/copilot/prompt-date.js +2 -1
- package/dist/copilot/session-manager.js +25 -23
- package/dist/copilot/session-manager.test.js +35 -1
- package/dist/copilot/standup.js +2 -2
- package/dist/copilot/task-event-log.js +7 -1
- package/dist/copilot/task-event-log.test.js +13 -0
- package/dist/copilot/tools/agent.js +608 -0
- package/dist/copilot/tools/index.js +19 -0
- package/dist/copilot/tools/memory.js +678 -0
- package/dist/copilot/tools/models.js +2 -0
- package/dist/copilot/tools/okr.js +171 -0
- package/dist/copilot/tools/wiki.js +333 -0
- package/dist/copilot/tools-deps.js +4 -0
- package/dist/copilot/tools.agent.test.js +10 -8
- package/dist/copilot/tools.inventory.test.js +76 -0
- package/dist/copilot/tools.js +1 -1780
- package/dist/copilot/tools.okr.test.js +31 -0
- package/dist/copilot/tools.wiki.test.js +6 -3
- package/dist/copilot/turn-event-log.js +31 -4
- package/dist/copilot/turn-event-log.test.js +24 -2
- package/dist/copilot/workiq-installer.test.js +2 -2
- package/dist/daemon-install.js +3 -2
- package/dist/daemon.js +9 -17
- package/dist/integrations/ado-client.js +90 -9
- package/dist/integrations/ado-client.test.js +56 -0
- package/dist/integrations/team-push.js +1 -0
- package/dist/integrations/team-push.test.js +6 -0
- package/dist/integrations/teams-notify.js +1 -0
- package/dist/integrations/teams-notify.test.js +5 -0
- package/dist/memory/active-scope.test.js +0 -1
- package/dist/memory/checkpoint.js +89 -72
- package/dist/memory/checkpoint.test.js +23 -3
- package/dist/memory/eot.js +87 -85
- package/dist/memory/eot.test.js +71 -3
- package/dist/memory/hooks.js +2 -4
- package/dist/memory/housekeeping-scheduler.js +1 -1
- package/dist/memory/housekeeping-scheduler.test.js +1 -2
- package/dist/memory/housekeeping.js +100 -3
- package/dist/memory/housekeeping.test.js +33 -2
- package/dist/memory/reflect.test.js +2 -0
- package/dist/memory/scope-lock.js +26 -0
- package/dist/memory/scope-lock.test.js +118 -0
- package/dist/memory/scopes.test.js +0 -1
- package/dist/mode-context.js +58 -5
- package/dist/mode-context.test.js +68 -0
- package/dist/paths.js +1 -0
- package/dist/setup.js +3 -2
- package/dist/shared/api-schemas.js +48 -5
- package/dist/store/connection.js +96 -0
- package/dist/store/db.js +5 -1498
- package/dist/store/db.test.js +182 -1
- package/dist/store/migrations.js +460 -0
- package/dist/store/repositories/memory.js +281 -0
- package/dist/store/repositories/okr.js +3 -0
- package/dist/store/repositories/projects.js +5 -0
- package/dist/store/repositories/sessions.js +284 -0
- package/dist/store/repositories/wiki.js +60 -0
- package/dist/store/schema.js +501 -0
- package/dist/util/logger.js +3 -2
- package/dist/wiki/consolidation.js +50 -9
- package/dist/wiki/consolidation.test.js +45 -0
- package/dist/wiki/frontmatter.js +43 -13
- package/dist/wiki/frontmatter.test.js +24 -0
- package/dist/wiki/fs.js +16 -4
- package/dist/wiki/fs.test.js +84 -0
- package/dist/wiki/index-manager.js +30 -2
- package/dist/wiki/index-manager.test.js +43 -12
- package/dist/wiki/ingest.js +1 -1
- package/dist/wiki/lock.js +11 -1
- package/dist/wiki/log-manager.js +2 -7
- package/dist/wiki/migrate.js +44 -17
- package/dist/wiki/project-registry.js +10 -5
- package/dist/wiki/project-registry.test.js +14 -0
- package/dist/wiki/scheduler.js +1 -1
- package/dist/wiki/seed-team-wiki.js +2 -1
- package/dist/wiki/team-sync.js +31 -6
- package/dist/wiki/team-sync.test.js +81 -0
- package/package.json +1 -1
- package/web/dist/assets/WikiEdit-BZXAdarz.js +30 -0
- package/web/dist/assets/WikiEdit-BZXAdarz.js.map +1 -0
- package/web/dist/assets/WikiGraph-KrCYco4v.js +2 -0
- package/web/dist/assets/WikiGraph-KrCYco4v.js.map +1 -0
- package/web/dist/assets/index-CUm2Wbuh.js +250 -0
- package/web/dist/assets/index-CUm2Wbuh.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-iQrv3lQN.js +0 -286
- package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
package/dist/copilot/tools.js
CHANGED
|
@@ -1,1781 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { approveAll, defineTool } from "@github/copilot-sdk";
|
|
3
|
-
import { getDb, appendTaskOutputDeltaEvent, appendTaskStatusEvent, updateTaskResult } from "../store/db.js";
|
|
4
|
-
import { readdirSync, readFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { homedir } from "os";
|
|
7
|
-
import { listSkills, createSkill, removeSkill } from "./skills.js";
|
|
8
|
-
import { config, persistModel } from "../config.js";
|
|
9
|
-
import { ModeContext } from "../mode-context.js";
|
|
10
|
-
import { agentEventBus } from "./agent-event-bus.js";
|
|
11
|
-
import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, sendToAgentSession, invalidateOrchestratorSession, maybeScheduleScopeChangeCheckpoint, resetCheckpointSessionState, switchSessionModel, } from "./orchestrator.js";
|
|
12
|
-
import { getRouterConfig, updateRouterConfig } from "./router.js";
|
|
13
|
-
import { ensureWikiStructure, writePage, assertPagePath } from "../wiki/fs.js";
|
|
14
|
-
import { searchIndex, addToIndex, buildIndexEntryForPage, reindexWikiPages, } from "../wiki/index-manager.js";
|
|
15
|
-
import { traverse as wikiTraverse } from "../wiki/links.js";
|
|
16
|
-
import { validateWikiFrontmatter, validateAndBackfillFrontmatter } from "../wiki/frontmatter.js";
|
|
17
|
-
import { appendTimeline } from "../wiki/timeline.js";
|
|
18
|
-
import { ingestSource, detectSourceType, looksLikeLocalFilePath } from "../wiki/ingest.js";
|
|
19
|
-
import { appendLog } from "../wiki/log-manager.js";
|
|
20
|
-
import { loadTaxonomy } from "../wiki/taxonomy.js";
|
|
21
|
-
import { topicPagePath } from "../wiki/topic-structure.js";
|
|
22
|
-
import { withWikiWrite } from "../wiki/lock.js";
|
|
23
|
-
import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
|
|
24
|
-
import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createTaskId, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
|
|
25
|
-
import * as agentsModule from "./agents.js";
|
|
26
|
-
import { detectProjectRuleWarnings } from "./project-rule-warnings.js";
|
|
27
|
-
import { renderDelegatedProjectRulesPreamble } from "./project-rules-injection.js";
|
|
28
|
-
import { adoGetOkrs, adoOkrSummary, adoUpdateKr } from "../integrations/ado-skill.js";
|
|
29
|
-
import { TeamsNotifier } from "../integrations/teams-notify.js";
|
|
30
|
-
import { TeamPushClient } from "../integrations/team-push.js";
|
|
31
|
-
import { OKRMapper, parseOKRPageContent } from "./okr-mapper.js";
|
|
32
|
-
import { childLogger } from "../util/logger.js";
|
|
33
|
-
import { getActiveScope as getMemoryActiveScope, createScope as createMemoryScope, getScope as getMemoryScope, inferScopeFromText, completeActionItem, demoteToCold, demoteToWarm, dropActionItem, queueMemoryProposal, recall as recallMemory, listActionItems, recordDecision, recordActionItem, recordObservation, runHousekeeping, reflectOnScope, reflectAllScopes, setActiveScope as setMemoryActiveScope, snoozeActionItem, promoteToHot, upsertEntity, hookDispatcher, } from "../memory/index.js";
|
|
34
|
-
const log = childLogger("tools");
|
|
35
|
-
const modeContext = new ModeContext(config);
|
|
36
|
-
/** Escape a string for safe inclusion as a single-line YAML scalar value. */
|
|
37
|
-
function yamlEscape(value) {
|
|
38
|
-
// Always quote and escape backslashes, double quotes, and newlines.
|
|
39
|
-
const escaped = value
|
|
40
|
-
.replace(/\\/g, "\\\\")
|
|
41
|
-
.replace(/"/g, '\\"')
|
|
42
|
-
.replace(/\n/g, "\\n")
|
|
43
|
-
.replace(/\r/g, "\\r");
|
|
44
|
-
return `"${escaped}"`;
|
|
45
|
-
}
|
|
46
|
-
/** Escape a single token for use inside a YAML inline list `[a, b]`. */
|
|
47
|
-
function yamlListItem(value) {
|
|
48
|
-
// Restrict to a safe character set; replace anything else.
|
|
49
|
-
const safe = value.replace(/[^A-Za-z0-9_./-]/g, "-");
|
|
50
|
-
return safe || "untagged";
|
|
51
|
-
}
|
|
52
|
-
/** Sanitize a single line for safe inclusion as an index/log table entry. */
|
|
53
|
-
function indexSafe(text) {
|
|
54
|
-
return text.replace(/[\r\n|]/g, " ").trim();
|
|
55
|
-
}
|
|
56
|
-
function sanitizeWikiUpdateError(err) {
|
|
57
|
-
if (err instanceof z.ZodError) {
|
|
58
|
-
return err.issues.map((issue) => issue.message).join("; ") || "Invalid wiki_update arguments.";
|
|
59
|
-
}
|
|
60
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
-
if (message.startsWith("Wiki page frontmatter violates the required shape:")
|
|
62
|
-
|| message.startsWith("Wiki path")
|
|
63
|
-
|| message.startsWith("Wiki page paths must end in .md:")
|
|
64
|
-
|| message.startsWith("Refused unsafe wiki path:")
|
|
65
|
-
|| message.startsWith("Refused: only pages under pages/")
|
|
66
|
-
|| message === "Wiki path is required") {
|
|
67
|
-
return message;
|
|
68
|
-
}
|
|
69
|
-
return "Wiki update failed. Check the page path and frontmatter, then try again.";
|
|
70
|
-
}
|
|
71
|
-
function isTimeoutError(err) {
|
|
72
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
-
return /timeout|timed?\s*out/i.test(msg);
|
|
74
|
-
}
|
|
75
|
-
function validateWikiPageInput(path, content, allowedTags = loadTaxonomy()) {
|
|
76
|
-
assertPagePath(path);
|
|
77
|
-
const backfilled = validateAndBackfillFrontmatter(path, content);
|
|
78
|
-
const nextContent = backfilled.changed ? backfilled.content : content;
|
|
79
|
-
const validation = validateWikiFrontmatter(nextContent, { allowedTags });
|
|
80
|
-
if (!validation.valid) {
|
|
81
|
-
throw new Error(validation.errors.map((error) => error.message).join("\n\n"));
|
|
82
|
-
}
|
|
83
|
-
return nextContent;
|
|
84
|
-
}
|
|
85
|
-
function writeWikiPageAndRefreshIndex(page, logSource) {
|
|
86
|
-
writePage(page.path, page.content);
|
|
87
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
88
|
-
const rebuilt = buildIndexEntryForPage(page.path, {
|
|
89
|
-
section: page.section || "Knowledge",
|
|
90
|
-
updated: today,
|
|
91
|
-
});
|
|
92
|
-
if (rebuilt) {
|
|
93
|
-
rebuilt.section = page.section || "Knowledge";
|
|
94
|
-
addToIndex(rebuilt);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
addToIndex({
|
|
98
|
-
path: page.path,
|
|
99
|
-
title: page.title,
|
|
100
|
-
summary: indexSafe(page.summary),
|
|
101
|
-
section: page.section || "Knowledge",
|
|
102
|
-
updated: today,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
appendLog("update", `${logSource}: ${indexSafe(page.title)} (${page.path})`);
|
|
106
|
-
}
|
|
107
|
-
function hasAdoPat() {
|
|
108
|
-
return (process.env.ADO_PAT?.trim() || config.adoPat).length > 0;
|
|
109
|
-
}
|
|
110
|
-
function requireOrchestratorMemoryWrite() {
|
|
111
|
-
const agentSlug = agentsModule.getCurrentToolAgentSlug?.();
|
|
112
|
-
if (agentSlug && agentSlug !== "chapterhouse") {
|
|
113
|
-
return "Memory writes are orchestrator-only. Use memory_propose instead.";
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
function resolveProposalScopeSlug(requestedScopeSlug) {
|
|
118
|
-
if (requestedScopeSlug) {
|
|
119
|
-
return requestedScopeSlug;
|
|
120
|
-
}
|
|
121
|
-
const agentSlug = agentsModule.getCurrentToolAgentSlug?.();
|
|
122
|
-
if (!agentSlug || agentSlug === "chapterhouse") {
|
|
123
|
-
return getMemoryActiveScope()?.slug;
|
|
124
|
-
}
|
|
125
|
-
const agent = getAgent(agentSlug);
|
|
126
|
-
const boundScope = agent?.scope ?? loadAgents().find((entry) => entry.slug === agentSlug)?.scope;
|
|
127
|
-
if (boundScope && getMemoryScope(boundScope)) {
|
|
128
|
-
return boundScope;
|
|
129
|
-
}
|
|
130
|
-
return getMemoryActiveScope()?.slug;
|
|
131
|
-
}
|
|
132
|
-
function resolveMemoryScopeForWrite(explicitScope, content) {
|
|
133
|
-
const explicit = explicitScope ? getMemoryScope(explicitScope) : undefined;
|
|
134
|
-
if (explicitScope && !explicit) {
|
|
135
|
-
throw new Error(`Unknown memory scope '${explicitScope}'.`);
|
|
136
|
-
}
|
|
137
|
-
if (explicit) {
|
|
138
|
-
return { scopeId: explicit.id, scopeSlug: explicit.slug };
|
|
139
|
-
}
|
|
140
|
-
const active = getMemoryActiveScope();
|
|
141
|
-
if (active) {
|
|
142
|
-
return { scopeId: active.id, scopeSlug: active.slug };
|
|
143
|
-
}
|
|
144
|
-
const inferred = inferScopeFromText(content);
|
|
145
|
-
if (inferred) {
|
|
146
|
-
const inferredScope = getMemoryScope(inferred.scope_id);
|
|
147
|
-
if (inferredScope) {
|
|
148
|
-
return { scopeId: inferredScope.id, scopeSlug: inferredScope.slug };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const validScopes = getDb().prepare(`
|
|
152
|
-
SELECT slug
|
|
153
|
-
FROM mem_scopes
|
|
154
|
-
WHERE active = 1
|
|
155
|
-
ORDER BY slug
|
|
156
|
-
`).all();
|
|
157
|
-
const activeScope = getMemoryActiveScope()?.slug ?? "none";
|
|
158
|
-
throw new Error(`No scope inferred. Active scope: ${activeScope}. Valid scopes: [${validScopes.map((row) => row.slug).join(", ")}]. `
|
|
159
|
-
+ "Set active scope with memory_set_scope or pass scope explicitly.");
|
|
160
|
-
}
|
|
161
|
-
const observationProposalPayloadSchema = z.object({
|
|
162
|
-
content: z.string(),
|
|
163
|
-
entity_id: z.number().int().positive().optional(),
|
|
164
|
-
source: z.string().optional(),
|
|
165
|
-
});
|
|
166
|
-
const decisionProposalPayloadSchema = z.object({
|
|
167
|
-
title: z.string(),
|
|
168
|
-
rationale: z.string().optional(),
|
|
169
|
-
decided_at: z.string().optional(),
|
|
170
|
-
});
|
|
171
|
-
const entityProposalPayloadSchema = z.object({
|
|
172
|
-
name: z.string(),
|
|
173
|
-
entity_kind: z.string().optional(),
|
|
174
|
-
kind: z.string().optional(),
|
|
175
|
-
summary: z.string().optional(),
|
|
176
|
-
}).superRefine((value, context) => {
|
|
177
|
-
if (!value.entity_kind && !value.kind) {
|
|
178
|
-
context.addIssue({
|
|
179
|
-
code: z.ZodIssueCode.custom,
|
|
180
|
-
message: "entity_kind is required for entity proposals.",
|
|
181
|
-
path: ["entity_kind"],
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}).transform((value) => ({
|
|
185
|
-
name: value.name,
|
|
186
|
-
entity_kind: value.entity_kind ?? value.kind,
|
|
187
|
-
summary: value.summary,
|
|
188
|
-
}));
|
|
189
|
-
const actionItemProposalPayloadSchema = z.object({
|
|
190
|
-
title: z.string().trim().min(1, "title is required for action_item proposals."),
|
|
191
|
-
detail: z.string().optional(),
|
|
192
|
-
due_at: z.string().optional(),
|
|
193
|
-
source: z.string().optional(),
|
|
194
|
-
entity_id: z.number().int().positive().optional(),
|
|
195
|
-
entity_name: z.string().trim().min(1, "entity_name must be non-empty when provided.").optional(),
|
|
196
|
-
entity_kind: z.string().trim().min(1, "entity_kind must be non-empty when provided.").optional(),
|
|
197
|
-
}).superRefine((value, context) => {
|
|
198
|
-
if ((value.entity_name && !value.entity_kind) || (!value.entity_name && value.entity_kind)) {
|
|
199
|
-
context.addIssue({
|
|
200
|
-
code: z.ZodIssueCode.custom,
|
|
201
|
-
message: "entity_name and entity_kind must be provided together for action_item proposals.",
|
|
202
|
-
path: ["entity_name"],
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
if (value.entity_id !== undefined && value.entity_name) {
|
|
206
|
-
context.addIssue({
|
|
207
|
-
code: z.ZodIssueCode.custom,
|
|
208
|
-
message: "Provide either entity_id or entity_name/entity_kind for action_item proposals, not both.",
|
|
209
|
-
path: ["entity_id"],
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
const memoryProposeArgsSchema = z.object({
|
|
214
|
-
kind: z.enum(["observation", "decision", "entity", "action_item"]),
|
|
215
|
-
scope_slug: z.string().optional(),
|
|
216
|
-
payload: z.record(z.string(), z.unknown()),
|
|
217
|
-
confidence: z.number().min(0).max(1).optional(),
|
|
218
|
-
reason: z.string().optional(),
|
|
219
|
-
}).superRefine((value, context) => {
|
|
220
|
-
const schema = value.kind === "observation"
|
|
221
|
-
? observationProposalPayloadSchema
|
|
222
|
-
: value.kind === "decision"
|
|
223
|
-
? decisionProposalPayloadSchema
|
|
224
|
-
: value.kind === "entity"
|
|
225
|
-
? entityProposalPayloadSchema
|
|
226
|
-
: actionItemProposalPayloadSchema;
|
|
227
|
-
const parsed = schema.safeParse(value.payload);
|
|
228
|
-
if (!parsed.success) {
|
|
229
|
-
for (const issue of parsed.error.issues) {
|
|
230
|
-
context.addIssue({
|
|
231
|
-
code: z.ZodIssueCode.custom,
|
|
232
|
-
message: issue.message,
|
|
233
|
-
path: ["payload", ...issue.path],
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
const memoryTierTableSchema = z.enum(["observation", "decision", "entity", "action_item"]);
|
|
239
|
-
const wikiPageArgsSchema = z.object({
|
|
240
|
-
path: z.string().describe("Page path relative to wiki root (e.g. 'pages/projects/chapterhouse/index.md', 'pages/projects/chapterhouse/decisions.md', 'pages/people/brian/index.md')"),
|
|
241
|
-
title: z.string().describe("Page title for the index"),
|
|
242
|
-
summary: z.string().max(160, "Summary must be 160 characters or fewer").describe("One-line summary for the index"),
|
|
243
|
-
section: z.string().optional().describe("Index section (default: 'Knowledge')"),
|
|
244
|
-
content: z.string().describe("Full page content (markdown)"),
|
|
245
|
-
});
|
|
246
|
-
const wikiUpdateArgsSchema = wikiPageArgsSchema;
|
|
247
|
-
const wikiBatchUpdateArgsSchema = z.object({
|
|
248
|
-
pages: z.array(wikiPageArgsSchema)
|
|
249
|
-
.min(1)
|
|
250
|
-
.max(50)
|
|
251
|
-
.describe("Array of pages to create or update (1–50 items)"),
|
|
252
|
-
});
|
|
253
|
-
function getCurrentQuarter(now = new Date()) {
|
|
254
|
-
return `${now.getUTCFullYear()}-Q${Math.floor(now.getUTCMonth() / 3) + 1}`;
|
|
255
|
-
}
|
|
256
|
-
function isSharedTeamWikiPath(path) {
|
|
257
|
-
return path.startsWith("pages/shared/");
|
|
258
|
-
}
|
|
259
|
-
export async function getMyOkrsSummary(options) {
|
|
260
|
-
const user = options.getCurrentUser();
|
|
261
|
-
if (!user) {
|
|
262
|
-
return "I don't have the current user identity for this session, so I can't filter your OKRs.";
|
|
263
|
-
}
|
|
264
|
-
const content = await options.createTeamPushClient().fetchOKRs(options.period);
|
|
265
|
-
const owned = parseOKRPageContent(content)
|
|
266
|
-
.filter((kr) => isOwnedByCurrentUser(kr.owner, user))
|
|
267
|
-
.sort((a, b) => a.krId.localeCompare(b.krId));
|
|
268
|
-
if (owned.length === 0) {
|
|
269
|
-
return `No current OKRs found for ${user.name}.`;
|
|
270
|
-
}
|
|
271
|
-
const lines = owned.map((kr) => {
|
|
272
|
-
const progress = Number.isFinite(kr.currentValue) && Number.isFinite(kr.targetValue)
|
|
273
|
-
? ` — ${kr.currentValue}/${kr.targetValue}${kr.unit ? ` ${kr.unit}` : ""}`
|
|
274
|
-
: "";
|
|
275
|
-
return `• ${kr.krId}: ${kr.title} (${kr.objectiveTitle})${progress}`;
|
|
276
|
-
});
|
|
277
|
-
return `Current OKRs for ${user.name}:\n${lines.join("\n")}`;
|
|
278
|
-
}
|
|
279
|
-
export function createTools(deps) {
|
|
280
|
-
const getCurrentUser = deps.getCurrentUser ?? (() => getCurrentAuthenticatedUser() ?? getLastAuthenticatedUser());
|
|
281
|
-
const createTeamPushClient = deps.createTeamPushClient ?? (() => new TeamPushClient({
|
|
282
|
-
getAuthorizationHeader: getCurrentAuthorizationHeader,
|
|
283
|
-
getCurrentUser,
|
|
284
|
-
}));
|
|
285
|
-
const createOKRMapper = deps.createOKRMapper ?? (() => new OKRMapper(teamWikiSync));
|
|
286
|
-
return [
|
|
287
|
-
// ----- Agent Delegation Tools (for @chapterhouse) -----
|
|
288
|
-
defineTool("delegate_to_agent", {
|
|
289
|
-
description: "Delegate a task to a specialist agent. The task runs in the background — you'll be notified when it's done. " +
|
|
290
|
-
"Available agents: use show_agent_roster to see the roster. For @general-purpose, specify model_override based on task complexity.",
|
|
291
|
-
parameters: z.object({
|
|
292
|
-
agent_name: z.string().describe("Name or slug of the agent to delegate to (e.g. 'coder', 'designer', 'general-purpose')"),
|
|
293
|
-
task: z.string().describe("Detailed task description for the agent"),
|
|
294
|
-
summary: z.string().describe("Short human-readable summary of the task (under 80 chars, e.g. 'Fix login button styling')"),
|
|
295
|
-
model_override: z.string().optional().describe("Model override for agents with model 'auto' (e.g. 'gpt-4.1', 'claude-sonnet-4.6', 'claude-opus-4.6')"),
|
|
296
|
-
}),
|
|
297
|
-
handler: async (args) => {
|
|
298
|
-
const agent = getAgent(args.agent_name);
|
|
299
|
-
if (agent?.slug === "chapterhouse") {
|
|
300
|
-
return "Cannot delegate to yourself. Handle this directly or pick a specialist agent.";
|
|
301
|
-
}
|
|
302
|
-
if (!agent) {
|
|
303
|
-
const available = getAgentRegistry().map((a) => a.slug).join(", ");
|
|
304
|
-
return `Agent '${args.agent_name}' not found. Available agents: ${available}`;
|
|
305
|
-
}
|
|
306
|
-
const delegatedSlug = agent.slug;
|
|
307
|
-
const taskId = createTaskId();
|
|
308
|
-
const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel(), taskId);
|
|
309
|
-
const activeProjectRules = getCurrentActiveProjectRules();
|
|
310
|
-
const warningLines = activeProjectRules
|
|
311
|
-
? detectProjectRuleWarnings(args.task, activeProjectRules.rules.hard)
|
|
312
|
-
: [];
|
|
313
|
-
const warningBlock = warningLines.length > 0 ? `${warningLines.join("\n")}\n\n` : "";
|
|
314
|
-
const taskPrompt = activeProjectRules
|
|
315
|
-
? `${warningBlock}${renderDelegatedProjectRulesPreamble(activeProjectRules.project.slug, activeProjectRules.project.path, activeProjectRules.rules.path, activeProjectRules.rules.hard, activeProjectRules.rules.soft)}\n\n${args.task}`
|
|
316
|
-
: args.task;
|
|
317
|
-
// Persist task to DB
|
|
318
|
-
const db = getDb();
|
|
319
|
-
db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
|
|
320
|
-
VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(task.taskId, delegatedSlug, args.summary, args.task, task.originChannel || null, getCurrentSessionKey());
|
|
321
|
-
if (agent.persistent) {
|
|
322
|
-
(async () => {
|
|
323
|
-
try {
|
|
324
|
-
const output = await sendToAgentSession(delegatedSlug, taskPrompt, task.taskId);
|
|
325
|
-
completeTask(task.taskId, output);
|
|
326
|
-
updateTaskResult(task.taskId, "completed", output);
|
|
327
|
-
const statusEvent = appendTaskStatusEvent(task.taskId, "completed");
|
|
328
|
-
if (statusEvent) {
|
|
329
|
-
void agentEventBus.emit({
|
|
330
|
-
type: "session:tool_call",
|
|
331
|
-
sessionId: task.taskId,
|
|
332
|
-
payload: {
|
|
333
|
-
toolName: "",
|
|
334
|
-
toolArgs: {},
|
|
335
|
-
_kind: statusEvent.kind,
|
|
336
|
-
_seq: statusEvent.seq,
|
|
337
|
-
_ts: statusEvent.ts,
|
|
338
|
-
_summary: statusEvent.summary,
|
|
339
|
-
_text: statusEvent.text,
|
|
340
|
-
_status: statusEvent.status,
|
|
341
|
-
},
|
|
342
|
-
timestamp: new Date(statusEvent.ts),
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
|
|
346
|
-
}
|
|
347
|
-
catch (err) {
|
|
348
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
349
|
-
failTask(task.taskId, msg);
|
|
350
|
-
updateTaskResult(task.taskId, "error", msg);
|
|
351
|
-
const statusEvent = appendTaskStatusEvent(task.taskId, "error", msg);
|
|
352
|
-
if (statusEvent) {
|
|
353
|
-
void agentEventBus.emit({
|
|
354
|
-
type: "session:tool_call",
|
|
355
|
-
sessionId: task.taskId,
|
|
356
|
-
payload: {
|
|
357
|
-
toolName: "",
|
|
358
|
-
toolArgs: {},
|
|
359
|
-
_kind: statusEvent.kind,
|
|
360
|
-
_seq: statusEvent.seq,
|
|
361
|
-
_ts: statusEvent.ts,
|
|
362
|
-
_summary: statusEvent.summary,
|
|
363
|
-
_text: statusEvent.text,
|
|
364
|
-
_status: statusEvent.status,
|
|
365
|
-
},
|
|
366
|
-
timestamp: new Date(statusEvent.ts),
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
deps.onAgentTaskComplete(task.taskId, delegatedSlug, `Error: ${msg}`);
|
|
370
|
-
}
|
|
371
|
-
})();
|
|
372
|
-
const model = (args.model_override && args.model_override.length > 0)
|
|
373
|
-
? args.model_override
|
|
374
|
-
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
375
|
-
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
376
|
-
}
|
|
377
|
-
let session;
|
|
378
|
-
try {
|
|
379
|
-
const allTools = createTools(deps);
|
|
380
|
-
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override, undefined, taskId);
|
|
381
|
-
}
|
|
382
|
-
catch (err) {
|
|
383
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
384
|
-
return `Failed to create session for @${delegatedSlug}: ${msg}`;
|
|
385
|
-
}
|
|
386
|
-
// Capture the parent's activity callback so the child session can stream
|
|
387
|
-
// its events back to the originating SSE connection. This survives past
|
|
388
|
-
// the parent assistant turn — the child runs long after the parent's
|
|
389
|
-
// `executeOnSession` finishes.
|
|
390
|
-
const parentActivity = getCurrentActivityCallback();
|
|
391
|
-
const childUnsubs = [];
|
|
392
|
-
const emitTaskLogEvent = (taskEvent) => {
|
|
393
|
-
void agentEventBus.emit({
|
|
394
|
-
type: "session:tool_call",
|
|
395
|
-
sessionId: task.taskId,
|
|
396
|
-
payload: {
|
|
397
|
-
toolName: "",
|
|
398
|
-
toolArgs: {},
|
|
399
|
-
_kind: taskEvent.kind,
|
|
400
|
-
_seq: taskEvent.seq,
|
|
401
|
-
_ts: taskEvent.ts,
|
|
402
|
-
_summary: taskEvent.summary,
|
|
403
|
-
_text: taskEvent.text,
|
|
404
|
-
_status: taskEvent.status,
|
|
405
|
-
},
|
|
406
|
-
timestamp: new Date(taskEvent.ts),
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
let workerOutput = "";
|
|
410
|
-
childUnsubs.push(session.on("assistant.message_delta", (event) => {
|
|
411
|
-
const delta = typeof event.data.deltaContent === "string" ? event.data.deltaContent : "";
|
|
412
|
-
if (!delta)
|
|
413
|
-
return;
|
|
414
|
-
workerOutput += delta;
|
|
415
|
-
const taskEvent = appendTaskOutputDeltaEvent(task.taskId, delta);
|
|
416
|
-
if (!taskEvent)
|
|
417
|
-
return;
|
|
418
|
-
emitTaskLogEvent(taskEvent);
|
|
419
|
-
}));
|
|
420
|
-
if (parentActivity) {
|
|
421
|
-
childUnsubs.push(session.on("assistant.reasoning_delta", (event) => {
|
|
422
|
-
parentActivity({
|
|
423
|
-
kind: "thinking_delta",
|
|
424
|
-
reasoningId: event.data.reasoningId,
|
|
425
|
-
deltaContent: event.data.deltaContent,
|
|
426
|
-
agentSlug: delegatedSlug,
|
|
427
|
-
});
|
|
428
|
-
}), session.on("tool.execution_start", (event) => {
|
|
429
|
-
const data = event.data;
|
|
430
|
-
parentActivity({
|
|
431
|
-
kind: "tool_start",
|
|
432
|
-
toolCallId: data.toolCallId,
|
|
433
|
-
toolName: data.toolName,
|
|
434
|
-
mcpServerName: data.mcpServerName,
|
|
435
|
-
arguments: data.arguments,
|
|
436
|
-
agentSlug: delegatedSlug,
|
|
437
|
-
});
|
|
438
|
-
}), session.on("tool.execution_complete", (event) => {
|
|
439
|
-
const data = event.data;
|
|
440
|
-
const result = data.result;
|
|
441
|
-
const resultPreview = typeof result?.content === "string" ? result.content.slice(0, 400) : undefined;
|
|
442
|
-
const detailedContent = typeof result?.detailedContent === "string"
|
|
443
|
-
? result.detailedContent
|
|
444
|
-
: typeof result?.content === "string"
|
|
445
|
-
? result.content
|
|
446
|
-
: undefined;
|
|
447
|
-
parentActivity({
|
|
448
|
-
kind: "tool_complete",
|
|
449
|
-
toolCallId: data.toolCallId,
|
|
450
|
-
success: data.success,
|
|
451
|
-
resultPreview,
|
|
452
|
-
detailedContent,
|
|
453
|
-
agentSlug: delegatedSlug,
|
|
454
|
-
});
|
|
455
|
-
}));
|
|
456
|
-
}
|
|
457
|
-
const timeoutMs = config.workerTimeoutMs;
|
|
458
|
-
// Non-blocking: dispatch and return immediately. Session is always destroyed after.
|
|
459
|
-
(async () => {
|
|
460
|
-
try {
|
|
461
|
-
const result = await session.sendAndWait({ prompt: taskPrompt }, timeoutMs);
|
|
462
|
-
const output = workerOutput || result?.data?.content || "No response";
|
|
463
|
-
completeTask(task.taskId, output);
|
|
464
|
-
updateTaskResult(task.taskId, "completed", output);
|
|
465
|
-
const statusEvent = appendTaskStatusEvent(task.taskId, "completed");
|
|
466
|
-
if (statusEvent)
|
|
467
|
-
emitTaskLogEvent(statusEvent);
|
|
468
|
-
deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
|
|
469
|
-
}
|
|
470
|
-
catch (err) {
|
|
471
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
-
failTask(task.taskId, msg);
|
|
473
|
-
updateTaskResult(task.taskId, "error", msg);
|
|
474
|
-
const statusEvent = appendTaskStatusEvent(task.taskId, "error", msg);
|
|
475
|
-
if (statusEvent)
|
|
476
|
-
emitTaskLogEvent(statusEvent);
|
|
477
|
-
deps.onAgentTaskComplete(task.taskId, delegatedSlug, `Error: ${msg}`);
|
|
478
|
-
}
|
|
479
|
-
finally {
|
|
480
|
-
for (const unsub of childUnsubs) {
|
|
481
|
-
try {
|
|
482
|
-
unsub();
|
|
483
|
-
}
|
|
484
|
-
catch { /* best effort */ }
|
|
485
|
-
}
|
|
486
|
-
session.destroy().catch(() => { });
|
|
487
|
-
}
|
|
488
|
-
})();
|
|
489
|
-
const model = (args.model_override && args.model_override.length > 0)
|
|
490
|
-
? args.model_override
|
|
491
|
-
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
492
|
-
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
493
|
-
},
|
|
494
|
-
}),
|
|
495
|
-
defineTool("check_agent_status", {
|
|
496
|
-
description: "Check the status of an agent or a specific delegated task.",
|
|
497
|
-
parameters: z.object({
|
|
498
|
-
agent_name: z.string().optional().describe("Agent name/slug to check"),
|
|
499
|
-
task_id: z.string().optional().describe("Specific task ID to check"),
|
|
500
|
-
}),
|
|
501
|
-
handler: async (args) => {
|
|
502
|
-
if (args.task_id) {
|
|
503
|
-
const task = getTask(args.task_id);
|
|
504
|
-
if (!task)
|
|
505
|
-
return `Task '${args.task_id}' not found.`;
|
|
506
|
-
const elapsed = Math.round((Date.now() - task.startedAt) / 1000);
|
|
507
|
-
let info = `Task ${task.taskId} (@${task.agentSlug})\nStatus: ${task.status}\nDescription: ${task.description}\nElapsed: ${elapsed}s`;
|
|
508
|
-
if (task.result)
|
|
509
|
-
info += `\n\nResult:\n${task.result.slice(0, 2000)}`;
|
|
510
|
-
return info;
|
|
511
|
-
}
|
|
512
|
-
if (args.agent_name) {
|
|
513
|
-
const agent = getAgent(args.agent_name);
|
|
514
|
-
if (!agent)
|
|
515
|
-
return `Agent '${args.agent_name}' not found.`;
|
|
516
|
-
const status = getAgentSessionStatus(agent.slug);
|
|
517
|
-
let info = `@${agent.slug} (${agent.name})\nModel: ${agent.model}`;
|
|
518
|
-
if (status.tasks.length > 0) {
|
|
519
|
-
info += `\n\nActive tasks (${status.tasks.length}):`;
|
|
520
|
-
for (const t of status.tasks) {
|
|
521
|
-
info += `\n• ${t.taskId}: ${t.description} (${t.status})`;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return info;
|
|
525
|
-
}
|
|
526
|
-
// Show all agents
|
|
527
|
-
const agents = getAgentRegistry();
|
|
528
|
-
const lines = agents.map((a) => {
|
|
529
|
-
const status = getAgentSessionStatus(a.slug);
|
|
530
|
-
const runningTasks = status.tasks.filter((t) => t.status === "running");
|
|
531
|
-
const sessionBadge = runningTasks.length > 0 ? "●" : "○";
|
|
532
|
-
const taskInfo = runningTasks.length > 0 ? ` (${runningTasks.length} task(s) running)` : "";
|
|
533
|
-
return `${sessionBadge} @${a.slug} — ${a.description} [${a.model}]${taskInfo}`;
|
|
534
|
-
});
|
|
535
|
-
return `Agents (${agents.length}):\n${lines.join("\n")}`;
|
|
536
|
-
},
|
|
537
|
-
}),
|
|
538
|
-
defineTool("get_agent_result", {
|
|
539
|
-
description: "Get the result of a completed agent task.",
|
|
540
|
-
parameters: z.object({
|
|
541
|
-
task_id: z.string().describe("The task ID (from delegate_to_agent)"),
|
|
542
|
-
}),
|
|
543
|
-
handler: async (args) => {
|
|
544
|
-
const task = getTask(args.task_id);
|
|
545
|
-
if (!task) {
|
|
546
|
-
// Check DB for completed tasks that may have been cleared from memory
|
|
547
|
-
const db = getDb();
|
|
548
|
-
const row = db.prepare(`SELECT * FROM agent_tasks WHERE task_id = ?`).get(args.task_id);
|
|
549
|
-
if (!row)
|
|
550
|
-
return `Task '${args.task_id}' not found.`;
|
|
551
|
-
return `Task ${row.task_id} (@${row.agent_slug})\nStatus: ${row.status}\nDescription: ${row.description}\n\nResult:\n${row.result || "(no result)"}`;
|
|
552
|
-
}
|
|
553
|
-
if (task.status === "running") {
|
|
554
|
-
const elapsed = Math.round((Date.now() - task.startedAt) / 1000);
|
|
555
|
-
return `Task ${task.taskId} is still running (${elapsed}s elapsed).`;
|
|
556
|
-
}
|
|
557
|
-
return `Task ${task.taskId} (@${task.agentSlug}) — ${task.status}\n\nResult:\n${task.result || "(no result)"}`;
|
|
558
|
-
},
|
|
559
|
-
}),
|
|
560
|
-
defineTool("show_agent_roster", {
|
|
561
|
-
description: "List all registered agents with their name, model, status, and current tasks.",
|
|
562
|
-
parameters: z.object({}),
|
|
563
|
-
handler: async () => {
|
|
564
|
-
const agents = getAgentRegistry();
|
|
565
|
-
const chLines = agents.map((a) => {
|
|
566
|
-
const status = getAgentSessionStatus(a.slug);
|
|
567
|
-
const runningTasks = status.tasks.filter((t) => t.status === "running");
|
|
568
|
-
const badge = runningTasks.length > 0 ? "● working" : "○ idle";
|
|
569
|
-
const taskInfo = runningTasks.length > 0
|
|
570
|
-
? `\n Tasks: ${runningTasks.map((t) => `${t.taskId}: ${t.description}`).join(", ")}`
|
|
571
|
-
: "";
|
|
572
|
-
return `• @${a.slug} (${a.name}) — ${a.model} — ${badge}${taskInfo}\n ${a.description}`;
|
|
573
|
-
});
|
|
574
|
-
if (chLines.length === 0)
|
|
575
|
-
return "No agents registered.";
|
|
576
|
-
return `Registered agents (${chLines.length}):\n${chLines.join("\n")}`;
|
|
577
|
-
},
|
|
578
|
-
}),
|
|
579
|
-
defineTool("teams_notify", {
|
|
580
|
-
description: "Send a notification to the team Microsoft Teams channel",
|
|
581
|
-
parameters: z.object({
|
|
582
|
-
title: z.string().min(1).describe("Notification title"),
|
|
583
|
-
message: z.string().min(1).describe("Notification body"),
|
|
584
|
-
}),
|
|
585
|
-
handler: async (args) => {
|
|
586
|
-
const notifier = new TeamsNotifier();
|
|
587
|
-
const sent = await notifier.sendMessage(args.title, args.message);
|
|
588
|
-
return sent
|
|
589
|
-
? "Sent notification to the team Microsoft Teams channel."
|
|
590
|
-
: "Teams notifications are disabled or TEAMS_WEBHOOK_URL is not configured.";
|
|
591
|
-
},
|
|
592
|
-
}),
|
|
593
|
-
defineTool("log_okr_progress", {
|
|
594
|
-
description: "Log progress on a team OKR key result. Use when the user mentions completing work, shipping features, or making progress on goals.",
|
|
595
|
-
parameters: z.object({
|
|
596
|
-
activity: z.string().min(1).describe("Human description of what was done"),
|
|
597
|
-
krId: z.string().optional().describe("Key result identifier"),
|
|
598
|
-
delta: z.number().finite().optional().describe("Progress delta in the range 0-100"),
|
|
599
|
-
notes: z.string().optional().describe("Optional notes about the work"),
|
|
600
|
-
}),
|
|
601
|
-
handler: async (args) => {
|
|
602
|
-
if (!modeContext.canLogToAdo()) {
|
|
603
|
-
return "OKR progress logging is only available from personal Chapterhouse instances.";
|
|
604
|
-
}
|
|
605
|
-
const mapper = createOKRMapper();
|
|
606
|
-
if (!args.krId) {
|
|
607
|
-
const matches = await mapper.findMatchingKRs(args.activity);
|
|
608
|
-
return matches.length > 0
|
|
609
|
-
? mapper.formatUpdatePrompt(args.activity, matches)
|
|
610
|
-
: `You mentioned: "${args.activity}". I couldn't confidently map that to a team key result yet. Tell me the KR id and delta (0-100), and I'll log it.`;
|
|
611
|
-
}
|
|
612
|
-
if (args.delta === undefined) {
|
|
613
|
-
return `I can log "${args.activity}" against ${args.krId}. What's the delta (0-100)?`;
|
|
614
|
-
}
|
|
615
|
-
const result = await createTeamPushClient().pushUpdate({
|
|
616
|
-
activity: args.activity,
|
|
617
|
-
krId: args.krId,
|
|
618
|
-
delta: args.delta,
|
|
619
|
-
notes: args.notes,
|
|
620
|
-
});
|
|
621
|
-
mapper.recordConfirmedMapping(args.activity, args.krId);
|
|
622
|
-
const deltaText = typeof result.entry?.delta === "number" ? ` (${result.entry.delta}% logged)` : "";
|
|
623
|
-
return `Logged OKR progress for ${args.krId}${deltaText}.`;
|
|
624
|
-
},
|
|
625
|
-
}),
|
|
626
|
-
defineTool("get_my_okrs", {
|
|
627
|
-
description: "Show the current OKR key results owned by this user",
|
|
628
|
-
parameters: z.object({
|
|
629
|
-
period: z.string().optional().describe("Optional OKR period in YYYY-QN format"),
|
|
630
|
-
}),
|
|
631
|
-
handler: async (args) => await getMyOkrsSummary({
|
|
632
|
-
createTeamPushClient,
|
|
633
|
-
getCurrentUser,
|
|
634
|
-
period: args.period,
|
|
635
|
-
}),
|
|
636
|
-
}),
|
|
637
|
-
defineTool("write_team_wiki", {
|
|
638
|
-
description: "Write or update a page in the shared team wiki",
|
|
639
|
-
parameters: z.object({
|
|
640
|
-
path: z.string().min(1).describe("Shared team wiki page path, starting with pages/shared/"),
|
|
641
|
-
content: z.string().describe("Full markdown content to write to the shared team wiki page"),
|
|
642
|
-
}),
|
|
643
|
-
handler: async (args) => {
|
|
644
|
-
if (!isSharedTeamWikiPath(args.path)) {
|
|
645
|
-
return 'Shared team wiki path must start with "pages/shared/".';
|
|
646
|
-
}
|
|
647
|
-
await createTeamPushClient().writePage(args.path, args.content);
|
|
648
|
-
return `Wrote shared team wiki page: ${args.path}`;
|
|
649
|
-
},
|
|
650
|
-
}),
|
|
651
|
-
defineTool("ado_get_okrs", {
|
|
652
|
-
description: "Get current OKR status from Azure DevOps for a given period (e.g. '2026-Q2')",
|
|
653
|
-
parameters: z.object({
|
|
654
|
-
period: z.string().optional().describe("Optional OKR period such as '2026-Q2'"),
|
|
655
|
-
}),
|
|
656
|
-
handler: async (args) => {
|
|
657
|
-
if (!hasAdoPat()) {
|
|
658
|
-
return "Azure DevOps OKR integration is not configured. Set ADO_PAT in ~/.chapterhouse/.env.";
|
|
659
|
-
}
|
|
660
|
-
return await adoGetOkrs(args.period);
|
|
661
|
-
},
|
|
662
|
-
}),
|
|
663
|
-
defineTool("ado_update_kr", {
|
|
664
|
-
description: "Update the current value of a Key Result in Azure DevOps",
|
|
665
|
-
parameters: z.object({
|
|
666
|
-
workItemId: z.number().int().positive().describe("Azure DevOps work item ID for the key result"),
|
|
667
|
-
currentValue: z.number().finite().describe("New current value for the key result"),
|
|
668
|
-
notes: z.string().optional().describe("Optional progress note to add as an ADO comment"),
|
|
669
|
-
}),
|
|
670
|
-
handler: async (args) => {
|
|
671
|
-
if (!hasAdoPat()) {
|
|
672
|
-
return "Azure DevOps OKR integration is not configured. Set ADO_PAT in ~/.chapterhouse/.env.";
|
|
673
|
-
}
|
|
674
|
-
return await adoUpdateKr(args.workItemId, args.currentValue, args.notes);
|
|
675
|
-
},
|
|
676
|
-
}),
|
|
677
|
-
defineTool("ado_okr_summary", {
|
|
678
|
-
description: "Get a full OKR summary including percent complete for all objectives",
|
|
679
|
-
parameters: z.object({
|
|
680
|
-
period: z.string().optional().describe("Optional OKR period such as '2026-Q2'"),
|
|
681
|
-
}),
|
|
682
|
-
handler: async (args) => {
|
|
683
|
-
if (!hasAdoPat()) {
|
|
684
|
-
return { error: "Azure DevOps OKR integration is not configured. Set ADO_PAT in ~/.chapterhouse/.env." };
|
|
685
|
-
}
|
|
686
|
-
return await adoOkrSummary(args.period);
|
|
687
|
-
},
|
|
688
|
-
}),
|
|
689
|
-
...(hasAdoPat() ? [
|
|
690
|
-
defineTool("generate_okr_report", {
|
|
691
|
-
description: "Generate a monthly OKR report narrative for the team. Queries ADO for current KR values and drafts an executive summary.",
|
|
692
|
-
parameters: z.object({
|
|
693
|
-
period: z.string().optional().describe("Optional OKR period such as '2026-Q2'"),
|
|
694
|
-
}),
|
|
695
|
-
handler: async (args) => {
|
|
696
|
-
const generator = deps.createReportGenerator?.() ?? await (async () => {
|
|
697
|
-
const { ReportGenerator } = await import("../integrations/report-generator.js");
|
|
698
|
-
return new ReportGenerator();
|
|
699
|
-
})();
|
|
700
|
-
return await generator.generateMonthlyReport(args.period?.trim() || getCurrentQuarter());
|
|
701
|
-
},
|
|
702
|
-
}),
|
|
703
|
-
] : []),
|
|
704
|
-
defineTool("hire_agent", {
|
|
705
|
-
description: "Create a new custom agent by writing an .agent.md file to ~/.chapterhouse/agents/. " +
|
|
706
|
-
"The agent will be available immediately after creation.",
|
|
707
|
-
parameters: z.object({
|
|
708
|
-
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe("Kebab-case identifier, e.g. 'data-analyst'"),
|
|
709
|
-
name: z.string().describe("Human-readable name"),
|
|
710
|
-
description: z.string().describe("One-line description of the agent's specialty"),
|
|
711
|
-
model: z.string().describe("Model to use (e.g. 'claude-sonnet-4.6', 'gpt-5.4', or 'auto')"),
|
|
712
|
-
system_prompt: z.string().describe("The agent's system prompt (markdown)"),
|
|
713
|
-
skills: z.array(z.string()).optional().describe("Skills to attach to this agent"),
|
|
714
|
-
tools: z.array(z.string()).optional().describe("Tool allowlist (omit for all execution tools)"),
|
|
715
|
-
}),
|
|
716
|
-
handler: async (args) => {
|
|
717
|
-
const err = createAgentFile(args.slug, args.name, args.description, args.model, args.system_prompt, args.skills, args.tools);
|
|
718
|
-
if (err)
|
|
719
|
-
return err;
|
|
720
|
-
// Reload registry
|
|
721
|
-
loadAgents();
|
|
722
|
-
return `Agent @${args.slug} created. It's ready for delegation.`;
|
|
723
|
-
},
|
|
724
|
-
}),
|
|
725
|
-
defineTool("fire_agent", {
|
|
726
|
-
description: "Remove a custom agent's .agent.md file and destroy its session. Cannot remove built-in agents.",
|
|
727
|
-
parameters: z.object({
|
|
728
|
-
slug: z.string().describe("The agent slug to remove"),
|
|
729
|
-
}),
|
|
730
|
-
handler: async (args) => {
|
|
731
|
-
const err = removeAgentFile(args.slug);
|
|
732
|
-
if (err)
|
|
733
|
-
return err;
|
|
734
|
-
loadAgents();
|
|
735
|
-
return `Agent @${args.slug} removed.`;
|
|
736
|
-
},
|
|
737
|
-
}),
|
|
738
|
-
defineTool("list_machine_sessions", {
|
|
739
|
-
description: "List ALL Copilot CLI sessions on this machine — including sessions started from VS Code, " +
|
|
740
|
-
"the terminal, or other tools. Shows session ID, summary, working directory. " +
|
|
741
|
-
"Use this when the user asks about existing sessions running on the machine. " +
|
|
742
|
-
"By default shows the 20 most recently active sessions.",
|
|
743
|
-
parameters: z.object({
|
|
744
|
-
cwd_filter: z.string().optional().describe("Optional: only show sessions whose working directory contains this string"),
|
|
745
|
-
limit: z.number().int().min(1).max(100).optional().describe("Chapterhouse sessions to return (default 20)"),
|
|
746
|
-
}),
|
|
747
|
-
handler: async (args) => {
|
|
748
|
-
const sessionStateDir = join(homedir(), ".copilot", "session-state");
|
|
749
|
-
const limit = args.limit || 20;
|
|
750
|
-
let entries = [];
|
|
751
|
-
try {
|
|
752
|
-
const dirs = readdirSync(sessionStateDir);
|
|
753
|
-
for (const dir of dirs) {
|
|
754
|
-
const yamlPath = join(sessionStateDir, dir, "workspace.yaml");
|
|
755
|
-
try {
|
|
756
|
-
const content = readFileSync(yamlPath, "utf-8");
|
|
757
|
-
const parsed = parseSimpleYaml(content);
|
|
758
|
-
if (args.cwd_filter && !parsed.cwd?.includes(args.cwd_filter))
|
|
759
|
-
continue;
|
|
760
|
-
entries.push({
|
|
761
|
-
id: parsed.id || dir,
|
|
762
|
-
cwd: parsed.cwd || "unknown",
|
|
763
|
-
summary: parsed.summary || "",
|
|
764
|
-
updatedAt: parsed.updated_at ? new Date(parsed.updated_at) : new Date(0),
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
catch {
|
|
768
|
-
// Skip dirs without valid workspace.yaml
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
catch (err) {
|
|
773
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
774
|
-
return "No Copilot sessions found on this machine (session state directory does not exist yet).";
|
|
775
|
-
}
|
|
776
|
-
return "Could not read session state directory.";
|
|
777
|
-
}
|
|
778
|
-
// Sort by most recently updated
|
|
779
|
-
entries.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
780
|
-
entries = entries.slice(0, limit);
|
|
781
|
-
if (entries.length === 0) {
|
|
782
|
-
return "No Copilot sessions found on this machine.";
|
|
783
|
-
}
|
|
784
|
-
const lines = entries.map((s) => {
|
|
785
|
-
const age = formatAge(s.updatedAt);
|
|
786
|
-
const summary = s.summary ? ` — ${s.summary}` : "";
|
|
787
|
-
return `• ID: ${s.id}\n ${s.cwd} (${age})${summary}`;
|
|
788
|
-
});
|
|
789
|
-
return `Found ${entries.length} session(s) (most recent first):\n${lines.join("\n")}`;
|
|
790
|
-
},
|
|
791
|
-
}),
|
|
792
|
-
defineTool("attach_machine_session", {
|
|
793
|
-
description: "Attach to an existing Copilot CLI session on this machine (e.g. one started from VS Code or terminal). " +
|
|
794
|
-
"Resumes the session so you can observe or interact with it.",
|
|
795
|
-
parameters: z.object({
|
|
796
|
-
session_id: z.string().describe("The session ID to attach to (from list_machine_sessions)"),
|
|
797
|
-
name: z.string().describe("A short name to reference this session by, e.g. 'vscode-main'"),
|
|
798
|
-
}),
|
|
799
|
-
handler: async (args) => {
|
|
800
|
-
try {
|
|
801
|
-
const session = await deps.client.resumeSession(args.session_id, {
|
|
802
|
-
model: config.copilotModel,
|
|
803
|
-
onPermissionRequest: approveAll,
|
|
804
|
-
});
|
|
805
|
-
const db = getDb();
|
|
806
|
-
db.prepare(`INSERT OR REPLACE INTO agent_sessions (slug, copilot_session_id, model, status)
|
|
807
|
-
VALUES (?, ?, ?, 'idle')`).run(args.name, args.session_id, config.copilotModel);
|
|
808
|
-
return `Attached to session ${args.session_id.slice(0, 8)}… as '${args.name}'.`;
|
|
809
|
-
}
|
|
810
|
-
catch (err) {
|
|
811
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
812
|
-
return `Failed to attach to session: ${msg}`;
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
}),
|
|
816
|
-
defineTool("list_skills", {
|
|
817
|
-
description: "List all available skills that Chapterhouse knows. Skills are instruction documents that teach Chapterhouse " +
|
|
818
|
-
"how to use external tools and services (e.g. Gmail, browser automation, YouTube transcripts). " +
|
|
819
|
-
"Shows skill name, description, and whether it's a local or global skill.",
|
|
820
|
-
parameters: z.object({}),
|
|
821
|
-
handler: async () => {
|
|
822
|
-
const skills = listSkills();
|
|
823
|
-
if (skills.length === 0) {
|
|
824
|
-
return "No skills installed yet. Use learn_skill to teach me something new.";
|
|
825
|
-
}
|
|
826
|
-
const lines = skills.map((s) => `• ${s.name} (${s.source}) — ${s.description}`);
|
|
827
|
-
return `Available skills (${skills.length}):\n${lines.join("\n")}`;
|
|
828
|
-
},
|
|
829
|
-
}),
|
|
830
|
-
defineTool("learn_skill", {
|
|
831
|
-
description: "Teach Chapterhouse a new skill by creating a SKILL.md instruction file. Use this when the user asks Chapterhouse " +
|
|
832
|
-
"to do something it doesn't know how to do yet (e.g. 'check my email', 'search the web'). " +
|
|
833
|
-
"First, use a worker session to research what CLI tools are available on the system (run 'which', " +
|
|
834
|
-
"'--help', etc.), then create the skill with the instructions you've learned. " +
|
|
835
|
-
"The skill becomes available on the next message (no restart needed).",
|
|
836
|
-
parameters: z.object({
|
|
837
|
-
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe("Short kebab-case identifier for the skill, e.g. 'gmail', 'web-search'"),
|
|
838
|
-
name: z.string().refine(s => !s.includes('\n'), "must be single-line").describe("Human-readable name for the skill, e.g. 'Gmail', 'Web Search'"),
|
|
839
|
-
description: z.string().refine(s => !s.includes('\n'), "must be single-line").describe("One-line description of when to use this skill"),
|
|
840
|
-
instructions: z.string().describe("Markdown instructions for how to use the skill. Include: what CLI tool to use, " +
|
|
841
|
-
"common commands with examples, authentication steps if needed, tips and gotchas. " +
|
|
842
|
-
"This becomes the SKILL.md content body."),
|
|
843
|
-
}),
|
|
844
|
-
handler: async (args) => {
|
|
845
|
-
return createSkill(args.slug, args.name, args.description, args.instructions);
|
|
846
|
-
},
|
|
847
|
-
}),
|
|
848
|
-
defineTool("uninstall_skill", {
|
|
849
|
-
description: "Remove a skill from Chapterhouse's local skills directory (~/.chapterhouse/skills/). " +
|
|
850
|
-
"The skill will no longer be available on the next message. " +
|
|
851
|
-
"Only works for local skills — bundled and global skills cannot be removed this way.",
|
|
852
|
-
parameters: z.object({
|
|
853
|
-
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe("The kebab-case slug of the skill to remove, e.g. 'gmail', 'web-search'"),
|
|
854
|
-
}),
|
|
855
|
-
handler: async (args) => {
|
|
856
|
-
const result = removeSkill(args.slug);
|
|
857
|
-
return result.message;
|
|
858
|
-
},
|
|
859
|
-
}),
|
|
860
|
-
defineTool("list_models", {
|
|
861
|
-
description: "List all available Copilot models. Shows model id, name, and billing tier. " +
|
|
862
|
-
"Marks the currently active model. Use when the user asks what models are available " +
|
|
863
|
-
"or wants to know which model is in use.",
|
|
864
|
-
parameters: z.object({}),
|
|
865
|
-
handler: async () => {
|
|
866
|
-
try {
|
|
867
|
-
const models = await deps.client.listModels();
|
|
868
|
-
if (models.length === 0) {
|
|
869
|
-
return "No models available.";
|
|
870
|
-
}
|
|
871
|
-
const current = config.copilotModel;
|
|
872
|
-
const lines = models.map((m) => {
|
|
873
|
-
const active = m.id === current ? " ← active" : "";
|
|
874
|
-
const billing = m.billing ? ` (${m.billing.multiplier}x)` : "";
|
|
875
|
-
return `• ${m.id}${billing}${active}`;
|
|
876
|
-
});
|
|
877
|
-
return `Available models (${models.length}):\n${lines.join("\n")}\n\nCurrent: ${current}`;
|
|
878
|
-
}
|
|
879
|
-
catch (err) {
|
|
880
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
881
|
-
return `Failed to list models: ${msg}`;
|
|
882
|
-
}
|
|
883
|
-
},
|
|
884
|
-
}),
|
|
885
|
-
defineTool("switch_model", {
|
|
886
|
-
description: "Switch the Copilot model Chapterhouse uses for conversations. Takes effect on the next message. " +
|
|
887
|
-
"The change is persisted across restarts. Use when the user asks to change or switch models.",
|
|
888
|
-
parameters: z.object({
|
|
889
|
-
model_id: z.string().describe("The model id to switch to (from list_models)"),
|
|
890
|
-
}),
|
|
891
|
-
handler: async (args) => {
|
|
892
|
-
try {
|
|
893
|
-
const models = await deps.client.listModels();
|
|
894
|
-
const match = models.find((m) => m.id === args.model_id);
|
|
895
|
-
if (!match) {
|
|
896
|
-
const suggestions = models
|
|
897
|
-
.filter((m) => m.id.includes(args.model_id) || m.id.toLowerCase().includes(args.model_id.toLowerCase()))
|
|
898
|
-
.map((m) => m.id);
|
|
899
|
-
const hint = suggestions.length > 0
|
|
900
|
-
? ` Did you mean: ${suggestions.join(", ")}?`
|
|
901
|
-
: " Use list_models to see available options.";
|
|
902
|
-
return `Model '${args.model_id}' not found.${hint}`;
|
|
903
|
-
}
|
|
904
|
-
const previous = config.copilotModel;
|
|
905
|
-
config.copilotModel = args.model_id;
|
|
906
|
-
persistModel(args.model_id);
|
|
907
|
-
// Apply model change to the live session immediately
|
|
908
|
-
try {
|
|
909
|
-
await switchSessionModel(args.model_id);
|
|
910
|
-
}
|
|
911
|
-
catch (err) {
|
|
912
|
-
log.warn({ err: err instanceof Error ? err.message : err }, "setModel() failed during switch_model, will apply on next session");
|
|
913
|
-
}
|
|
914
|
-
// Disable router when manually switching — user has explicit preference
|
|
915
|
-
if (getRouterConfig().enabled) {
|
|
916
|
-
updateRouterConfig({ enabled: false });
|
|
917
|
-
return `Switched model from '${previous}' to '${args.model_id}'. Auto-routing disabled (use /auto or toggle_auto to re-enable).`;
|
|
918
|
-
}
|
|
919
|
-
return `Switched model from '${previous}' to '${args.model_id}'.`;
|
|
920
|
-
}
|
|
921
|
-
catch (err) {
|
|
922
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
923
|
-
return `Failed to switch model: ${msg}`;
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
}),
|
|
927
|
-
defineTool("toggle_auto", {
|
|
928
|
-
description: "Enable or disable automatic model routing (auto mode). When enabled, Chapterhouse automatically picks " +
|
|
929
|
-
"the best model (fast/standard/premium) for each message to save cost and optimize speed. " +
|
|
930
|
-
"Use when the user asks to turn auto-routing on or off.",
|
|
931
|
-
parameters: z.object({
|
|
932
|
-
enabled: z.boolean().describe("true to enable auto-routing, false to disable"),
|
|
933
|
-
}),
|
|
934
|
-
handler: async (args) => {
|
|
935
|
-
const updated = updateRouterConfig({ enabled: args.enabled });
|
|
936
|
-
if (args.enabled) {
|
|
937
|
-
const tiers = updated.tierModels;
|
|
938
|
-
return `Auto-routing enabled. Tier models:\n• fast: ${tiers.fast}\n• standard: ${tiers.standard}\n• premium: ${tiers.premium}\n\nMax will automatically pick the best model for each message.`;
|
|
939
|
-
}
|
|
940
|
-
return `Auto-routing disabled. Using fixed model: ${config.copilotModel}`;
|
|
941
|
-
},
|
|
942
|
-
}),
|
|
943
|
-
defineTool("memory_remember", {
|
|
944
|
-
description: "Write scoped agent memory (observation or decision) into the SQLite memory store. " +
|
|
945
|
-
"Use this for the new agent-memory system, not the legacy wiki-backed remember tool.",
|
|
946
|
-
parameters: z.object({
|
|
947
|
-
content: z.string().describe("Observation content or decision rationale."),
|
|
948
|
-
scope: z.string().optional().describe("Optional memory scope slug. Defaults to active scope, then keyword inference."),
|
|
949
|
-
kind: z.enum(["observation", "decision"]).optional().describe("Memory entry kind. Defaults to observation."),
|
|
950
|
-
entity_name: z.string().optional().describe("Optional entity name to attach. Auto-upserts the entity if provided."),
|
|
951
|
-
entity_kind: z.string().optional().describe("Required when entity_name is provided."),
|
|
952
|
-
title: z.string().optional().describe("Required for decision entries."),
|
|
953
|
-
decided_at: z.string().optional().describe("Decision date. Defaults to today."),
|
|
954
|
-
tier: z.enum(["hot", "warm", "cold"]).optional().describe("Storage tier. Defaults to warm."),
|
|
955
|
-
}),
|
|
956
|
-
handler: async (args) => {
|
|
957
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
958
|
-
if (denied)
|
|
959
|
-
return denied;
|
|
960
|
-
if (args.entity_name && !args.entity_kind) {
|
|
961
|
-
return "entity_kind is required when entity_name is provided.";
|
|
962
|
-
}
|
|
963
|
-
try {
|
|
964
|
-
const { scopeId, scopeSlug } = resolveMemoryScopeForWrite(args.scope, args.content);
|
|
965
|
-
const kind = args.kind ?? "observation";
|
|
966
|
-
const entity = args.entity_name
|
|
967
|
-
? upsertEntity({
|
|
968
|
-
scope_id: scopeId,
|
|
969
|
-
kind: args.entity_kind,
|
|
970
|
-
name: args.entity_name,
|
|
971
|
-
tier: args.tier ?? "warm",
|
|
972
|
-
})
|
|
973
|
-
: undefined;
|
|
974
|
-
if (kind === "decision") {
|
|
975
|
-
if (!args.title) {
|
|
976
|
-
return "title is required when kind='decision'.";
|
|
977
|
-
}
|
|
978
|
-
const decision = recordDecision({
|
|
979
|
-
scope_id: scopeId,
|
|
980
|
-
entity_id: entity?.id,
|
|
981
|
-
title: args.title,
|
|
982
|
-
rationale: args.content,
|
|
983
|
-
decided_at: args.decided_at ?? new Date().toISOString().slice(0, 10),
|
|
984
|
-
tier: args.tier ?? "warm",
|
|
985
|
-
});
|
|
986
|
-
if (entity) {
|
|
987
|
-
appendTimeline(topicPagePath(entity.kind, entity.name), `${decision.title}\n\n${decision.rationale}`);
|
|
988
|
-
}
|
|
989
|
-
hookDispatcher.dispatch("memory:decision", {
|
|
990
|
-
id: decision.id,
|
|
991
|
-
scope_id: scopeId,
|
|
992
|
-
title: decision.title,
|
|
993
|
-
rationale: decision.rationale,
|
|
994
|
-
}).catch((err) => {
|
|
995
|
-
log.error({ err }, "memory.hooks.decision.dispatch_error");
|
|
996
|
-
});
|
|
997
|
-
return {
|
|
998
|
-
ok: true,
|
|
999
|
-
id: decision.id,
|
|
1000
|
-
scope: scopeSlug,
|
|
1001
|
-
kind,
|
|
1002
|
-
entity_id: entity?.id,
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
const observation = recordObservation({
|
|
1006
|
-
scope_id: scopeId,
|
|
1007
|
-
entity_id: entity?.id,
|
|
1008
|
-
content: args.content,
|
|
1009
|
-
source: `agent:${agentsModule.getCurrentToolAgentSlug?.() ?? "chapterhouse"}`,
|
|
1010
|
-
tier: args.tier ?? "warm",
|
|
1011
|
-
});
|
|
1012
|
-
return {
|
|
1013
|
-
ok: true,
|
|
1014
|
-
id: observation.id,
|
|
1015
|
-
scope: scopeSlug,
|
|
1016
|
-
kind,
|
|
1017
|
-
entity_id: entity?.id,
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
catch (err) {
|
|
1021
|
-
return err instanceof Error ? err.message : String(err);
|
|
1022
|
-
}
|
|
1023
|
-
},
|
|
1024
|
-
}),
|
|
1025
|
-
defineTool("memory_propose", {
|
|
1026
|
-
description: "Queue a proposed scoped memory item for orchestrator review at end-of-task. " +
|
|
1027
|
-
"Available to all agents; writes land in mem_inbox as pending proposals.",
|
|
1028
|
-
parameters: memoryProposeArgsSchema,
|
|
1029
|
-
handler: async (args) => {
|
|
1030
|
-
try {
|
|
1031
|
-
const parsedArgs = memoryProposeArgsSchema.parse(args);
|
|
1032
|
-
const payload = parsedArgs.kind === "observation"
|
|
1033
|
-
? observationProposalPayloadSchema.parse(parsedArgs.payload)
|
|
1034
|
-
: parsedArgs.kind === "decision"
|
|
1035
|
-
? decisionProposalPayloadSchema.parse(parsedArgs.payload)
|
|
1036
|
-
: parsedArgs.kind === "entity"
|
|
1037
|
-
? entityProposalPayloadSchema.parse(parsedArgs.payload)
|
|
1038
|
-
: actionItemProposalPayloadSchema.parse(parsedArgs.payload);
|
|
1039
|
-
const proposal = queueMemoryProposal({
|
|
1040
|
-
kind: parsedArgs.kind,
|
|
1041
|
-
scopeSlug: resolveProposalScopeSlug(parsedArgs.scope_slug),
|
|
1042
|
-
payload,
|
|
1043
|
-
confidence: parsedArgs.confidence ?? 0.5,
|
|
1044
|
-
reason: parsedArgs.reason,
|
|
1045
|
-
sourceAgent: agentsModule.getCurrentToolAgentSlug?.() ?? "chapterhouse",
|
|
1046
|
-
sourceTaskId: agentsModule.getCurrentToolTaskId?.(),
|
|
1047
|
-
});
|
|
1048
|
-
return {
|
|
1049
|
-
proposal_id: proposal.id,
|
|
1050
|
-
status: "queued",
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
catch (err) {
|
|
1054
|
-
if (err instanceof z.ZodError) {
|
|
1055
|
-
return err.issues.map((issue) => issue.message).join("; ");
|
|
1056
|
-
}
|
|
1057
|
-
return err instanceof Error ? err.message : String(err);
|
|
1058
|
-
}
|
|
1059
|
-
},
|
|
1060
|
-
}),
|
|
1061
|
-
defineTool("memory_create_scope", {
|
|
1062
|
-
description: "Create a new user-defined memory scope. Slugs must be unique kebab-case; baseline installs only include global and chapterhouse.",
|
|
1063
|
-
parameters: z.object({
|
|
1064
|
-
slug: z.string()
|
|
1065
|
-
.trim()
|
|
1066
|
-
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "slug must be unique kebab-case"),
|
|
1067
|
-
title: z.string().trim().min(1, "title is required"),
|
|
1068
|
-
description: z.string().optional(),
|
|
1069
|
-
}),
|
|
1070
|
-
handler: async (args) => {
|
|
1071
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1072
|
-
if (denied)
|
|
1073
|
-
return denied;
|
|
1074
|
-
const parsed = z.object({
|
|
1075
|
-
slug: z.string()
|
|
1076
|
-
.trim()
|
|
1077
|
-
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "slug must be unique kebab-case"),
|
|
1078
|
-
title: z.string().trim().min(1, "title is required"),
|
|
1079
|
-
description: z.string().optional(),
|
|
1080
|
-
}).safeParse(args);
|
|
1081
|
-
if (!parsed.success) {
|
|
1082
|
-
return parsed.error.issues.map((issue) => issue.message).join("; ");
|
|
1083
|
-
}
|
|
1084
|
-
try {
|
|
1085
|
-
if (getMemoryScope(parsed.data.slug)) {
|
|
1086
|
-
return `Memory scope '${parsed.data.slug}' already exists.`;
|
|
1087
|
-
}
|
|
1088
|
-
const scope = createMemoryScope({
|
|
1089
|
-
slug: parsed.data.slug,
|
|
1090
|
-
title: parsed.data.title,
|
|
1091
|
-
description: parsed.data.description ?? "",
|
|
1092
|
-
keywords: [parsed.data.slug],
|
|
1093
|
-
});
|
|
1094
|
-
hookDispatcher.dispatch("scope:created", {
|
|
1095
|
-
slug: scope.slug,
|
|
1096
|
-
title: scope.title,
|
|
1097
|
-
description: scope.description,
|
|
1098
|
-
}).catch((err) => {
|
|
1099
|
-
log.error({ err }, "memory.hooks.scope_created.dispatch_error");
|
|
1100
|
-
});
|
|
1101
|
-
return {
|
|
1102
|
-
ok: true,
|
|
1103
|
-
scope: {
|
|
1104
|
-
slug: scope.slug,
|
|
1105
|
-
title: scope.title,
|
|
1106
|
-
description: scope.description,
|
|
1107
|
-
active: scope.active,
|
|
1108
|
-
},
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
catch (err) {
|
|
1112
|
-
return err instanceof Error ? err.message : String(err);
|
|
1113
|
-
}
|
|
1114
|
-
},
|
|
1115
|
-
}),
|
|
1116
|
-
defineTool("memory_add_action_item", {
|
|
1117
|
-
description: "Add a scoped memory action item/reminder. Orchestrator-only write tool for follow-ups and proactive reminders.",
|
|
1118
|
-
parameters: z.object({
|
|
1119
|
-
scope: z.string().optional().describe("Optional memory scope slug. Defaults to active scope, then keyword inference."),
|
|
1120
|
-
title: z.string().min(1).describe("Short action item title."),
|
|
1121
|
-
detail: z.string().optional().describe("Longer action item detail."),
|
|
1122
|
-
due_at: z.string().optional().describe("Optional ISO due timestamp."),
|
|
1123
|
-
entity_name: z.string().optional().describe("Optional entity name to attach. Auto-upserts the entity if provided."),
|
|
1124
|
-
entity_kind: z.string().optional().describe("Required when entity_name is provided."),
|
|
1125
|
-
source: z.string().optional().describe("Action item source, e.g. manual, subagent_proposal, external."),
|
|
1126
|
-
}),
|
|
1127
|
-
handler: async (args) => {
|
|
1128
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1129
|
-
if (denied)
|
|
1130
|
-
return denied;
|
|
1131
|
-
if (args.entity_name && !args.entity_kind) {
|
|
1132
|
-
return "entity_kind is required when entity_name is provided.";
|
|
1133
|
-
}
|
|
1134
|
-
try {
|
|
1135
|
-
const { scopeId, scopeSlug } = resolveMemoryScopeForWrite(args.scope, `${args.title}\n${args.detail ?? ""}`);
|
|
1136
|
-
const entity = args.entity_name
|
|
1137
|
-
? upsertEntity({
|
|
1138
|
-
scope_id: scopeId,
|
|
1139
|
-
kind: args.entity_kind,
|
|
1140
|
-
name: args.entity_name,
|
|
1141
|
-
})
|
|
1142
|
-
: undefined;
|
|
1143
|
-
const actionItem = recordActionItem({
|
|
1144
|
-
scope_id: scopeId,
|
|
1145
|
-
entity_id: entity?.id,
|
|
1146
|
-
title: args.title,
|
|
1147
|
-
detail: args.detail,
|
|
1148
|
-
due_at: args.due_at,
|
|
1149
|
-
source: args.source ?? "manual",
|
|
1150
|
-
});
|
|
1151
|
-
return {
|
|
1152
|
-
ok: true,
|
|
1153
|
-
id: actionItem.id,
|
|
1154
|
-
scope: scopeSlug,
|
|
1155
|
-
status: actionItem.status,
|
|
1156
|
-
entity_id: entity?.id,
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
catch (err) {
|
|
1160
|
-
return err instanceof Error ? err.message : String(err);
|
|
1161
|
-
}
|
|
1162
|
-
},
|
|
1163
|
-
}),
|
|
1164
|
-
defineTool("memory_complete_action_item", {
|
|
1165
|
-
description: "Complete a memory action item. Orchestrator-only write tool.",
|
|
1166
|
-
parameters: z.object({
|
|
1167
|
-
id: z.number().int().positive(),
|
|
1168
|
-
resolution_reason: z.string().optional(),
|
|
1169
|
-
}),
|
|
1170
|
-
handler: async (args) => {
|
|
1171
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1172
|
-
if (denied)
|
|
1173
|
-
return denied;
|
|
1174
|
-
try {
|
|
1175
|
-
const actionItem = completeActionItem(args.id, args.resolution_reason);
|
|
1176
|
-
return { ok: true, id: actionItem.id, status: actionItem.status, resolved_at: actionItem.resolvedAt };
|
|
1177
|
-
}
|
|
1178
|
-
catch (err) {
|
|
1179
|
-
return err instanceof Error ? err.message : String(err);
|
|
1180
|
-
}
|
|
1181
|
-
},
|
|
1182
|
-
}),
|
|
1183
|
-
defineTool("memory_drop_action_item", {
|
|
1184
|
-
description: "Drop a memory action item with a reason. Orchestrator-only write tool.",
|
|
1185
|
-
parameters: z.object({
|
|
1186
|
-
id: z.number().int().positive(),
|
|
1187
|
-
reason: z.string().min(1),
|
|
1188
|
-
}),
|
|
1189
|
-
handler: async (args) => {
|
|
1190
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1191
|
-
if (denied)
|
|
1192
|
-
return denied;
|
|
1193
|
-
try {
|
|
1194
|
-
const actionItem = dropActionItem(args.id, args.reason);
|
|
1195
|
-
return { ok: true, id: actionItem.id, status: actionItem.status, resolved_at: actionItem.resolvedAt };
|
|
1196
|
-
}
|
|
1197
|
-
catch (err) {
|
|
1198
|
-
return err instanceof Error ? err.message : String(err);
|
|
1199
|
-
}
|
|
1200
|
-
},
|
|
1201
|
-
}),
|
|
1202
|
-
defineTool("memory_snooze_action_item", {
|
|
1203
|
-
description: "Snooze a memory action item until an ISO timestamp. Orchestrator-only write tool.",
|
|
1204
|
-
parameters: z.object({
|
|
1205
|
-
id: z.number().int().positive(),
|
|
1206
|
-
snooze_until: z.string().min(1),
|
|
1207
|
-
}),
|
|
1208
|
-
handler: async (args) => {
|
|
1209
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1210
|
-
if (denied)
|
|
1211
|
-
return denied;
|
|
1212
|
-
try {
|
|
1213
|
-
const actionItem = snoozeActionItem(args.id, args.snooze_until);
|
|
1214
|
-
return {
|
|
1215
|
-
ok: true,
|
|
1216
|
-
id: actionItem.id,
|
|
1217
|
-
status: actionItem.status,
|
|
1218
|
-
snooze_until: actionItem.snoozeUntil,
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
catch (err) {
|
|
1222
|
-
return err instanceof Error ? err.message : String(err);
|
|
1223
|
-
}
|
|
1224
|
-
},
|
|
1225
|
-
}),
|
|
1226
|
-
defineTool("memory_list_action_items", {
|
|
1227
|
-
description: "List scoped memory action items/reminders. Defaults to currently actionable open items.",
|
|
1228
|
-
parameters: z.object({
|
|
1229
|
-
scope: z.string().optional().describe("Optional memory scope slug. Defaults to active scope when available."),
|
|
1230
|
-
status: z.enum(["open", "done", "dropped", "snoozed"]).optional().describe("Optional status filter."),
|
|
1231
|
-
due_before: z.string().optional().describe("Optional ISO timestamp; only include items due at or before this time."),
|
|
1232
|
-
includeArchived: z.boolean().optional().describe("Include cold-tier items. Defaults to false."),
|
|
1233
|
-
}),
|
|
1234
|
-
handler: async (args) => {
|
|
1235
|
-
const requestedScope = args.scope ? getMemoryScope(args.scope) : undefined;
|
|
1236
|
-
if (args.scope && !requestedScope) {
|
|
1237
|
-
return `Unknown memory scope '${args.scope}'.`;
|
|
1238
|
-
}
|
|
1239
|
-
const activeScope = getMemoryActiveScope();
|
|
1240
|
-
const scope = requestedScope ?? activeScope ?? undefined;
|
|
1241
|
-
const actionItems = listActionItems({
|
|
1242
|
-
scope_id: scope?.id,
|
|
1243
|
-
status: args.status,
|
|
1244
|
-
due_before: args.due_before,
|
|
1245
|
-
includeArchived: args.includeArchived,
|
|
1246
|
-
});
|
|
1247
|
-
return {
|
|
1248
|
-
active_scope: activeScope ? { slug: activeScope.slug, title: activeScope.title } : null,
|
|
1249
|
-
scope: scope ? { slug: scope.slug, title: scope.title } : null,
|
|
1250
|
-
action_items: actionItems.map((item) => ({
|
|
1251
|
-
id: item.id,
|
|
1252
|
-
scope_id: item.scopeId,
|
|
1253
|
-
entity_id: item.entityId,
|
|
1254
|
-
title: item.title,
|
|
1255
|
-
detail: item.detail,
|
|
1256
|
-
status: item.status,
|
|
1257
|
-
due_at: item.dueAt,
|
|
1258
|
-
snooze_until: item.snoozeUntil,
|
|
1259
|
-
source: item.source,
|
|
1260
|
-
created_at: item.createdAt,
|
|
1261
|
-
updated_at: item.updatedAt,
|
|
1262
|
-
resolved_at: item.resolvedAt,
|
|
1263
|
-
resolution_reason: item.resolutionReason,
|
|
1264
|
-
})),
|
|
1265
|
-
};
|
|
1266
|
-
},
|
|
1267
|
-
}),
|
|
1268
|
-
defineTool("memory_recall", {
|
|
1269
|
-
description: "Search scoped agent memory with FTS-backed recall. Use this for the new agent-memory store, not the wiki-backed recall tool.",
|
|
1270
|
-
parameters: z.object({
|
|
1271
|
-
query: z.string().describe("Query text to search for."),
|
|
1272
|
-
scope: z.string().optional().describe("Optional scope slug. Defaults to the active scope when available."),
|
|
1273
|
-
kinds: z.array(z.enum(["observation", "decision", "entity", "action_item"])).optional()
|
|
1274
|
-
.describe("Optional filter for memory entry kinds."),
|
|
1275
|
-
limit: z.number().int().positive().optional().describe("Maximum number of ranked hits to return. Defaults to 10."),
|
|
1276
|
-
includeSuperseded: z.boolean().optional().describe("Include superseded observations and decisions. Defaults to false."),
|
|
1277
|
-
includeArchived: z.boolean().optional().describe("Include archived observations and decisions. Defaults to false."),
|
|
1278
|
-
includeCold: z.boolean().optional().describe("Include cold-tier rows. Defaults to false."),
|
|
1279
|
-
}),
|
|
1280
|
-
handler: async (args) => {
|
|
1281
|
-
const requestedScope = args.scope ? getMemoryScope(args.scope) : undefined;
|
|
1282
|
-
if (args.scope && !requestedScope) {
|
|
1283
|
-
return `Unknown memory scope '${args.scope}'.`;
|
|
1284
|
-
}
|
|
1285
|
-
const result = recallMemory({
|
|
1286
|
-
query: args.query,
|
|
1287
|
-
scope_id: requestedScope?.id,
|
|
1288
|
-
kinds: args.kinds,
|
|
1289
|
-
limit: args.limit ?? 10,
|
|
1290
|
-
includeSuperseded: args.includeSuperseded,
|
|
1291
|
-
includeArchived: args.includeArchived,
|
|
1292
|
-
includeCold: args.includeCold,
|
|
1293
|
-
});
|
|
1294
|
-
return {
|
|
1295
|
-
active_scope: result.activeScope
|
|
1296
|
-
? { slug: result.activeScope.slug, title: result.activeScope.title }
|
|
1297
|
-
: null,
|
|
1298
|
-
hot_tier: result.hotTier,
|
|
1299
|
-
hits: result.hits.map((hit) => ({
|
|
1300
|
-
kind: hit.kind,
|
|
1301
|
-
id: hit.id,
|
|
1302
|
-
scope: hit.scope,
|
|
1303
|
-
content: hit.content,
|
|
1304
|
-
decided_at: hit.decidedAt,
|
|
1305
|
-
score: hit.score,
|
|
1306
|
-
snippet: hit.snippet,
|
|
1307
|
-
})),
|
|
1308
|
-
};
|
|
1309
|
-
},
|
|
1310
|
-
}),
|
|
1311
|
-
defineTool("memory_housekeep", {
|
|
1312
|
-
description: "Run the scoped agent-memory housekeeping pipeline. Orchestrator-only write-tier maintenance tool.",
|
|
1313
|
-
parameters: z.object({
|
|
1314
|
-
scope_slug: z.string().optional().describe("Optional scope slug. Defaults to the active scope."),
|
|
1315
|
-
all_scopes: z.boolean().optional().describe("Run scoped passes for all active scopes instead of one scope."),
|
|
1316
|
-
passes: z.array(z.string()).optional().describe("Optional pass names: dedup_observations, dedup_decisions, orphan_cleanup, decay, compact_inbox."),
|
|
1317
|
-
}),
|
|
1318
|
-
handler: async (args) => {
|
|
1319
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1320
|
-
if (denied)
|
|
1321
|
-
return denied;
|
|
1322
|
-
if (args.scope_slug && args.all_scopes) {
|
|
1323
|
-
return "Pass either scope_slug or all_scopes, not both.";
|
|
1324
|
-
}
|
|
1325
|
-
try {
|
|
1326
|
-
const requestedScope = args.scope_slug ? getMemoryScope(args.scope_slug) : undefined;
|
|
1327
|
-
if (args.scope_slug && !requestedScope) {
|
|
1328
|
-
return `Unknown memory scope '${args.scope_slug}'.`;
|
|
1329
|
-
}
|
|
1330
|
-
const result = await runHousekeeping({
|
|
1331
|
-
scopeIds: requestedScope ? [requestedScope.id] : undefined,
|
|
1332
|
-
allScopes: args.all_scopes,
|
|
1333
|
-
passes: args.passes,
|
|
1334
|
-
});
|
|
1335
|
-
return {
|
|
1336
|
-
ok: result.summaries.every((summary) => summary.errors.length === 0),
|
|
1337
|
-
scope_ids: result.scopeIds,
|
|
1338
|
-
total_examined: result.totalExamined,
|
|
1339
|
-
total_modified: result.totalModified,
|
|
1340
|
-
duration_ms: result.durationMs,
|
|
1341
|
-
summaries: result.summaries,
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
catch (err) {
|
|
1345
|
-
return err instanceof Error ? err.message : String(err);
|
|
1346
|
-
}
|
|
1347
|
-
},
|
|
1348
|
-
}),
|
|
1349
|
-
defineTool("memory_reflect", {
|
|
1350
|
-
description: "Synthesize durable scoped memory patterns from repeated observations. Orchestrator-only reflect tool.",
|
|
1351
|
-
parameters: z.object({
|
|
1352
|
-
scope: z.string().optional(),
|
|
1353
|
-
}),
|
|
1354
|
-
handler: async (args) => {
|
|
1355
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1356
|
-
if (denied)
|
|
1357
|
-
return denied;
|
|
1358
|
-
try {
|
|
1359
|
-
if (args.scope) {
|
|
1360
|
-
const requestedScope = getMemoryScope(args.scope);
|
|
1361
|
-
if (!requestedScope) {
|
|
1362
|
-
return `Unknown memory scope '${args.scope}'.`;
|
|
1363
|
-
}
|
|
1364
|
-
const result = await reflectOnScope(args.scope, getDb());
|
|
1365
|
-
return {
|
|
1366
|
-
ok: true,
|
|
1367
|
-
scope: args.scope,
|
|
1368
|
-
patterns_created: result.patternsCreated,
|
|
1369
|
-
patterns_updated: result.patternsUpdated,
|
|
1370
|
-
contradictions_found: result.contradictionsFound,
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
const results = await reflectAllScopes(getDb());
|
|
1374
|
-
const totals = Object.values(results).reduce((acc, result) => ({
|
|
1375
|
-
patterns_created: acc.patterns_created + result.patternsCreated,
|
|
1376
|
-
patterns_updated: acc.patterns_updated + result.patternsUpdated,
|
|
1377
|
-
contradictions_found: acc.contradictions_found + result.contradictionsFound,
|
|
1378
|
-
}), {
|
|
1379
|
-
patterns_created: 0,
|
|
1380
|
-
patterns_updated: 0,
|
|
1381
|
-
contradictions_found: 0,
|
|
1382
|
-
});
|
|
1383
|
-
return {
|
|
1384
|
-
ok: true,
|
|
1385
|
-
scope: "all",
|
|
1386
|
-
...totals,
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
catch (err) {
|
|
1390
|
-
return err instanceof Error ? err.message : String(err);
|
|
1391
|
-
}
|
|
1392
|
-
},
|
|
1393
|
-
}),
|
|
1394
|
-
defineTool("memory_promote", {
|
|
1395
|
-
description: "Promote a memory row to the hot tier. Orchestrator-only manual override.",
|
|
1396
|
-
parameters: z.object({
|
|
1397
|
-
table: memoryTierTableSchema,
|
|
1398
|
-
id: z.number().int().positive(),
|
|
1399
|
-
reason: z.string().min(1),
|
|
1400
|
-
}),
|
|
1401
|
-
handler: async (args) => {
|
|
1402
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1403
|
-
if (denied)
|
|
1404
|
-
return denied;
|
|
1405
|
-
try {
|
|
1406
|
-
promoteToHot(args.table, args.id, args.reason);
|
|
1407
|
-
return { ok: true, table: args.table, id: args.id, tier: "hot" };
|
|
1408
|
-
}
|
|
1409
|
-
catch (err) {
|
|
1410
|
-
return err instanceof Error ? err.message : String(err);
|
|
1411
|
-
}
|
|
1412
|
-
},
|
|
1413
|
-
}),
|
|
1414
|
-
defineTool("memory_demote", {
|
|
1415
|
-
description: "Demote a memory row to warm or cold tier. Orchestrator-only manual override.",
|
|
1416
|
-
parameters: z.object({
|
|
1417
|
-
table: memoryTierTableSchema,
|
|
1418
|
-
id: z.number().int().positive(),
|
|
1419
|
-
reason: z.string().min(1),
|
|
1420
|
-
tier: z.enum(["warm", "cold"]).optional().describe("Target demotion tier. Defaults to warm."),
|
|
1421
|
-
}),
|
|
1422
|
-
handler: async (args) => {
|
|
1423
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1424
|
-
if (denied)
|
|
1425
|
-
return denied;
|
|
1426
|
-
try {
|
|
1427
|
-
if (args.tier === "cold") {
|
|
1428
|
-
demoteToCold(args.table, args.id, args.reason);
|
|
1429
|
-
return { ok: true, table: args.table, id: args.id, tier: "cold" };
|
|
1430
|
-
}
|
|
1431
|
-
demoteToWarm(args.table, args.id, args.reason);
|
|
1432
|
-
return { ok: true, table: args.table, id: args.id, tier: "warm" };
|
|
1433
|
-
}
|
|
1434
|
-
catch (err) {
|
|
1435
|
-
return err instanceof Error ? err.message : String(err);
|
|
1436
|
-
}
|
|
1437
|
-
},
|
|
1438
|
-
}),
|
|
1439
|
-
defineTool("memory_set_scope", {
|
|
1440
|
-
description: "Set or clear the active scope for the agent-memory system. This affects default routing for memory_remember and memory_recall.",
|
|
1441
|
-
parameters: z.object({
|
|
1442
|
-
slug: z.string().nullable().describe("Scope slug to activate, or null to clear the active scope."),
|
|
1443
|
-
}),
|
|
1444
|
-
handler: async (args) => {
|
|
1445
|
-
const denied = requireOrchestratorMemoryWrite();
|
|
1446
|
-
if (denied)
|
|
1447
|
-
return denied;
|
|
1448
|
-
try {
|
|
1449
|
-
const previousScope = getMemoryActiveScope();
|
|
1450
|
-
const nextScope = args.slug === null ? null : (getMemoryScope(args.slug) ?? null);
|
|
1451
|
-
if (args.slug !== null && !nextScope) {
|
|
1452
|
-
return `Unknown memory scope '${args.slug}'.`;
|
|
1453
|
-
}
|
|
1454
|
-
const previousSlug = previousScope?.slug ?? null;
|
|
1455
|
-
const nextSlug = nextScope?.slug ?? null;
|
|
1456
|
-
const didChange = previousSlug !== nextSlug;
|
|
1457
|
-
const sessionKey = getCurrentSessionKey();
|
|
1458
|
-
if (didChange) {
|
|
1459
|
-
maybeScheduleScopeChangeCheckpoint(sessionKey, previousScope, nextScope);
|
|
1460
|
-
}
|
|
1461
|
-
const activeScope = setMemoryActiveScope(args.slug);
|
|
1462
|
-
if (didChange) {
|
|
1463
|
-
invalidateOrchestratorSession(sessionKey);
|
|
1464
|
-
resetCheckpointSessionState(sessionKey);
|
|
1465
|
-
}
|
|
1466
|
-
return {
|
|
1467
|
-
active_scope: activeScope
|
|
1468
|
-
? { slug: activeScope.slug, title: activeScope.title }
|
|
1469
|
-
: null,
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
catch (err) {
|
|
1473
|
-
return err instanceof Error ? err.message : String(err);
|
|
1474
|
-
}
|
|
1475
|
-
},
|
|
1476
|
-
}),
|
|
1477
|
-
// ----- Removed legacy wiki facades kept as compatibility stubs -----
|
|
1478
|
-
defineTool("remember", {
|
|
1479
|
-
description: "REMOVED: Use wiki_update or memory_remember instead.",
|
|
1480
|
-
parameters: z.object({}).passthrough(),
|
|
1481
|
-
handler: async () => "This tool has been removed. Use wiki_update to write to wiki pages, or memory_remember for agent memory.",
|
|
1482
|
-
}),
|
|
1483
|
-
defineTool("recall", {
|
|
1484
|
-
description: "REMOVED: Use wiki_search or memory_recall instead.",
|
|
1485
|
-
parameters: z.object({}).passthrough(),
|
|
1486
|
-
handler: async () => "This tool has been removed. Use wiki_search to search wiki pages, or memory_recall for agent memory.",
|
|
1487
|
-
}),
|
|
1488
|
-
defineTool("forget", {
|
|
1489
|
-
description: "REMOVED: Use wiki_update instead.",
|
|
1490
|
-
parameters: z.object({}).passthrough(),
|
|
1491
|
-
handler: async () => "This tool has been removed. Use wiki_update to modify wiki pages.",
|
|
1492
|
-
}),
|
|
1493
|
-
// ----- New wiki tools -----
|
|
1494
|
-
defineTool("wiki_search", {
|
|
1495
|
-
description: "Search Chapterhouse's wiki knowledge base. Returns matching page titles, paths, and summaries " +
|
|
1496
|
-
"from the wiki index. Use this to find relevant knowledge before answering questions.",
|
|
1497
|
-
parameters: z.object({
|
|
1498
|
-
query: z.string().describe("What to search for in the wiki"),
|
|
1499
|
-
}),
|
|
1500
|
-
handler: async (args) => {
|
|
1501
|
-
ensureWikiStructure();
|
|
1502
|
-
const matches = searchIndex(args.query, 10);
|
|
1503
|
-
if (matches.length === 0)
|
|
1504
|
-
return "No matching wiki pages found.";
|
|
1505
|
-
const lines = matches.map((m) => `• [${m.title}](${m.path}) — ${m.summary}`);
|
|
1506
|
-
return `Found ${matches.length} page(s):\n${lines.join("\n")}`;
|
|
1507
|
-
},
|
|
1508
|
-
}),
|
|
1509
|
-
defineTool("wiki_read", {
|
|
1510
|
-
description: "Read a specific wiki page by path. Use after wiki_search to read full page content. " +
|
|
1511
|
-
"Paths are relative to the wiki root. Layout: entity categories (projects, people, orgs, tools, " +
|
|
1512
|
-
"topics, areas) live at 'pages/<category>/<topic>/index.md' (the topic overview) plus optional " +
|
|
1513
|
-
"'pages/<category>/<topic>/<facet>.md' sub-pages; flat categories at 'pages/<category>.md' " +
|
|
1514
|
-
"(preferences, facts, routines, decisions); daily summaries at 'pages/conversations/YYYY-MM-DD.md'.",
|
|
1515
|
-
parameters: z.object({
|
|
1516
|
-
path: z.string().describe("Path to the wiki page (e.g. 'pages/people/brian/index.md', 'pages/projects/chapterhouse/decisions.md', 'index.md')"),
|
|
1517
|
-
}),
|
|
1518
|
-
handler: async (args) => {
|
|
1519
|
-
ensureWikiStructure();
|
|
1520
|
-
const content = await readWikiPage(args.path);
|
|
1521
|
-
if (!content)
|
|
1522
|
-
return `Page not found: ${args.path}`;
|
|
1523
|
-
return content;
|
|
1524
|
-
},
|
|
1525
|
-
}),
|
|
1526
|
-
defineTool("wiki_update", {
|
|
1527
|
-
description: "Create or update a wiki page. You provide the full page content (markdown with optional " +
|
|
1528
|
-
"YAML frontmatter). The page will be written to disk and the index updated. Use this for " +
|
|
1529
|
-
"rich knowledge pages, entity pages, synthesis documents — anything more structured than " +
|
|
1530
|
-
"a quick 'remember' call. After creating/updating a page, the index is automatically updated. " +
|
|
1531
|
-
"PATH RULES: entity-category pages MUST be 'pages/<category>/<topic-slug>/<page>.md' where " +
|
|
1532
|
-
"category is one of projects, people, orgs, tools, topics, areas, '<page>' is 'index' for the " +
|
|
1533
|
-
"topic overview or a facet name (e.g. 'decisions', 'feature-ideas') — exactly one topic level, " +
|
|
1534
|
-
"lowercase slugs only. Flat-category pages MUST be 'pages/<category>.md' (preferences, facts, " +
|
|
1535
|
-
"routines, decisions). Bad paths are rejected with a suggested correction.",
|
|
1536
|
-
parameters: wikiUpdateArgsSchema,
|
|
1537
|
-
handler: async (args) => {
|
|
1538
|
-
try {
|
|
1539
|
-
const parsedArgs = wikiUpdateArgsSchema.parse(args);
|
|
1540
|
-
ensureWikiStructure();
|
|
1541
|
-
return await withWikiWrite(async () => {
|
|
1542
|
-
const content = validateWikiPageInput(parsedArgs.path, parsedArgs.content);
|
|
1543
|
-
const page = { ...parsedArgs, content };
|
|
1544
|
-
writeWikiPageAndRefreshIndex(page, "wiki_update");
|
|
1545
|
-
return `Wiki page updated: ${page.title} (${page.path})`;
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
catch (err) {
|
|
1549
|
-
const error = sanitizeWikiUpdateError(err);
|
|
1550
|
-
log.error({ err: err instanceof Error ? err.message : err, path: typeof args?.path === "string" ? args.path : undefined }, "wiki_update failed");
|
|
1551
|
-
return { error };
|
|
1552
|
-
}
|
|
1553
|
-
},
|
|
1554
|
-
}),
|
|
1555
|
-
defineTool("wiki_batch_update", {
|
|
1556
|
-
description: "Create or update multiple wiki pages in a single operation. " +
|
|
1557
|
-
"Each page follows the same path rules as wiki_update. " +
|
|
1558
|
-
"Up to 50 pages per call. Returns a per-page success/error summary.",
|
|
1559
|
-
parameters: wikiBatchUpdateArgsSchema,
|
|
1560
|
-
handler: async (args) => {
|
|
1561
|
-
try {
|
|
1562
|
-
const parsedArgs = wikiBatchUpdateArgsSchema.parse(args);
|
|
1563
|
-
ensureWikiStructure();
|
|
1564
|
-
return await withWikiWrite(async () => {
|
|
1565
|
-
const allowedTags = loadTaxonomy();
|
|
1566
|
-
const results = [];
|
|
1567
|
-
for (const pageArgs of parsedArgs.pages) {
|
|
1568
|
-
try {
|
|
1569
|
-
const content = validateWikiPageInput(pageArgs.path, pageArgs.content, allowedTags);
|
|
1570
|
-
writeWikiPageAndRefreshIndex({ ...pageArgs, content }, "wiki_batch_update");
|
|
1571
|
-
results.push({ path: pageArgs.path, status: "ok" });
|
|
1572
|
-
}
|
|
1573
|
-
catch (err) {
|
|
1574
|
-
results.push({
|
|
1575
|
-
path: pageArgs.path,
|
|
1576
|
-
status: "error",
|
|
1577
|
-
error: sanitizeWikiUpdateError(err),
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
const createdCount = results.filter((result) => result.status === "ok").length;
|
|
1582
|
-
const errors = results.filter((result) => result.status === "error");
|
|
1583
|
-
if (errors.length === 0) {
|
|
1584
|
-
return `Created ${createdCount} pages successfully.`;
|
|
1585
|
-
}
|
|
1586
|
-
return `Created ${createdCount} pages successfully.\nErrors (${errors.length}):\n${errors.map((result) => ` • ${result.path} — ${result.error}`).join("\n")}`;
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
catch (err) {
|
|
1590
|
-
const error = sanitizeWikiUpdateError(err);
|
|
1591
|
-
log.error({ err: err instanceof Error ? err.message : err }, "wiki_batch_update failed");
|
|
1592
|
-
return { error };
|
|
1593
|
-
}
|
|
1594
|
-
},
|
|
1595
|
-
}),
|
|
1596
|
-
defineTool("wiki_ingest", {
|
|
1597
|
-
description: "REMOVED: Use wiki_ingest_source instead.",
|
|
1598
|
-
parameters: z.object({}).passthrough(),
|
|
1599
|
-
handler: async () => "This tool has been removed. Use wiki_ingest_source instead.",
|
|
1600
|
-
}),
|
|
1601
|
-
defineTool("wiki_lint", {
|
|
1602
|
-
description: "REMOVED: Wiki health checks are no longer needed.",
|
|
1603
|
-
parameters: z.object({}).passthrough(),
|
|
1604
|
-
handler: async () => "This tool has been removed. Wiki health checks are no longer needed with SQLite-backed storage.",
|
|
1605
|
-
}),
|
|
1606
|
-
defineTool("wiki_rebuild_index", {
|
|
1607
|
-
description: "REMOVED: The wiki index is maintained automatically.",
|
|
1608
|
-
parameters: z.object({}).passthrough(),
|
|
1609
|
-
handler: async () => "This tool has been removed. The wiki index is now maintained automatically via SQLite FTS5.",
|
|
1610
|
-
}),
|
|
1611
|
-
defineTool("wiki_reindex", {
|
|
1612
|
-
description: "Force a full wiki filesystem-to-SQLite reindex pass.",
|
|
1613
|
-
parameters: z.object({}),
|
|
1614
|
-
handler: async () => {
|
|
1615
|
-
ensureWikiStructure();
|
|
1616
|
-
const result = reindexWikiPages();
|
|
1617
|
-
appendLog("update", `wiki_reindex: rebuilt ${result.indexedPageCount} page(s)`);
|
|
1618
|
-
return `Reindexed ${result.indexedPageCount} wiki page(s) from disk.`;
|
|
1619
|
-
},
|
|
1620
|
-
}),
|
|
1621
|
-
defineTool("wiki_traverse", {
|
|
1622
|
-
description: "Walk the wiki entity graph from a starting page. Returns pages connected by typed links. " +
|
|
1623
|
-
"Use to discover related knowledge, trace dependencies, find who works on a project, etc. " +
|
|
1624
|
-
"Depth 1 returns direct neighbors; depth 2-3 expands further (max 3). " +
|
|
1625
|
-
"Optionally filter by link_type: references, implements, supersedes, member_of, works_on, decided_by, depends_on, attended, follow_up.",
|
|
1626
|
-
parameters: z.object({
|
|
1627
|
-
page: z.string().describe("Wiki page path to traverse from (e.g. 'pages/topics/rust/index.md')"),
|
|
1628
|
-
link_type: z.string().optional().describe("Filter by link type (e.g. 'references', 'implements')"),
|
|
1629
|
-
depth: z.number().int().min(1).max(3).optional().describe("Traversal depth (1–3, default 1)"),
|
|
1630
|
-
}),
|
|
1631
|
-
handler: async (args) => {
|
|
1632
|
-
ensureWikiStructure();
|
|
1633
|
-
const results = wikiTraverse(args.page, args.link_type, args.depth ?? 1);
|
|
1634
|
-
if (results.length === 0)
|
|
1635
|
-
return `No linked pages found for: ${args.page}`;
|
|
1636
|
-
const lines = results.map((r) => `• [depth ${r.depth}] ${r.direction === "outbound" ? "→" : "←"} ${r.page} (${r.link_type})`);
|
|
1637
|
-
return `Found ${results.length} linked page(s):\n${lines.join("\n")}`;
|
|
1638
|
-
},
|
|
1639
|
-
}),
|
|
1640
|
-
defineTool("wiki_fix", {
|
|
1641
|
-
description: "REMOVED: Use wiki_update instead.",
|
|
1642
|
-
parameters: z.object({}).passthrough(),
|
|
1643
|
-
handler: async () => "This tool has been removed. Use wiki_update to correct wiki pages.",
|
|
1644
|
-
}),
|
|
1645
|
-
defineTool("wiki_append_timeline", {
|
|
1646
|
-
description: "Append an entry to the '## Timeline' section of a wiki page. " +
|
|
1647
|
-
"Creates the section (and the page itself) if absent. " +
|
|
1648
|
-
"Timeline is append-only — existing entries are never modified. " +
|
|
1649
|
-
"Use for recording events, source ingestion, and interaction history.",
|
|
1650
|
-
parameters: z.object({
|
|
1651
|
-
page: z.string().describe("Relative path from wiki root (e.g. 'pages/people/alice/index.md')"),
|
|
1652
|
-
entry: z.string().describe("Markdown text for the timeline entry"),
|
|
1653
|
-
source_id: z.string().optional().describe("Optional reference to a wiki_sources id"),
|
|
1654
|
-
}),
|
|
1655
|
-
handler: async (args) => {
|
|
1656
|
-
ensureWikiStructure();
|
|
1657
|
-
try {
|
|
1658
|
-
const entry = args.source_id
|
|
1659
|
-
? `${args.entry}\n\n_Source: ${args.source_id}_`
|
|
1660
|
-
: args.entry;
|
|
1661
|
-
return await withWikiWrite(async () => {
|
|
1662
|
-
appendTimeline(args.page, entry);
|
|
1663
|
-
return `Timeline entry appended to ${args.page}`;
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
catch (err) {
|
|
1667
|
-
log.error({ err: err instanceof Error ? err.message : err, page: args.page }, "wiki_append_timeline failed");
|
|
1668
|
-
return { error: err instanceof Error ? err.message : "wiki_append_timeline failed" };
|
|
1669
|
-
}
|
|
1670
|
-
},
|
|
1671
|
-
}),
|
|
1672
|
-
defineTool("wiki_ingest_source", {
|
|
1673
|
-
description: "Ingest an external source (URL, PDF, Git repo, or raw text) into the PKB. " +
|
|
1674
|
-
"Fetches and parses the content, extracts entities with LLM, creates/updates wiki pages, " +
|
|
1675
|
-
"and writes timeline entries. Idempotent: re-ingesting the same source returns the existing result. " +
|
|
1676
|
-
"Type is auto-detected if omitted.",
|
|
1677
|
-
parameters: z.object({
|
|
1678
|
-
source: z.string().describe("URL, file path, git repo URL, or raw text content to ingest"),
|
|
1679
|
-
type: z.enum(["url", "pdf", "repo", "text"]).optional().describe("Source type. Auto-detected if omitted: http(s) URL → url/repo, .pdf path → pdf, else text"),
|
|
1680
|
-
topic: z.string().optional().describe("Optional hint for entity extraction focus"),
|
|
1681
|
-
session_id: z.string().optional().describe("Optional research session id to persist in wiki_sources"),
|
|
1682
|
-
session_name: z.string().optional().describe("Optional human-readable research session name"),
|
|
1683
|
-
}),
|
|
1684
|
-
handler: async (args) => {
|
|
1685
|
-
ensureWikiStructure();
|
|
1686
|
-
try {
|
|
1687
|
-
if (looksLikeLocalFilePath(args.source)) {
|
|
1688
|
-
return {
|
|
1689
|
-
error: "wiki_ingest_source does not support local file paths. Provide a URL, git repo URL, or raw text content.",
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
const sourceType = args.type ?? detectSourceType(args.source);
|
|
1693
|
-
const result = await ingestSource(args.source, sourceType, args.topic, {
|
|
1694
|
-
sessionId: args.session_id,
|
|
1695
|
-
sessionName: args.session_name,
|
|
1696
|
-
});
|
|
1697
|
-
if (result.already_existed) {
|
|
1698
|
-
return (`Source already ingested (id: ${result.source_id}).\n` +
|
|
1699
|
-
`Pages previously updated: ${result.pages_updated.length > 0 ? result.pages_updated.join(", ") : "none"}`);
|
|
1700
|
-
}
|
|
1701
|
-
const lines = [
|
|
1702
|
-
`✅ Ingested source (id: ${result.source_id})`,
|
|
1703
|
-
];
|
|
1704
|
-
if (result.pages_created.length > 0) {
|
|
1705
|
-
lines.push(`📄 Pages created (${result.pages_created.length}): ${result.pages_created.join(", ")}`);
|
|
1706
|
-
}
|
|
1707
|
-
if (result.pages_updated.length > 0) {
|
|
1708
|
-
lines.push(`✏️ Pages updated (${result.pages_updated.length}): ${result.pages_updated.join(", ")}`);
|
|
1709
|
-
}
|
|
1710
|
-
if (result.entities.length > 0) {
|
|
1711
|
-
lines.push(`🔍 Entities extracted (${result.entities.length}):`);
|
|
1712
|
-
for (const e of result.entities) {
|
|
1713
|
-
lines.push(` • ${e.name} (${e.type}) → ${e.path}`);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
else {
|
|
1717
|
-
lines.push("🔍 No entities extracted.");
|
|
1718
|
-
}
|
|
1719
|
-
return lines.join("\n");
|
|
1720
|
-
}
|
|
1721
|
-
catch (err) {
|
|
1722
|
-
log.error({ err: err instanceof Error ? err.message : err }, "wiki_ingest_source failed");
|
|
1723
|
-
return { error: err instanceof Error ? err.message : "Ingestion failed" };
|
|
1724
|
-
}
|
|
1725
|
-
},
|
|
1726
|
-
}),
|
|
1727
|
-
defineTool("restart_chapterhouse", {
|
|
1728
|
-
description: "Restart the Chapterhouse daemon process. Use when the user asks Chapterhouse to restart himself, " +
|
|
1729
|
-
"or when a restart is needed to pick up configuration changes. " +
|
|
1730
|
-
"Spawns a new process and exits the current one.",
|
|
1731
|
-
parameters: z.object({
|
|
1732
|
-
reason: z.string().optional().describe("Optional reason for the restart"),
|
|
1733
|
-
}),
|
|
1734
|
-
handler: async (args) => {
|
|
1735
|
-
const reason = args.reason ? ` (${args.reason})` : "";
|
|
1736
|
-
// Dynamic import to avoid circular dependency
|
|
1737
|
-
const { restartDaemon } = await import("../daemon.js");
|
|
1738
|
-
// Schedule restart after returning the response
|
|
1739
|
-
setTimeout(() => {
|
|
1740
|
-
restartDaemon().catch((err) => {
|
|
1741
|
-
log.error({ err: err instanceof Error ? err.message : err }, "Restart failed");
|
|
1742
|
-
});
|
|
1743
|
-
}, 1000);
|
|
1744
|
-
return `Restarting Chapterhouse${reason}. I'll be back in a few seconds.`;
|
|
1745
|
-
},
|
|
1746
|
-
}),
|
|
1747
|
-
];
|
|
1748
|
-
}
|
|
1749
|
-
function formatAge(date) {
|
|
1750
|
-
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
1751
|
-
if (seconds < 60)
|
|
1752
|
-
return "just now";
|
|
1753
|
-
if (seconds < 3600)
|
|
1754
|
-
return `${Math.floor(seconds / 60)}m ago`;
|
|
1755
|
-
if (seconds < 86400)
|
|
1756
|
-
return `${Math.floor(seconds / 3600)}h ago`;
|
|
1757
|
-
return `${Math.floor(seconds / 86400)}d ago`;
|
|
1758
|
-
}
|
|
1759
|
-
function parseSimpleYaml(content) {
|
|
1760
|
-
const result = {};
|
|
1761
|
-
for (const line of content.split("\n")) {
|
|
1762
|
-
const idx = line.indexOf(": ");
|
|
1763
|
-
if (idx > 0) {
|
|
1764
|
-
const key = line.slice(0, idx).trim();
|
|
1765
|
-
const value = line.slice(idx + 2).trim();
|
|
1766
|
-
result[key] = value;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
return result;
|
|
1770
|
-
}
|
|
1771
|
-
function isOwnedByCurrentUser(owner, user) {
|
|
1772
|
-
const normalizedOwner = normalizeIdentity(owner);
|
|
1773
|
-
return [user.id, user.name, user.email]
|
|
1774
|
-
.map((value) => normalizeIdentity(value))
|
|
1775
|
-
.filter(Boolean)
|
|
1776
|
-
.includes(normalizedOwner);
|
|
1777
|
-
}
|
|
1778
|
-
function normalizeIdentity(value) {
|
|
1779
|
-
return (value ?? "").trim().toLowerCase();
|
|
1780
|
-
}
|
|
1
|
+
export * from "./tools/index.js";
|
|
1781
2
|
//# sourceMappingURL=tools.js.map
|