mindgraph-mcp 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -35,14 +35,14 @@ function errorFields(body) {
35
35
  ...typeof fields.retriable === "boolean" ? { retriable: fields.retriable } : {}
36
36
  };
37
37
  }
38
- function canRetryRequest(method, path9, body) {
38
+ function canRetryRequest(method, path10, body) {
39
39
  if (method === "GET" || method === "HEAD") return true;
40
40
  if (method !== "POST") return false;
41
- if (READ_POSTS.has(path9)) return true;
41
+ if (READ_POSTS.has(path10)) return true;
42
42
  const fields = body && typeof body === "object" && !Array.isArray(body) ? body : {};
43
43
  if (typeof fields.action !== "string") return false;
44
- if (Object.prototype.hasOwnProperty.call(READ_ACTIONS, path9) && READ_ACTIONS[path9].includes(fields.action)) return true;
45
- return path9 === "/agent/plan" && IDEMPOTENT_WORK.has(fields.action) && typeof fields.idempotency_key === "string" && fields.idempotency_key.trim().length > 0;
44
+ if (Object.prototype.hasOwnProperty.call(READ_ACTIONS, path10) && READ_ACTIONS[path10].includes(fields.action)) return true;
45
+ return path10 === "/agent/plan" && IDEMPOTENT_WORK.has(fields.action) && typeof fields.idempotency_key === "string" && fields.idempotency_key.trim().length > 0;
46
46
  }
47
47
  function permitsRetry(fields) {
48
48
  return fields.retriable !== false && ![
@@ -179,8 +179,8 @@ var init_dist = __esm({
179
179
  }
180
180
  }
181
181
  // ---- HTTP helpers ----
182
- async request(method, path9, body) {
183
- const url = `${this.baseUrl}${path9}`;
182
+ async request(method, path10, body) {
183
+ const url = `${this.baseUrl}${path10}`;
184
184
  const requestId = createRequestId();
185
185
  const init = {
186
186
  method,
@@ -195,12 +195,12 @@ var init_dist = __esm({
195
195
  }
196
196
  const retrySafe = canRetryRequest(
197
197
  method,
198
- path9,
198
+ path10,
199
199
  typeof init.body === "string" ? JSON.parse(init.body) : void 0
200
200
  );
201
201
  let lastError;
202
202
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
203
- const res = await this.fetchWithDeadline(url, init, method, path9);
203
+ const res = await this.fetchWithDeadline(url, init, method, path10);
204
204
  if (!res.ok) {
205
205
  const text22 = await res.text().catch(() => "");
206
206
  let parsed;
@@ -210,7 +210,7 @@ var init_dist = __esm({
210
210
  parsed = text22;
211
211
  }
212
212
  const err6 = new MindGraphError(
213
- `${method} ${path9} failed: ${res.status}`,
213
+ `${method} ${path10} failed: ${res.status}`,
214
214
  res.status,
215
215
  parsed
216
216
  );
@@ -234,7 +234,7 @@ var init_dist = __esm({
234
234
  * "timeout"`, `status: 0`). Without it the request is sent unchanged — no
235
235
  * AbortSignal is attached, preserving the pre-0.16 behaviour.
236
236
  */
237
- async fetchWithDeadline(url, init, method, path9) {
237
+ async fetchWithDeadline(url, init, method, path10) {
238
238
  if (this.timeoutMs === void 0) {
239
239
  return fetch(url, init);
240
240
  }
@@ -245,7 +245,7 @@ var init_dist = __esm({
245
245
  } catch (error2) {
246
246
  if (controller.signal.aborted) {
247
247
  throw new MindGraphError(
248
- `${method} ${path9} timed out after ${this.timeoutMs}ms`,
248
+ `${method} ${path10} timed out after ${this.timeoutMs}ms`,
249
249
  0,
250
250
  { error: "timeout", code: "timeout", retriable: false }
251
251
  );
@@ -255,17 +255,17 @@ var init_dist = __esm({
255
255
  clearTimeout(timer);
256
256
  }
257
257
  }
258
- get(path9) {
259
- return this.request("GET", path9);
258
+ get(path10) {
259
+ return this.request("GET", path10);
260
260
  }
261
- post(path9, body) {
262
- return this.request("POST", path9, body);
261
+ post(path10, body) {
262
+ return this.request("POST", path10, body);
263
263
  }
264
- patch(path9, body) {
265
- return this.request("PATCH", path9, body);
264
+ patch(path10, body) {
265
+ return this.request("PATCH", path10, body);
266
266
  }
267
- del(path9) {
268
- return this.request("DELETE", path9);
267
+ del(path10) {
268
+ return this.request("DELETE", path10);
269
269
  }
270
270
  // ---- Health ----
271
271
  async health() {
@@ -1240,14 +1240,14 @@ function unavailabilityCaveats(reason, message) {
1240
1240
  }
1241
1241
  return caveats;
1242
1242
  }
1243
- function normalizePath(path9) {
1244
- return path9.replaceAll("\\", "/").replace(/^\.\//, "");
1243
+ function normalizePath(path10) {
1244
+ return path10.replaceAll("\\", "/").replace(/^\.\//, "");
1245
1245
  }
1246
1246
  function normalizeSignature(signature) {
1247
1247
  return signature.replace(/\s+/g, " ").trim();
1248
1248
  }
1249
- function languageFromPath(path9) {
1250
- const extension = (0, import_node_path2.extname)(path9).toLowerCase();
1249
+ function languageFromPath(path10) {
1250
+ const extension = (0, import_node_path2.extname)(path10).toLowerCase();
1251
1251
  return {
1252
1252
  ".c": "c",
1253
1253
  ".cc": "cpp",
@@ -1313,6 +1313,15 @@ function nearestExistingDirectory(candidate) {
1313
1313
  current = parent;
1314
1314
  }
1315
1315
  }
1316
+ function findChildWorkspaceFiles(start) {
1317
+ let entries;
1318
+ try {
1319
+ entries = (0, import_node_fs2.readdirSync)(start, { withFileTypes: true });
1320
+ } catch {
1321
+ return [];
1322
+ }
1323
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).slice(0, MAX_CHILD_DIRECTORIES).map((entry) => (0, import_node_path2.join)(start, entry.name, ".mindgraph", "workspace.json")).filter((candidate) => (0, import_node_fs2.existsSync)(candidate)).sort();
1324
+ }
1316
1325
  function findWorkspaceFile(start, explicit) {
1317
1326
  if (explicit) return (0, import_node_path2.resolve)(start, explicit);
1318
1327
  let current = (0, import_node_path2.resolve)(start);
@@ -1408,7 +1417,7 @@ function codeRefIdentityKey(ref) {
1408
1417
  }
1409
1418
  };
1410
1419
  }
1411
- var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto3, UnknownRepositoryError, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, CodegraphAdapter;
1420
+ var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto3, UnknownRepositoryError, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, MAX_CHILD_DIRECTORIES, CodegraphAdapter;
1412
1421
  var init_codegraph = __esm({
1413
1422
  "src/codegraph.ts"() {
1414
1423
  "use strict";
@@ -1431,12 +1440,14 @@ var init_codegraph = __esm({
1431
1440
  DEFAULT_TIMEOUT_MS = 5e3;
1432
1441
  DEFAULT_STATUS_TTL_MS = 6e4;
1433
1442
  MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
1443
+ MAX_CHILD_DIRECTORIES = 200;
1434
1444
  CodegraphAdapter = class {
1435
1445
  timeoutMs;
1436
1446
  statusTtlMs;
1437
1447
  env;
1438
1448
  cwd;
1439
1449
  workspaceFile;
1450
+ childWorkspaceFiles;
1440
1451
  now;
1441
1452
  identityCache = /* @__PURE__ */ new Map();
1442
1453
  statusCache = /* @__PURE__ */ new Map();
@@ -1449,6 +1460,7 @@ var init_codegraph = __esm({
1449
1460
  this.cwd,
1450
1461
  options.workspaceFile ?? this.env.MINDGRAPH_WORKSPACE_FILE
1451
1462
  );
1463
+ this.childWorkspaceFiles = !this.workspaceFile && options.childWorkspaces ? findChildWorkspaceFiles(this.cwd) : [];
1452
1464
  this.now = options.now ?? Date.now;
1453
1465
  }
1454
1466
  async resolveRepository(repo, fallbackSpaceUid) {
@@ -1772,13 +1784,15 @@ var init_codegraph = __esm({
1772
1784
  }
1773
1785
  configuredRepositories() {
1774
1786
  const configured = [];
1775
- if (this.workspaceFile && (0, import_node_fs2.existsSync)(this.workspaceFile)) {
1787
+ const workspaceFiles = this.workspaceFile ? [this.workspaceFile] : this.childWorkspaceFiles;
1788
+ for (const workspaceFile of workspaceFiles) {
1789
+ if (!(0, import_node_fs2.existsSync)(workspaceFile)) continue;
1776
1790
  try {
1777
1791
  const parsed = JSON.parse(
1778
- (0, import_node_fs2.readFileSync)(this.workspaceFile, "utf8")
1792
+ (0, import_node_fs2.readFileSync)(workspaceFile, "utf8")
1779
1793
  );
1780
1794
  if (parsed.v === 1 && Array.isArray(parsed.repositories)) {
1781
- const configDirectory = (0, import_node_path2.dirname)(this.workspaceFile);
1795
+ const configDirectory = (0, import_node_path2.dirname)(workspaceFile);
1782
1796
  const base = (0, import_node_path2.basename)(configDirectory) === ".mindgraph" ? (0, import_node_path2.dirname)(configDirectory) : configDirectory;
1783
1797
  for (const repository of parsed.repositories) {
1784
1798
  if (!repository || typeof repository.root !== "string") continue;
@@ -1959,1735 +1973,1926 @@ var init_codegraph = __esm({
1959
1973
  }
1960
1974
  });
1961
1975
 
1962
- // src/error-detail.ts
1963
- function errorDetail(e) {
1964
- if (e instanceof MindGraphError && e.body !== void 0) {
1965
- const detail = typeof e.body === "string" ? e.body : JSON.stringify(e.body);
1966
- return `${e.message} \u2014 ${detail}`;
1976
+ // src/render.ts
1977
+ function obj(value) {
1978
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1979
+ }
1980
+ function str(value) {
1981
+ return typeof value === "string" && value ? value : void 0;
1982
+ }
1983
+ function num(value) {
1984
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1985
+ }
1986
+ function edgeTypeName(value) {
1987
+ return str(value) ?? str(obj(value)?.Custom);
1988
+ }
1989
+ function renderBudget(requested) {
1990
+ if (requested === void 0 || !Number.isFinite(requested)) return RENDER_CHAR_BUDGET;
1991
+ return Math.min(
1992
+ RENDER_CHAR_BUDGET,
1993
+ Math.max(MIN_RENDER_CHAR_BUDGET, Math.floor(requested))
1994
+ );
1995
+ }
1996
+ function clip(value, max, state) {
1997
+ const text2 = str(value);
1998
+ if (!text2) return void 0;
1999
+ if (text2.length <= max) return text2;
2000
+ state.bounded = true;
2001
+ return `${text2.slice(0, max - 1)}\u2026`;
2002
+ }
2003
+ function isNodeShaped(value) {
2004
+ const node = obj(value);
2005
+ return Boolean(
2006
+ node && str(node.uid) && (str(node.label) || str(node.node_type) || obj(node.node_type))
2007
+ );
2008
+ }
2009
+ function nodeDisplayType(node) {
2010
+ const props = obj(node.props);
2011
+ const plain = str(node.node_type);
2012
+ return (plain && plain !== "Custom" ? plain : void 0) ?? str(obj(node.node_type)?.Custom) ?? str(props?.object_type) ?? str(props?.type_name) ?? str(obj(props?.data)?.domain_type) ?? plain ?? "Node";
2013
+ }
2014
+ function nodeFields(node) {
2015
+ const props = obj(node.props);
2016
+ return obj(props?.fields) ?? obj(obj(props?.data)?.fields);
2017
+ }
2018
+ function unwrapSearchResult(value) {
2019
+ if (isNodeShaped(value)) return value;
2020
+ const wrapper = obj(value);
2021
+ const inner = obj(wrapper?.node) ?? obj(wrapper?.object);
2022
+ if (inner && isNodeShaped(inner)) {
2023
+ const score = num(wrapper.score);
2024
+ return score !== void 0 ? { ...inner, retrieval_score: score } : inner;
1967
2025
  }
1968
- return e instanceof Error ? e.message : String(e);
2026
+ return void 0;
1969
2027
  }
1970
- function conflictState(e) {
1971
- if (!(e instanceof MindGraphError) || !e.body || typeof e.body !== "object") {
1972
- return {};
2028
+ function supersededBy(node) {
2029
+ const marker = node.superseded;
2030
+ if (!marker) return void 0;
2031
+ const head = obj(marker);
2032
+ if (!head) return str(node.superseded_by) ?? "";
2033
+ const label = (str(head.head_label) ?? str(head.head_uid) ?? "").replace(/\s+/g, " ").trim();
2034
+ const short = label.length <= 80 ? label : `${label.slice(0, 79)}\u2026`;
2035
+ const date = str(head.head_stated_at);
2036
+ return short && date ? `${short} (${date.slice(0, 10)})` : short;
2037
+ }
2038
+ function statusTag(node) {
2039
+ const tags = [];
2040
+ const replacement = supersededBy(node);
2041
+ if (replacement !== void 0) {
2042
+ tags.push(replacement ? `SUPERSEDED by ${replacement}` : "SUPERSEDED");
1973
2043
  }
1974
- const body = e.body;
1975
- const lifted = {};
1976
- for (const key of CONFLICT_STATE_KEYS) {
1977
- if (body[key] !== void 0) lifted[key] = body[key];
2044
+ const truth = str(node.truth_status) ?? str(obj(node.props)?.truth_status);
2045
+ if (truth && truth !== "accepted") tags.push(`truth: ${truth}`);
2046
+ if (node.currently_valid === false) tags.push("OUTSIDE ITS VALIDITY WINDOW");
2047
+ if (node.valid_at_time === false) tags.push("NOT VALID AT THE REQUESTED TIME");
2048
+ return tags.length > 0 ? ` [${tags.join("; ")}]` : "";
2049
+ }
2050
+ function firstQuote(node, state) {
2051
+ const chunks = Array.isArray(node.source_chunks) ? node.source_chunks : [];
2052
+ for (const raw of chunks) {
2053
+ const chunk = obj(raw);
2054
+ const quote = str(chunk?.quote) ?? str(obj(chunk?.anchor)?.exact);
2055
+ if (quote) return clip(quote, QUOTE_MAX, state);
1978
2056
  }
1979
- return lifted;
2057
+ return void 0;
1980
2058
  }
1981
- function toolError(e, fallbackCode) {
1982
- const body = e instanceof MindGraphError && e.body && typeof e.body === "object" && !Array.isArray(e.body) ? e.body : {};
1983
- const code = typeof body.code === "string" ? body.code : fallbackCode;
1984
- const payload = {
1985
- error: errorDetail(e),
1986
- ...conflictState(e),
1987
- ...e instanceof MindGraphError ? { status: e.status } : {},
1988
- ...code !== void 0 ? { code } : {},
1989
- ...typeof body.retriable === "boolean" ? { retriable: body.retriable } : {}
1990
- };
1991
- return {
1992
- content: [{ type: "text", text: JSON.stringify(payload) }],
1993
- structuredContent: payload,
1994
- isError: true
1995
- };
2059
+ function sourceTag(node) {
2060
+ const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
2061
+ const first = obj(docs[0]);
2062
+ const title = str(first?.title);
2063
+ if (!title) return void 0;
2064
+ const extra = docs.length > 1 ? ` (+${docs.length - 1} more)` : "";
2065
+ return `from: ${title}${extra}`;
1996
2066
  }
1997
- var CONFLICT_STATE_KEYS;
1998
- var init_error_detail = __esm({
1999
- "src/error-detail.ts"() {
2000
- "use strict";
2001
- init_dist();
2002
- CONFLICT_STATE_KEYS = [
2003
- "current_version",
2004
- "current_epoch",
2005
- "lease_expires_at",
2006
- "lease_owner_agent_id"
2007
- ];
2008
- }
2009
- });
2010
-
2011
- // src/code-tool.ts
2012
- async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cwd: invocationCwd(args) })) {
2013
- const codeClient = client2;
2014
- const action = args.action;
2015
- const refs = Array.isArray(args.code_refs) ? args.code_refs : [];
2016
- const anchorUids = Array.isArray(args.anchor_uids) ? args.anchor_uids : [];
2017
- const agentId = typeof args.agent_id === "string" ? args.agent_id : "mcp";
2018
- const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : void 0;
2019
- const options = {
2020
- repo: typeof args.repo === "string" ? args.repo : void 0,
2021
- fallbackSpaceUid
2022
- };
2023
- const limit = typeof args.limit === "number" ? args.limit : void 0;
2024
- try {
2025
- switch (action) {
2026
- case "anchor": {
2027
- if (refs.length === 0) return err("missing_code_refs", "code_refs is required for anchor");
2028
- const resolution = await adapter.resolveRefs(refs, options);
2029
- const resolved = resolution.results.flatMap(
2030
- (result) => result.resolved && result.repository ? [{ ref: result.resolved, repository: result.repository }] : []
2031
- );
2032
- const unresolved = resolution.results.filter((result) => !result.resolved);
2033
- if (resolved.length === 0) {
2034
- return ok({ ...resolution, anchors: [], wrote: false });
2035
- }
2036
- const missingSpace = resolved.find(
2037
- ({ repository }) => typeof args.repo_space_uid !== "string" && !repository.spaceUid
2038
- );
2039
- if (missingSpace) {
2040
- return err(
2041
- "missing_repository_space",
2042
- `repo_space_uid, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID is required for ${missingSpace.repository.repoId}`
2043
- );
2044
- }
2045
- const repositoryAnchors = /* @__PURE__ */ new Map();
2046
- for (const { repository } of resolved) {
2047
- if (repositoryAnchors.has(repository.repoRoot)) continue;
2048
- const spaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : repository.spaceUid;
2049
- const anchored = await anchorRepository(
2050
- codeClient,
2051
- repository,
2052
- spaceUid,
2053
- agentId
2054
- );
2055
- repositoryAnchors.set(repository.repoRoot, {
2056
- ...anchored,
2057
- repository
2058
- });
2059
- }
2060
- const anchors = [];
2061
- for (const { ref, repository } of resolved) {
2062
- const repositoryAnchor = repositoryAnchors.get(repository.repoRoot);
2063
- if (!repositoryAnchor.uid) continue;
2064
- const spaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : repository.spaceUid;
2065
- const anchor = await anchorCodeRef(
2066
- codeClient,
2067
- ref,
2068
- repositoryAnchor.uid,
2069
- spaceUid,
2070
- agentId
2071
- );
2072
- anchors.push(anchor);
2073
- }
2074
- return ok({
2075
- availability: resolution.availability,
2076
- repository: repositoryAnchors.values().next().value,
2077
- repositories: [...repositoryAnchors.values()],
2078
- anchors,
2079
- unresolved,
2080
- wrote: anchors.some((anchor) => anchor.created)
2081
- });
2082
- }
2083
- case "recall": {
2084
- const materialized = new Set(anchorUids);
2085
- let resolution;
2086
- if (refs.length > 0) {
2087
- resolution = await adapter.resolveRefs(refs, options);
2088
- for (const result of resolution.results) {
2089
- if (!result.resolved) continue;
2090
- const uid = await resolveMaterializedUid(codeClient, result.resolved, agentId);
2091
- if (uid) materialized.add(uid);
2092
- }
2093
- }
2094
- if (materialized.size === 0) {
2095
- return ok({
2096
- availability: resolution?.availability,
2097
- anchors: [],
2098
- context: [],
2099
- unresolved: resolution?.results.filter((result) => !result.resolved) ?? []
2100
- });
2101
- }
2102
- const context = await Promise.all(
2103
- [...materialized].map(async (uid) => ({
2104
- anchor_uid: uid,
2105
- graph: await codeClient.traverse({
2106
- action: "neighborhood",
2107
- start_uid: uid,
2108
- depth: 2,
2109
- max_nodes: Math.min(limit ?? 20, 20)
2110
- })
2111
- }))
2112
- );
2113
- return ok({
2114
- availability: resolution?.availability,
2115
- anchors: [...materialized],
2116
- context,
2117
- unresolved: resolution?.results.filter((result) => !result.resolved) ?? []
2118
- });
2119
- }
2120
- case "expand": {
2121
- const target = await resolveSingleTarget(
2122
- codeClient,
2123
- adapter,
2124
- refs,
2125
- anchorUids,
2126
- options
2127
- );
2128
- if ("error" in target) return target.error;
2129
- const expanded = await adapter.expand(target.ref, limit);
2130
- const joined = await joinMaterialized(
2131
- codeClient,
2132
- [...expanded.callers, ...expanded.callees],
2133
- agentId,
2134
- Math.min(limit ?? 20, 20)
2135
- );
2136
- return ok({ anchor: target.ref, ...expanded, materialized: joined });
2137
- }
2138
- case "affected": {
2139
- const targetUid = typeof args.task_uid === "string" ? args.task_uid : typeof args.execution_uid === "string" ? args.execution_uid : void 0;
2140
- let graphContext;
2141
- const graphRefs = [];
2142
- let unavailableRepositories = [];
2143
- if (targetUid) {
2144
- graphContext = await codeClient.traverse({
2145
- action: "neighborhood",
2146
- start_uid: targetUid,
2147
- depth: 2,
2148
- max_nodes: 50
2149
- });
2150
- const fromGraph = await codeRefsFromGraphContext(
2151
- codeClient,
2152
- adapter,
2153
- graphContext
2154
- );
2155
- graphRefs.push(...fromGraph.refs);
2156
- unavailableRepositories = fromGraph.unavailable;
2157
- }
2158
- if (refs.length > 0 || anchorUids.length > 0) {
2159
- const target = await resolveSingleTarget(
2160
- codeClient,
2161
- adapter,
2162
- refs,
2163
- anchorUids,
2164
- options
2165
- );
2166
- if ("error" in target) return target.error;
2167
- graphRefs.unshift(target.ref);
2168
- }
2169
- const unique = uniqueRefs(graphRefs);
2170
- if (unique.length === 0) {
2171
- return ok({
2172
- target_uid: targetUid,
2173
- graph_context: graphContext,
2174
- affected: [],
2175
- degradation: unavailableRepositories.length > 0 ? `attached code anchors reference repositories unavailable here: ${unavailableRepositories.join(", ")}` : "no materialized code anchors were attached",
2176
- ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
2177
- });
2178
- }
2179
- const impacts = await Promise.all(
2180
- unique.map(async (ref) => ({ anchor: ref, ...await adapter.affected(ref, limit) }))
2181
- );
2182
- const structural = uniqueRefs(impacts.flatMap((impact) => impact.affected));
2183
- const joined = await joinMaterialized(
2184
- codeClient,
2185
- structural,
2186
- agentId,
2187
- Math.min(limit ?? 50, 50)
2188
- );
2189
- return ok({
2190
- target_uid: targetUid,
2191
- graph_context: graphContext,
2192
- impacts,
2193
- materialized: joined,
2194
- ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
2195
- });
2196
- }
2197
- default:
2198
- return err("unknown_action", `unknown mindgraph_code action: ${action}`);
2199
- }
2200
- } catch (cause) {
2201
- if (cause instanceof UnknownRepositoryError) {
2202
- return err("unknown_repository", cause.message);
2203
- }
2204
- return toolError(cause, "code_tool_failed");
2067
+ function layer7Fields(node, state) {
2068
+ const fields = nodeFields(node);
2069
+ if (!fields) return void 0;
2070
+ const pairs = Object.entries(fields).filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
2071
+ if (pairs.length === 0) return void 0;
2072
+ return clip(pairs.join("; "), SUMMARY_MAX, state);
2073
+ }
2074
+ function nodeSummaryText(node, state) {
2075
+ const props = obj(node.props);
2076
+ return clip(node.summary, SUMMARY_MAX, state) ?? clip(props?.content, SUMMARY_MAX, state) ?? clip(props?.description, SUMMARY_MAX, state) ?? clip(props?.definition, SUMMARY_MAX, state);
2077
+ }
2078
+ function nodeLines(node, state) {
2079
+ const label = clip(node.label, LABEL_MAX, state) || "(unlabeled)";
2080
+ const type = nodeDisplayType(node);
2081
+ const confidence = num(node.confidence);
2082
+ const score = num(node.retrieval_score);
2083
+ const parenthetical = [
2084
+ type,
2085
+ confidence !== void 0 ? `confidence ${confidence}` : void 0,
2086
+ score !== void 0 ? `score ${Math.round(score * 100) / 100}` : void 0
2087
+ ].filter(Boolean).join(", ");
2088
+ const head = `- **${label}** [${str(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
2089
+ const summary = nodeSummaryText(node, state);
2090
+ const lines = [summary && summary !== label ? `${head}: ${summary}` : head];
2091
+ const fields = layer7Fields(node, state);
2092
+ if (fields) lines.push(` - fields: ${fields}`);
2093
+ const quote = firstQuote(node, state);
2094
+ if (quote) lines.push(` - source quote: "${quote}"`);
2095
+ const source = sourceTag(node);
2096
+ if (source) lines.push(` - ${source}`);
2097
+ const believers = Array.isArray(node.believed_by) ? node.believed_by : [];
2098
+ if (believers.length > 0) {
2099
+ const rendered2 = believers.slice(0, 3).map((raw) => {
2100
+ const believer = obj(raw);
2101
+ const conf = num(believer?.confidence);
2102
+ return `${str(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
2103
+ }).join(", ");
2104
+ lines.push(` - believed by: ${rendered2}`);
2205
2105
  }
2106
+ return lines.join("\n");
2206
2107
  }
2207
- async function attachCodeRefsToToolResult(client2, result, args, adapter) {
2208
- if (result.isError || !Array.isArray(args.code_refs) || args.code_refs.length === 0) {
2209
- return result;
2108
+ function groupedNodeSections(nodes, state, headingPrefix = "") {
2109
+ const groups = /* @__PURE__ */ new Map();
2110
+ for (const node of nodes) {
2111
+ const type = nodeDisplayType(node);
2112
+ const group = groups.get(type) ?? [];
2113
+ group.push(node);
2114
+ groups.set(type, group);
2210
2115
  }
2211
- const original = JSON.parse(result.content[0]?.text ?? "null");
2212
- const targetUids = primaryUids(original);
2213
- if (targetUids.length === 0) {
2214
- return ok({
2215
- result: original,
2216
- code_anchors: {
2217
- wrote: false,
2218
- caveat: "the write response exposed no primary UID to attach"
2219
- }
2116
+ const sections = [];
2117
+ let first = true;
2118
+ for (const [type, group] of groups) {
2119
+ sections.push({
2120
+ heading: `${first ? headingPrefix : ""}### ${type}`,
2121
+ items: group.map((node) => nodeLines(node, state))
2220
2122
  });
2123
+ first = false;
2221
2124
  }
2222
- const anchorResult = await handleCodeTool(
2223
- client2,
2224
- {
2225
- action: "anchor",
2226
- code_refs: args.code_refs,
2227
- repo: args.repo,
2228
- repo_space_uid: args.repo_space_uid,
2229
- agent_id: args.agent_id,
2230
- // Keeps the default adapter anchored to the live session directory
2231
- // rather than wherever the MCP server process was launched.
2232
- invocation_context: args.invocation_context
2233
- },
2234
- adapter
2235
- );
2236
- const anchors = JSON.parse(anchorResult.content[0]?.text ?? "null");
2237
- if (!anchorResult.isError) {
2238
- const codeClient = client2;
2239
- const anchorUids = (anchors.anchors ?? []).flatMap(
2240
- (anchor) => anchor.uid ? [anchor.uid] : []
2241
- );
2242
- const relateFailures = [];
2243
- for (const targetUid of targetUids) {
2244
- for (const anchorUid of anchorUids) {
2245
- try {
2246
- await codeClient.entity({
2247
- action: "relate",
2248
- source_uid: targetUid,
2249
- target_uid: anchorUid,
2250
- edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
2251
- agent_id: args.agent_id
2252
- });
2253
- } catch (cause) {
2254
- relateFailures.push(
2255
- `${targetUid} -> ${anchorUid}: ${errorDetail(cause)}`
2256
- );
2257
- }
2125
+ return sections;
2126
+ }
2127
+ function relationshipItems(edges, labelByUid, state, options) {
2128
+ const lines = [];
2129
+ for (const raw of edges) {
2130
+ const edge = obj(raw);
2131
+ if (!edge) continue;
2132
+ const type = edgeTypeName(edge.edge_type) || "?";
2133
+ const from = str(edge.from_uid);
2134
+ const to = str(edge.to_uid);
2135
+ if (CURATION_EDGE_TYPES.has(type)) continue;
2136
+ if (!from || !to || from === to) continue;
2137
+ const fromLabel = labelByUid.get(from) ?? (options?.fallbackToUid ? `[${from}]` : void 0);
2138
+ const toLabel = labelByUid.get(to) ?? (options?.fallbackToUid ? `[${to}]` : void 0);
2139
+ if (!fromLabel || !toLabel) continue;
2140
+ lines.push(`- ${fromLabel} \u2014${type}\u2192 ${toLabel}`);
2141
+ }
2142
+ if (lines.length > RELATIONSHIP_MAX) {
2143
+ state.bounded = true;
2144
+ const extra = lines.length - RELATIONSHIP_MAX;
2145
+ state.omitted += extra;
2146
+ return [...lines.slice(0, RELATIONSHIP_MAX), `- (+${extra} more relationships)`];
2147
+ }
2148
+ return lines;
2149
+ }
2150
+ function assemble(header, sections, state, maxChars) {
2151
+ const budget = renderBudget(maxChars);
2152
+ const packingBudget = Math.max(header.length, budget - BOUNDED_MARKER_RESERVE);
2153
+ const lines = [header];
2154
+ let used = header.length;
2155
+ for (const section of sections) {
2156
+ if (section.items.length === 0 && !section.heading) continue;
2157
+ const headingCost = section.heading.length + 2;
2158
+ if (used + headingCost > packingBudget) {
2159
+ state.bounded = true;
2160
+ state.omitted += Math.max(1, section.items.length);
2161
+ continue;
2162
+ }
2163
+ let sectionUsed = 0;
2164
+ const kept = [];
2165
+ let dropped = 0;
2166
+ for (const item of section.items) {
2167
+ if (used + headingCost + sectionUsed + item.length + 1 > packingBudget) {
2168
+ dropped += 1;
2169
+ continue;
2258
2170
  }
2171
+ kept.push(item);
2172
+ sectionUsed += item.length + 1;
2259
2173
  }
2260
- if (relateFailures.length > 0) {
2261
- return ok({
2262
- result: original,
2263
- code_anchors: {
2264
- ...anchors,
2265
- caveat: `anchors written, but ${relateFailures.length} relate edge(s) failed: ${relateFailures.join("; ")}`
2266
- }
2267
- });
2174
+ if (kept.length === 0 && section.items.length > 0) {
2175
+ state.bounded = true;
2176
+ state.omitted += dropped;
2177
+ continue;
2178
+ }
2179
+ if (dropped > 0) {
2180
+ state.bounded = true;
2181
+ state.omitted += dropped;
2182
+ const marker = `(+${dropped} more not shown)`;
2183
+ if (used + headingCost + sectionUsed + marker.length + 1 <= packingBudget) {
2184
+ kept.push(marker);
2185
+ sectionUsed += marker.length + 1;
2186
+ }
2268
2187
  }
2188
+ lines.push(
2189
+ section.items.length === 0 ? section.heading : section.heading ? `${section.heading}
2190
+ ${kept.join("\n")}` : kept.join("\n")
2191
+ );
2192
+ used += headingCost + sectionUsed;
2269
2193
  }
2270
- return ok({ result: original, code_anchors: anchors });
2194
+ if (state.bounded) {
2195
+ lines.push(
2196
+ `\u2026[rendered context bounded to ${budget} chars; ${state.omitted} item(s) omitted \u2014 fetch full node content by uid, narrow the query, raise max_output_chars, or pass format: "json" for the raw response]`
2197
+ );
2198
+ }
2199
+ return lines.join("\n\n").slice(0, budget);
2271
2200
  }
2272
- async function anchorRepository(client2, repository, spaceUid, agentId) {
2273
- return client2.entity({
2274
- action: "create",
2275
- label: repository.repoId,
2276
- props: {
2277
- entity_type: "repository",
2278
- canonical_name: repository.repoId,
2279
- identifiers: { repo_id: repository.repoId }
2280
- },
2281
- identity: {
2282
- namespace: "external.code",
2283
- key_version: 1,
2284
- key: repositoryIdentityKey(repository)
2285
- },
2286
- identity_space_uid: spaceUid,
2287
- agent_id: agentId
2201
+ function policySections(payload, state) {
2202
+ const policies = Array.isArray(payload.applicable_policies) ? payload.applicable_policies : [];
2203
+ if (policies.length === 0) return [];
2204
+ const items = policies.map((raw) => obj(raw)).filter((policy) => Boolean(policy)).map((policy) => {
2205
+ const rules = Array.isArray(policy.prose_rules) ? policy.prose_rules.map(String).join("; ") : policy.prose_rules;
2206
+ return `- ${str(policy.name) || "(unnamed policy)"}: ${clip(rules, SUMMARY_MAX, state) || "(structured rules apply)"}`;
2288
2207
  });
2208
+ return [{ heading: "## Applicable policies", items }];
2289
2209
  }
2290
- async function anchorCodeRef(client2, ref, repositoryUid, spaceUid, agentId) {
2291
- const response = await client2.entity({
2292
- action: "create",
2293
- label: ref.qualifiedName,
2294
- props: {
2295
- entity_type: ref.kind === "file" ? "code_file" : "code_symbol",
2296
- canonical_name: ref.qualifiedName,
2297
- identifiers: {
2298
- repo_id: ref.repoId,
2299
- path: ref.path,
2300
- qualified_name: ref.qualifiedName
2301
- },
2302
- attributes: { code_ref: storedCodeRef(ref) }
2303
- },
2304
- identity: {
2305
- namespace: "external.code",
2306
- key_version: 1,
2307
- key: codeRefIdentityKey(ref)
2308
- },
2309
- identity_space_uid: spaceUid,
2310
- agent_id: agentId
2311
- });
2312
- if (response.uid && response.created !== false && response.status !== "existing") {
2313
- await client2.entity({
2314
- action: "relate",
2315
- source_uid: response.uid,
2316
- target_uid: repositoryUid,
2317
- edge_type: "PART_OF",
2318
- agent_id: agentId
2319
- });
2320
- }
2321
- return { ...response, ref };
2322
- }
2323
- async function resolveMaterializedUid(client2, ref, agentId) {
2324
- const response = await client2.entity({
2325
- action: "resolve_identity",
2326
- identity: {
2327
- namespace: "external.code",
2328
- key_version: 1,
2329
- key: codeRefIdentityKey(ref)
2330
- },
2331
- agent_id: agentId
2332
- });
2333
- return response.status === "existing" ? response.uid : void 0;
2334
- }
2335
- async function codeRefFromAnchor(client2, adapter, uid) {
2336
- const node = await client2.getNode(uid);
2337
- const attributes = node.props?.attributes;
2338
- if (!attributes || typeof attributes !== "object") return void 0;
2339
- const codeRef = attributes.code_ref;
2340
- if (!isStoredCodeRef(codeRef)) return void 0;
2341
- try {
2342
- const repository = await adapter.resolveRepository(codeRef.repoId);
2343
- return { ref: { ...codeRef, repoRoot: repository.repoRoot } };
2344
- } catch (cause) {
2345
- if (cause instanceof UnknownRepositoryError) {
2346
- return { missingRepo: codeRef.repoId };
2210
+ function hiddenSections(payload) {
2211
+ const hidden = payload.hidden_item_count;
2212
+ if (hidden === void 0 || hidden === null) return [];
2213
+ return [
2214
+ {
2215
+ heading: `Note: ${hidden} item(s) were hidden by access scoping.`,
2216
+ items: []
2347
2217
  }
2348
- throw cause;
2349
- }
2218
+ ];
2350
2219
  }
2351
- async function resolveSingleTarget(client2, adapter, refs, anchorUids, options) {
2352
- if (refs.length > 1 || anchorUids.length > 1) {
2353
- return { error: err("ambiguous_target", "expand/affected accepts exactly one code target") };
2354
- }
2355
- if (anchorUids.length === 1) {
2356
- const outcome = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
2357
- if (!outcome) {
2358
- return { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
2359
- }
2360
- if ("missingRepo" in outcome) {
2361
- return {
2362
- error: err(
2363
- "repository_unavailable",
2364
- `repository ${outcome.missingRepo} is not checked out here or declared in .mindgraph/workspace.json; expand/affected need its local index`
2365
- )
2366
- };
2220
+ function renderContext(response, maxChars) {
2221
+ const payload = obj(response);
2222
+ if (!payload) return void 0;
2223
+ if (typeof payload.context_text === "string" && payload.context_text.length > 0) {
2224
+ const text2 = payload.context_text;
2225
+ if (maxChars !== void 0 && text2.length > maxChars) {
2226
+ return `${text2.slice(0, maxChars)}
2227
+ [truncated to ${maxChars} characters]`;
2367
2228
  }
2368
- return { ref: outcome.ref };
2369
- }
2370
- if (refs.length !== 1) {
2371
- return { error: err("missing_code_ref", "one code_ref or anchor_uid is required") };
2372
- }
2373
- const resolution = await adapter.resolveRefs(refs, options);
2374
- const result = resolution.results[0];
2375
- if (!result.resolved) {
2376
- return {
2377
- error: ok({
2378
- availability: resolution.availability,
2379
- result,
2380
- wrote: false
2381
- })
2382
- };
2229
+ return text2;
2383
2230
  }
2384
- return { ref: result.resolved };
2385
- }
2386
- async function joinMaterialized(client2, refs, agentId, limit) {
2387
- const joined = [];
2388
- for (const ref of uniqueRefs(refs).slice(0, limit)) {
2389
- const uid = await resolveMaterializedUid(client2, ref, agentId);
2390
- if (!uid) continue;
2391
- joined.push({
2392
- ref,
2393
- anchor_uid: uid,
2394
- graph: await client2.traverse({
2395
- action: "neighborhood",
2396
- start_uid: uid,
2397
- depth: 2,
2398
- max_nodes: 20
2399
- })
2231
+ const graph = obj(payload.graph);
2232
+ const rawNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
2233
+ const shaped = rawNodes.filter(isNodeShaped);
2234
+ const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str(node.node_type) || ""));
2235
+ const infraOnly = shaped.length - nodes.length;
2236
+ const articles = Array.isArray(payload.articles) ? payload.articles : [];
2237
+ const chunks = Array.isArray(payload.chunks) ? payload.chunks : [];
2238
+ const state = { bounded: false, omitted: 0 };
2239
+ const sections = [];
2240
+ if (articles.length > 0) {
2241
+ sections.push({
2242
+ heading: "## Knowledge articles",
2243
+ items: articles.map((raw) => obj(raw)).filter((article) => Boolean(article)).map(
2244
+ (article) => `### ${str(article.label) || "(untitled)"} [${str(article.uid) || "?"}]
2245
+ ${clip(article.content, ARTICLE_MAX, state) || ""}`
2246
+ )
2400
2247
  });
2401
2248
  }
2402
- return joined;
2403
- }
2404
- async function codeRefsFromGraphContext(client2, adapter, context) {
2405
- const uids = /* @__PURE__ */ new Set();
2406
- collectUids(context, uids);
2407
- if (uids.size === 0) return { refs: [], unavailable: [] };
2408
- const nodes = client2.getNodesBatch ? await client2.getNodesBatch([...uids]) : await Promise.all([...uids].map((uid) => client2.getNode(uid).catch(() => void 0)));
2409
- const stored = nodes.flatMap((node) => {
2410
- if (!node) return [];
2411
- const attributes = node.props?.attributes;
2412
- if (!attributes || typeof attributes !== "object") return [];
2413
- const codeRef = attributes.code_ref;
2414
- return isStoredCodeRef(codeRef) ? [codeRef] : [];
2415
- });
2416
- const refs = [];
2417
- const unavailable = /* @__PURE__ */ new Set();
2418
- for (const ref of stored) {
2419
- try {
2420
- const repository = await adapter.resolveRepository(ref.repoId);
2421
- refs.push({ ...ref, repoRoot: repository.repoRoot });
2422
- } catch (cause) {
2423
- if (cause instanceof UnknownRepositoryError) {
2424
- unavailable.add(ref.repoId);
2425
- } else {
2426
- throw cause;
2427
- }
2249
+ if (nodes.length > 0) {
2250
+ sections.push(...groupedNodeSections(nodes, state, "## Knowledge graph\n\n"));
2251
+ const labelByUid = /* @__PURE__ */ new Map();
2252
+ for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
2253
+ const relationships = relationshipItems(
2254
+ Array.isArray(graph?.edges) ? graph.edges : [],
2255
+ labelByUid,
2256
+ state
2257
+ );
2258
+ if (relationships.length > 0) {
2259
+ sections.push({ heading: "## Relationships", items: relationships });
2428
2260
  }
2261
+ } else if (infraOnly > 0) {
2262
+ sections.push({
2263
+ heading: `Note: ${infraOnly} document/chunk node(s) matched \u2014 use action "document_index" or include_chunks for their content.`,
2264
+ items: []
2265
+ });
2429
2266
  }
2430
- return { refs, unavailable: [...unavailable].sort() };
2431
- }
2432
- function collectUids(value, uids) {
2433
- if (Array.isArray(value)) {
2434
- value.forEach((item) => collectUids(item, uids));
2435
- return;
2267
+ if (chunks.length > 0) {
2268
+ sections.push({
2269
+ heading: "## Source excerpts",
2270
+ items: chunks.map((raw) => obj(raw)).filter((chunk) => Boolean(chunk)).map(
2271
+ (chunk) => `- [${str(chunk.chunk_uid) || "?"}] from "${str(chunk.document_title) || "unknown document"}": "${clip(chunk.content, CHUNK_MAX, state) || ""}"`
2272
+ )
2273
+ });
2436
2274
  }
2437
- if (!value || typeof value !== "object") return;
2438
- for (const [key, child] of Object.entries(value)) {
2439
- if (typeof child === "string" && ["uid", "node_uid", "neighbor_uid", "from_uid", "to_uid"].includes(key)) {
2440
- uids.add(child);
2441
- } else {
2442
- collectUids(child, uids);
2443
- }
2275
+ sections.push(...policySections(payload, state));
2276
+ sections.push(...hiddenSections(payload));
2277
+ if (sections.length === 0) {
2278
+ return "No results found for this search.";
2444
2279
  }
2280
+ if (nodes.length === 0 && articles.length === 0 && chunks.length === 0) {
2281
+ return assemble(
2282
+ "# Retrieved context\n\nNo graph results matched this search.",
2283
+ sections,
2284
+ state,
2285
+ maxChars
2286
+ );
2287
+ }
2288
+ return assemble("# Retrieved context", sections, state, maxChars);
2445
2289
  }
2446
- function isStoredCodeRef(value) {
2447
- if (!value || typeof value !== "object") return false;
2448
- const ref = value;
2449
- return typeof ref.repoId === "string" && typeof ref.path === "string" && typeof ref.language === "string" && typeof ref.kind === "string" && typeof ref.qualifiedName === "string" && (typeof ref.signature === "string" || ref.signature === null) && typeof ref.startLine === "number" && typeof ref.endLine === "number";
2450
- }
2451
- function storedCodeRef(ref) {
2452
- const { repoRoot: _localOnly, ...stored } = ref;
2453
- return stored;
2290
+ function resumeEntry(raw, state, fallbackType) {
2291
+ const wrapper = obj(raw);
2292
+ if (!wrapper) return void 0;
2293
+ const value = obj(wrapper.node) ?? wrapper;
2294
+ if (isNodeShaped(value)) return nodeLines(value, state);
2295
+ const uid = str(value.uid);
2296
+ const props = obj(value.props);
2297
+ const label = clip(value.label, LABEL_MAX, state) ?? clip(value.name, LABEL_MAX, state) ?? clip(value.title, LABEL_MAX, state) ?? clip(value.action, LABEL_MAX, state) ?? fallbackType;
2298
+ const status = str(value.status) ?? str(props?.status);
2299
+ const summary = clip(value.summary, SUMMARY_MAX, state) ?? clip(value.description, SUMMARY_MAX, state) ?? clip(value.content, SUMMARY_MAX, state) ?? clip(value.outcome_summary, SUMMARY_MAX, state) ?? clip(props?.summary, SUMMARY_MAX, state);
2300
+ const details = [
2301
+ status ? `status ${status}` : void 0,
2302
+ str(value.path) ? `path ${str(value.path)}` : void 0,
2303
+ str(value.symbol) ? `symbol ${str(value.symbol)}` : void 0
2304
+ ].filter(Boolean);
2305
+ const head = `- **${label}**${uid ? ` [${uid}]` : ""}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
2306
+ return summary && summary !== label ? `${head}: ${summary}` : head;
2454
2307
  }
2455
- function uniqueRefs(refs) {
2456
- const seen = /* @__PURE__ */ new Set();
2457
- return refs.filter((ref) => {
2458
- const key = `${ref.repoId}:${ref.path}:${ref.qualifiedName}:${ref.signature ?? ""}`;
2459
- if (seen.has(key)) return false;
2460
- seen.add(key);
2461
- return true;
2462
- });
2308
+ function resumeSection(heading, values, state, fallbackType) {
2309
+ const items = values.map((value) => resumeEntry(value, state, fallbackType)).filter((value) => Boolean(value));
2310
+ return items.length > 0 ? { heading, items } : void 0;
2463
2311
  }
2464
- function primaryUids(value) {
2465
- if (!value || typeof value !== "object") return [];
2466
- const object2 = value;
2467
- const keys = [
2468
- "uid",
2469
- "claim_uid",
2470
- "goal_uid",
2471
- "project_uid",
2472
- "milestone_uid",
2473
- "decision_uid",
2474
- "task_uid",
2475
- "plan_uid",
2476
- "step_uid",
2477
- "execution_uid",
2478
- "lesson_uid"
2479
- ];
2480
- return [
2481
- ...new Set(
2482
- keys.flatMap((key) => typeof object2[key] === "string" ? [object2[key]] : [])
2483
- )
2484
- ];
2485
- }
2486
- var ok, err, CODE_TOOL;
2487
- var init_code_tool = __esm({
2488
- "src/code-tool.ts"() {
2489
- "use strict";
2490
- init_error_detail();
2491
- init_codegraph();
2492
- ok = (data) => ({
2493
- content: [{ type: "text", text: JSON.stringify(data) }]
2494
- });
2495
- err = (code, message, details) => ({
2496
- content: [
2497
- {
2498
- type: "text",
2499
- text: JSON.stringify({ error: message, code, details })
2500
- }
2501
- ],
2502
- isError: true
2312
+ function renderResumeContext(response, maxChars, repositoryNames = {}) {
2313
+ const payload = obj(response);
2314
+ if (!payload) return void 0;
2315
+ const state = { bounded: false, omitted: 0 };
2316
+ const status = str(payload.status);
2317
+ if (!payload.task && status === "no_eligible_work") {
2318
+ const scopedOut = typeof payload.scoped_out_tasks === "number" ? payload.scoped_out_tasks : 0;
2319
+ const note = scopedOut > 0 ? ` ${scopedOut} task(s) exist outside the requested scope.` : "";
2320
+ return assemble(
2321
+ "# Resume context",
2322
+ [{ heading: `No eligible work was found in the requested scope.${note}`, items: [] }],
2323
+ state,
2324
+ maxChars
2325
+ );
2326
+ }
2327
+ const sections = [];
2328
+ const push = (section) => {
2329
+ if (section) sections.push(section);
2330
+ };
2331
+ push(resumeSection("## Current task", [payload.task], state, "Task"));
2332
+ const other = otherEligibleWork(payload, repositoryNames);
2333
+ if (other.shown > 0) {
2334
+ sections.push({
2335
+ heading: `## Other eligible work (${other.shown} shown${other.more > 0 ? `, ${other.more} more not listed` : ""})`,
2336
+ items: other.groups.flatMap(
2337
+ (group) => group.tasks.map(
2338
+ (task) => `- ${group.repository}: ${task.label} [${task.uid}]${task.detail ? ` \u2014 ${task.detail}` : ""}`
2339
+ )
2340
+ )
2503
2341
  });
2504
- CODE_TOOL = {
2505
- name: "mindgraph_code",
2506
- description: "Resolve typed code references through the local codegraph index and join them to persistent MindGraph memory/work. anchor creates exact repository/code anchors; recall reads attached graph context; expand reads callers/callees; affected reads impact for a code ref or Task/Execution. Ambiguous refs return candidates without writing. Refs with a path route to the repository defining that file (nearest declared root in .mindgraph/workspace.json); bare symbols resolve against the current repository only \u2014 pass path or repo to target a sibling.",
2507
- inputSchema: {
2508
- type: "object",
2509
- properties: {
2510
- action: {
2511
- type: "string",
2512
- enum: ["anchor", "recall", "expand", "affected"]
2513
- },
2514
- code_refs: {
2515
- type: "array",
2516
- maxItems: 50,
2517
- items: {
2518
- oneOf: [
2519
- { type: "string" },
2520
- {
2521
- type: "object",
2522
- properties: {
2523
- repo: { type: "string" },
2524
- path: { type: "string" },
2525
- line: { type: "integer", minimum: 1 },
2526
- symbol: { type: "string" },
2527
- kind: { type: "string" },
2528
- language: { type: "string" },
2529
- signature: { type: "string" }
2530
- },
2531
- additionalProperties: false
2532
- }
2533
- ]
2534
- }
2535
- },
2536
- anchor_uids: {
2537
- type: "array",
2538
- maxItems: 50,
2539
- items: { type: "string" },
2540
- description: "Already-materialized code anchor UIDs (codegraph is not required for recall)"
2541
- },
2542
- repo: {
2543
- type: "string",
2544
- description: "Configured repository id or repository root"
2545
- },
2546
- repo_space_uid: {
2547
- type: "string",
2548
- description: "Writable shared Project/repository Space for new anchors (anchor requires this, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID)"
2549
- },
2550
- task_uid: { type: "string" },
2551
- execution_uid: { type: "string" },
2552
- limit: { type: "integer", minimum: 1, maximum: 50 },
2553
- agent_id: { type: "string" }
2554
- },
2555
- required: ["action"]
2556
- },
2557
- annotations: {
2558
- readOnlyHint: false,
2559
- destructiveHint: false,
2560
- idempotentHint: true,
2561
- openWorldHint: true
2562
- }
2563
- };
2564
2342
  }
2565
- });
2566
-
2567
- // src/call-ledger.ts
2568
- function callsDir(options) {
2569
- return import_node_path3.default.join(
2570
- options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path3.default.join(import_node_os3.default.homedir(), ".mindgraph", "runtime"),
2571
- "calls"
2343
+ push(
2344
+ resumeSection(
2345
+ "## Goal and project",
2346
+ [payload.goal, payload.project].filter(Boolean),
2347
+ state,
2348
+ "Scope"
2349
+ )
2572
2350
  );
2573
- }
2574
- function stableJson(value) {
2575
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
2576
- if (value && typeof value === "object") {
2577
- return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
2351
+ push(resumeSection("## Plan", [payload.plan], state, "Plan"));
2352
+ push(
2353
+ resumeSection(
2354
+ "## Next steps",
2355
+ Array.isArray(payload.next_steps) ? payload.next_steps : [],
2356
+ state,
2357
+ "Next step"
2358
+ )
2359
+ );
2360
+ push(
2361
+ resumeSection(
2362
+ "## Blockers",
2363
+ Array.isArray(payload.blockers) ? payload.blockers : [],
2364
+ state,
2365
+ "Blocker"
2366
+ )
2367
+ );
2368
+ const knowledge = obj(payload.knowledge);
2369
+ push(
2370
+ resumeSection(
2371
+ "## Relevant lessons and risks",
2372
+ [
2373
+ ...Array.isArray(knowledge?.lessons) ? knowledge.lessons : [],
2374
+ ...Array.isArray(knowledge?.risks) ? knowledge.risks : []
2375
+ ],
2376
+ state,
2377
+ "Knowledge"
2378
+ )
2379
+ );
2380
+ push(
2381
+ resumeSection(
2382
+ "## Active execution",
2383
+ [payload.active_execution].filter(Boolean),
2384
+ state,
2385
+ "Execution"
2386
+ )
2387
+ );
2388
+ push(
2389
+ resumeSection(
2390
+ "## Recent executions",
2391
+ Array.isArray(payload.recent_executions) ? payload.recent_executions : [],
2392
+ state,
2393
+ "Execution"
2394
+ )
2395
+ );
2396
+ push(
2397
+ resumeSection(
2398
+ "## Code targets",
2399
+ Array.isArray(payload.code_targets) ? payload.code_targets : [],
2400
+ state,
2401
+ "Code target"
2402
+ )
2403
+ );
2404
+ push(resumeSection("## Lease", [payload.lease].filter(Boolean), state, "Lease"));
2405
+ const warning = clip(payload.warning, SUMMARY_MAX, state);
2406
+ if (warning) sections.push({ heading: "## Warning", items: [`- ${warning}`] });
2407
+ const selectionReason = clip(payload.selection_reason, SUMMARY_MAX, state);
2408
+ if (selectionReason) {
2409
+ sections.push({ heading: "## Selection", items: [`- ${selectionReason}`] });
2578
2410
  }
2579
- return JSON.stringify(value) ?? "null";
2580
- }
2581
- function bareToolName(name) {
2582
- const cut = name.lastIndexOf("__");
2583
- return cut === -1 ? name : name.slice(cut + 2);
2584
- }
2585
- function callKey(toolName, toolInput) {
2586
- return import_node_crypto4.default.createHash("sha256").update(`${bareToolName(toolName)}\0${stableJson(toolInput)}`).digest("hex");
2587
- }
2588
- function sessionKey(sessionId) {
2589
- return import_node_crypto4.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
2411
+ if (sections.length === 0) return void 0;
2412
+ return assemble("# Resume context", sections, state, maxChars);
2590
2413
  }
2591
- function prune(dir, now) {
2592
- let names;
2593
- try {
2594
- names = import_node_fs3.default.readdirSync(dir);
2595
- } catch {
2596
- return;
2597
- }
2598
- for (const name of names) {
2599
- const file = import_node_path3.default.join(dir, name);
2600
- try {
2601
- if (now - import_node_fs3.default.statSync(file).mtimeMs > TTL_MS) import_node_fs3.default.unlinkSync(file);
2602
- } catch {
2603
- }
2414
+ function otherEligibleWork(brief, names = {}) {
2415
+ const listed = Array.isArray(brief?.other_eligible) ? brief.other_eligible : [];
2416
+ const groups = [];
2417
+ let shown = 0;
2418
+ for (const entry of listed) {
2419
+ const task = obj(entry);
2420
+ const uid = typeof task?.uid === "string" ? task.uid : void 0;
2421
+ if (!task || !uid) continue;
2422
+ const scopes = Array.isArray(task.scope_uids) ? task.scope_uids.map(String) : [];
2423
+ const named = scopes.find((scope) => names[scope]);
2424
+ const repository = named ? names[named] : "Other scopes";
2425
+ let group = groups.find((candidate) => candidate.repository === repository);
2426
+ if (!group) {
2427
+ group = { repository, tasks: [] };
2428
+ groups.push(group);
2429
+ }
2430
+ const label = typeof task.label === "string" && task.label ? task.label : "(unlabeled)";
2431
+ const detail = [
2432
+ typeof task.status === "string" ? task.status : void 0,
2433
+ typeof task.priority === "string" && task.priority ? `priority ${task.priority}` : void 0
2434
+ ].filter(Boolean).join(", ");
2435
+ group.tasks.push({
2436
+ uid,
2437
+ label: label.length > 160 ? `${label.slice(0, 159)}\u2026` : label,
2438
+ detail
2439
+ });
2440
+ shown += 1;
2604
2441
  }
2442
+ const total = typeof brief?.eligible_total === "number" ? brief.eligible_total : 0;
2443
+ return { groups, shown, more: Math.max(0, total - 1 - shown) };
2605
2444
  }
2606
- function recordPendingCall(toolName, toolInput, harnessSessionId, fills, options = {}) {
2607
- const now = (options.now ?? Date.now)();
2608
- const dir = callsDir(options);
2609
- try {
2610
- import_node_fs3.default.mkdirSync(dir, { recursive: true, mode: 448 });
2611
- prune(dir, now);
2612
- const record = {
2613
- harnessSessionId,
2614
- projectDir: options.projectDir ?? process.env.CLAUDE_PROJECT_DIR,
2615
- fills,
2616
- createdAt: now
2617
- };
2618
- const file = import_node_path3.default.join(
2619
- dir,
2620
- `${callKey(toolName, toolInput)}.${sessionKey(harnessSessionId)}.json`
2621
- );
2622
- const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
2623
- import_node_fs3.default.writeFileSync(temporary, JSON.stringify(record), { mode: 384 });
2624
- import_node_fs3.default.renameSync(temporary, file);
2625
- } catch {
2445
+ function renderOtherEligibleWork(brief, names = {}) {
2446
+ const { groups, shown, more } = otherEligibleWork(brief, names);
2447
+ if (shown === 0) return void 0;
2448
+ const lines = [
2449
+ `Other eligible work in this scope (${shown} shown${more > 0 ? `, ${more} more not listed` : ""}; resume one by task_uid):`
2450
+ ];
2451
+ for (const group of groups) {
2452
+ lines.push(`${group.repository}:`);
2453
+ for (const task of group.tasks) {
2454
+ lines.push(`- ${task.label} [${task.uid}]${task.detail ? ` \u2014 ${task.detail}` : ""}`);
2455
+ }
2626
2456
  }
2457
+ return lines.join("\n");
2627
2458
  }
2628
- function resolvePendingCall(toolName, toolInput, options = {}) {
2629
- const now = (options.now ?? Date.now)();
2630
- const dir = callsDir(options);
2631
- const prefix = `${callKey(toolName, toolInput)}.`;
2632
- let names;
2633
- try {
2634
- names = import_node_fs3.default.readdirSync(dir).filter((name) => name.startsWith(prefix) && name.endsWith(".json"));
2635
- } catch {
2636
- return void 0;
2637
- }
2638
- let matches = [];
2639
- for (const name of names) {
2640
- try {
2641
- const record = JSON.parse(
2642
- import_node_fs3.default.readFileSync(import_node_path3.default.join(dir, name), "utf8")
2643
- );
2644
- if (typeof record.createdAt === "number" && now - record.createdAt <= TTL_MS && record.fills && typeof record.fills === "object") {
2645
- matches.push(record);
2459
+ function renderNodeList(response, title, limit, maxChars) {
2460
+ let candidates = [];
2461
+ if (Array.isArray(response)) {
2462
+ candidates = response;
2463
+ } else {
2464
+ const payload = obj(response);
2465
+ if (!payload) return void 0;
2466
+ for (const key of ["nodes", "results", "items", "goals", "questions", "claims", "preferences", "documents"]) {
2467
+ const candidate = payload[key];
2468
+ if (Array.isArray(candidate) && candidate.some((entry) => unwrapSearchResult(entry))) {
2469
+ candidates = candidate;
2470
+ break;
2646
2471
  }
2647
- } catch {
2648
2472
  }
2649
2473
  }
2650
- if (matches.length > 1) {
2651
- const projectDir = options.projectDir ?? process.env.CLAUDE_PROJECT_DIR;
2652
- matches = projectDir ? matches.filter((record) => record.projectDir === projectDir) : [];
2474
+ let nodes = candidates.map(unwrapSearchResult).filter((node) => Boolean(node));
2475
+ if (nodes.length === 0) return void 0;
2476
+ const total = nodes.length;
2477
+ const state = { bounded: false, omitted: 0 };
2478
+ if (limit !== void 0 && limit > 0 && nodes.length > limit) {
2479
+ state.omitted += nodes.length - limit;
2480
+ nodes = nodes.slice(0, limit);
2481
+ state.bounded = true;
2653
2482
  }
2654
- return matches.length === 1 ? matches[0].fills : void 0;
2655
- }
2656
- var import_node_crypto4, import_node_fs3, import_node_os3, import_node_path3, TTL_MS;
2657
- var init_call_ledger = __esm({
2658
- "src/call-ledger.ts"() {
2659
- "use strict";
2660
- import_node_crypto4 = __toESM(require("crypto"));
2661
- import_node_fs3 = __toESM(require("fs"));
2662
- import_node_os3 = __toESM(require("os"));
2663
- import_node_path3 = __toESM(require("path"));
2664
- TTL_MS = 10 * 6e4;
2665
- }
2666
- });
2667
-
2668
- // src/hook-installer.ts
2669
- function storedFeatures(homeDir) {
2670
- const { promptRecall: promptRecall2, capture } = loadHookEnv(
2671
- import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), ".mindgraph")
2483
+ const sections = groupedNodeSections(nodes, state);
2484
+ const shown = nodes.length;
2485
+ return assemble(
2486
+ `# ${title} (${shown === total ? total : `${shown} of ${total}`})`,
2487
+ sections,
2488
+ state,
2489
+ maxChars
2672
2490
  );
2673
- return { promptRecall: promptRecall2, capture };
2674
- }
2675
- function command(harness) {
2676
- return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
2677
- }
2678
- function windowsCommand(harness) {
2679
- return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
2680
2491
  }
2681
- function installHookRunner(sourceFile, homeDir, version) {
2682
- const target = import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_RELATIVE);
2683
- import_node_fs6.default.mkdirSync(import_node_path5.default.dirname(target), { recursive: true });
2684
- const temporary = `${target}.${process.pid}.${import_node_crypto6.default.randomUUID()}.tmp`;
2685
- import_node_fs6.default.copyFileSync(sourceFile, temporary);
2686
- import_node_fs6.default.renameSync(temporary, target);
2687
- const versionPath = import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_VERSION_RELATIVE);
2688
- if (version) {
2689
- import_node_fs6.default.writeFileSync(versionPath, `${version}
2690
- `);
2691
- } else {
2692
- import_node_fs6.default.rmSync(versionPath, { force: true });
2492
+ function renderTraversal(response, maxChars) {
2493
+ const directSteps = Array.isArray(response) ? response : void 0;
2494
+ const payload = obj(response);
2495
+ if (!payload && !directSteps) return void 0;
2496
+ const body = payload ?? {};
2497
+ const container = obj(body.graph) ?? body;
2498
+ const rawNodes = Array.isArray(container.nodes) ? container.nodes : [];
2499
+ const nodes = rawNodes.filter(isNodeShaped);
2500
+ const paths = Array.isArray(body.paths) ? body.paths : [];
2501
+ const hasSteps = directSteps !== void 0 || Object.hasOwn(body, "steps");
2502
+ const rawSteps = directSteps ?? (Array.isArray(body.steps) ? body.steps : []);
2503
+ if (nodes.length === 0 && paths.length === 0 && !hasSteps) return void 0;
2504
+ const state = { bounded: false, omitted: 0 };
2505
+ const sections = [];
2506
+ const labelByUid = /* @__PURE__ */ new Map();
2507
+ for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
2508
+ if (paths.length > 0) {
2509
+ const items = paths.map((raw) => obj(raw)).filter((path10) => Boolean(path10)).map((path10, index) => {
2510
+ const uids = Array.isArray(path10.node_uids) ? path10.node_uids.map(String) : [];
2511
+ const labels = Array.isArray(path10.labels) ? path10.labels.map(String) : Array.isArray(path10.nodes) ? path10.nodes.map((hop) => {
2512
+ const hopNode = obj(hop);
2513
+ return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
2514
+ }) : [];
2515
+ const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
2516
+ const cost = num(path10.cost) ?? num(path10.path_cost);
2517
+ const confidence = num(path10.path_confidence);
2518
+ const meta = [
2519
+ cost !== void 0 ? `cost ${cost}` : void 0,
2520
+ confidence !== void 0 ? `confidence ${confidence}` : void 0
2521
+ ].filter(Boolean);
2522
+ const head = `${index + 1}. ${route}${meta.length > 0 ? ` (${meta.join(", ")})` : ""}`;
2523
+ return labels.length > 0 && uids.length > 0 ? `${head}
2524
+ uids: ${uids.join(" \u2192 ")}` : head;
2525
+ });
2526
+ sections.push({ heading: "## Paths", items });
2693
2527
  }
2694
- return target;
2695
- }
2696
- function installedHookRunnerVersion(homeDir) {
2697
- try {
2698
- const raw = import_node_fs6.default.readFileSync(
2699
- import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_VERSION_RELATIVE),
2700
- "utf8"
2701
- ).trim();
2702
- return raw || void 0;
2703
- } catch {
2704
- return void 0;
2528
+ if (hasSteps) {
2529
+ const stepItems = rawSteps.map((raw) => obj(raw)).filter((step) => Boolean(step)).map((step) => {
2530
+ const depth = num(step.depth);
2531
+ const uid = str(step.node_uid) ?? str(step.uid) ?? str(step.edge_to_uid) ?? "?";
2532
+ const parent = str(step.parent_uid) ?? str(step.edge_from_uid);
2533
+ const label = str(step.label) || "(unlabeled)";
2534
+ const relation = edgeTypeName(step.edge_type);
2535
+ const role = str(step.traversal_role);
2536
+ const type = str(step.node_type) ?? str(obj(step.node_type)?.Custom);
2537
+ const cost = num(step.path_cost);
2538
+ const confidence = num(step.path_confidence);
2539
+ const prefix = [
2540
+ depth !== void 0 ? `depth ${depth}` : void 0,
2541
+ role,
2542
+ relation ? `\u2014${relation}\u2192` : void 0
2543
+ ].filter(Boolean).join(" ");
2544
+ const meta = [
2545
+ type,
2546
+ parent ? `parent ${parent}` : void 0,
2547
+ cost !== void 0 ? `cost ${cost}` : void 0,
2548
+ confidence !== void 0 ? `confidence ${confidence}` : void 0
2549
+ ].filter(Boolean).join(", ");
2550
+ return `- ${prefix ? `${prefix}: ` : ""}**${label}** [${uid}]${meta ? ` (${meta})` : ""}`;
2551
+ });
2552
+ if (stepItems.length > 0) {
2553
+ sections.push({ heading: "## Traversal steps", items: stepItems });
2554
+ } else {
2555
+ const start = str(body.start_uid) || "?";
2556
+ const end = str(body.end_uid);
2557
+ const mode2 = str(body.mode);
2558
+ sections.push({
2559
+ heading: mode2 === "path" && end ? `No path found from [${start}] to [${end}].` : `No traversal steps returned from [${start}].`,
2560
+ items: []
2561
+ });
2562
+ }
2563
+ }
2564
+ if (nodes.length > 0) {
2565
+ sections.push(...groupedNodeSections(nodes, state, "## Nodes\n\n"));
2566
+ const relationships = relationshipItems(
2567
+ Array.isArray(container.edges) ? container.edges : [],
2568
+ labelByUid,
2569
+ state
2570
+ );
2571
+ if (relationships.length > 0) {
2572
+ sections.push({ heading: "## Relationships", items: relationships });
2573
+ }
2705
2574
  }
2575
+ const mode = str(body.mode);
2576
+ return assemble(
2577
+ `# Graph traversal${mode ? ` (${mode})` : ""}`,
2578
+ sections,
2579
+ state,
2580
+ maxChars
2581
+ );
2706
2582
  }
2707
- function parseSemver(value) {
2708
- if (!value) return void 0;
2709
- const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
2710
- if (!match) return void 0;
2711
- return [Number(match[1]), Number(match[2]), Number(match[3])];
2583
+ function neighborLine(value) {
2584
+ const node = obj(value);
2585
+ if (!node) return void 0;
2586
+ const label = str(node.label) || "(unlabeled)";
2587
+ const uid = str(node.uid);
2588
+ const type = str(node.node_type);
2589
+ return `- ${label}${uid ? ` [${uid}]` : ""}${type ? ` (${type})` : ""}`;
2712
2590
  }
2713
- function semverNewer(a, b) {
2714
- for (let index = 0; index < 3; index += 1) {
2715
- if (a[index] !== b[index]) return a[index] > b[index];
2591
+ function renderOntologyAnswer(response) {
2592
+ const payload = obj(response);
2593
+ if (!payload) return void 0;
2594
+ const objects = Array.isArray(payload.objects) ? payload.objects.filter(isNodeShaped) : isNodeShaped(payload.object) ? [payload.object] : [];
2595
+ const cognitive = obj(payload.cognitive_context);
2596
+ const provenance = Array.isArray(payload.provenance) ? payload.provenance : [];
2597
+ const hasQuerySignature = Array.isArray(payload.objects) || obj(payload.confidence) !== void 0;
2598
+ const cognitiveEmpty = !cognitive || Object.values(cognitive).every((v) => !Array.isArray(v) || v.length === 0);
2599
+ if (objects.length === 0 && cognitiveEmpty && provenance.length === 0) {
2600
+ return hasQuerySignature ? "No matching domain objects." : void 0;
2716
2601
  }
2717
- return false;
2718
- }
2719
- function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
2720
- try {
2721
- if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
2722
- return { refreshed: false, reason: "disabled" };
2723
- }
2724
- const home = homeDir || import_node_os5.default.homedir();
2725
- if (!import_node_fs6.default.existsSync(import_node_path5.default.join(home, RUNNER_RELATIVE))) {
2726
- return { refreshed: false, reason: "no_runner" };
2727
- }
2728
- if (!import_node_fs6.default.existsSync(sourceBundle)) {
2729
- return { refreshed: false, reason: "no_source" };
2730
- }
2731
- const server2 = parseSemver(serverVersion);
2732
- if (!server2) return { refreshed: false, reason: "unparseable" };
2733
- const runnerVersion = installedHookRunnerVersion(home);
2734
- if (runnerVersion !== void 0) {
2735
- const pinned = parseSemver(runnerVersion);
2736
- if (!pinned) {
2737
- return { refreshed: false, reason: "unparseable", runnerVersion };
2738
- }
2739
- if (!semverNewer(server2, pinned)) {
2740
- return {
2741
- refreshed: false,
2742
- reason: runnerVersion === serverVersion ? "current" : "downgrade",
2743
- runnerVersion
2744
- };
2602
+ const state = { bounded: false, omitted: 0 };
2603
+ const sections = [];
2604
+ if (objects.length > 0) {
2605
+ sections.push(...groupedNodeSections(objects, state));
2606
+ }
2607
+ const labelByUid = /* @__PURE__ */ new Map();
2608
+ for (const node of objects) labelByUid.set(str(node.uid), str(node.label) || "?");
2609
+ const graph = obj(payload.graph);
2610
+ for (const raw of Array.isArray(graph?.nodes) ? graph.nodes : []) {
2611
+ const node = obj(raw);
2612
+ const uid = str(node?.uid);
2613
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
2614
+ }
2615
+ if (cognitive) {
2616
+ for (const rawNodes of Object.values(cognitive)) {
2617
+ if (!Array.isArray(rawNodes)) continue;
2618
+ for (const raw of rawNodes) {
2619
+ const node = obj(raw);
2620
+ const uid = str(node?.uid);
2621
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
2745
2622
  }
2746
2623
  }
2747
- installHookRunner(sourceBundle, home, serverVersion);
2748
- return { refreshed: true, reason: "updated", runnerVersion };
2749
- } catch {
2750
- return { refreshed: false, reason: "error" };
2751
2624
  }
2752
- }
2753
- function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
2754
- const refreshed = [];
2755
- const scopes = ["user", "project"];
2756
- for (const scope of scopes) {
2757
- try {
2758
- const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
2759
- if (!import_node_fs6.default.existsSync(file)) continue;
2760
- if (!import_node_fs6.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2761
- installHooks(
2762
- "claude-code",
2763
- scope,
2764
- projectDir,
2765
- void 0,
2766
- homeDir,
2767
- storedFeatures(homeDir)
2768
- );
2769
- refreshed.push(file);
2770
- } catch {
2625
+ const relations = Array.isArray(payload.relations) ? payload.relations : [];
2626
+ const relationLines = relationshipItems(relations, labelByUid, state, { fallbackToUid: true });
2627
+ if (relationLines.length > 0) {
2628
+ sections.push({ heading: "## Relations", items: relationLines });
2629
+ }
2630
+ if (cognitive) {
2631
+ for (const [category, rawNodes] of Object.entries(cognitive)) {
2632
+ if (!Array.isArray(rawNodes) || rawNodes.length === 0) continue;
2633
+ const items = rawNodes.map(neighborLine).filter((line) => Boolean(line));
2634
+ if (items.length > 0) {
2635
+ sections.push({ heading: `### Cognitive context \u2014 ${category}`, items });
2636
+ }
2771
2637
  }
2772
2638
  }
2773
- return refreshed;
2774
- }
2775
- function versionSkewNote(serverVersion, homeDir) {
2776
- const runnerVersion = installedHookRunnerVersion(homeDir);
2777
- if (!runnerVersion || runnerVersion === serverVersion) return void 0;
2778
- return `NOTE: the installed MindGraph hook runner is v${runnerVersion} but this MCP server is v${serverVersion} \u2014 hook behavior and tool contracts may be out of sync. Tell the user to run: npx -y mindgraph-mcp@latest install-hooks (refreshes the pinned runner).`;
2779
- }
2780
- function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path5.default.join(import_node_os5.default.homedir(), ".codex"), homeDir) {
2781
- if (harness === "codex") {
2782
- return scope === "user" ? import_node_path5.default.join(codexHomeDir, "hooks.json") : import_node_path5.default.join(projectDir, ".codex", "hooks.json");
2639
+ if (provenance.length > 0) {
2640
+ const items = provenance.map((raw) => obj(raw)).filter((entry) => Boolean(entry)).map((entry) => {
2641
+ const span = clip(entry.text_span, QUOTE_MAX, state) || "";
2642
+ const by = str(entry.ingested_by_name);
2643
+ const suffix = `${by ? ` (ingested by ${by})` : ""} [source ${str(entry.source_uid) || "?"}]`;
2644
+ return str(entry.span_kind) === "anchor" ? `- "${span}"${suffix}` : `- context: ${span}${suffix}`;
2645
+ });
2646
+ sections.push({ heading: "## Sources", items });
2783
2647
  }
2784
- return scope === "user" ? import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), ".claude", "settings.json") : import_node_path5.default.join(projectDir, ".claude", "settings.json");
2648
+ const confidence = num(obj(payload.confidence)?.overall);
2649
+ const notes = [];
2650
+ if (confidence !== void 0) notes.push(`overall confidence ${confidence}`);
2651
+ if (payload.has_more === true) notes.push("more results exist (has_more)");
2652
+ if (payload.seed_cap_hit === true) notes.push("seed cap hit \u2014 narrow the query for a complete set");
2653
+ if (notes.length > 0) sections.push({ heading: `Note: ${notes.join("; ")}.`, items: [] });
2654
+ return assemble(
2655
+ `# Ontology results (${objects.length} object${objects.length === 1 ? "" : "s"})`,
2656
+ sections,
2657
+ state
2658
+ );
2785
2659
  }
2786
- function readSettings(file) {
2787
- if (!import_node_fs6.default.existsSync(file)) return {};
2788
- return JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
2660
+ function renderObjectList(response, title) {
2661
+ const payload = obj(response);
2662
+ if (!payload) return void 0;
2663
+ const rawItems = Array.isArray(payload.items) ? payload.items : Array.isArray(payload.rows) && payload.rows.some((row) => unwrapSearchResult(row)) ? payload.rows : void 0;
2664
+ const items = (rawItems ?? []).map(unwrapSearchResult).filter((node) => Boolean(node));
2665
+ if (items.length === 0) {
2666
+ return rawItems ? `# ${title} (0 results)` : void 0;
2667
+ }
2668
+ const state = { bounded: false, omitted: 0 };
2669
+ const sections = groupedNodeSections(items, state);
2670
+ const total = num(payload.total_count);
2671
+ const truncations = Array.isArray(payload.truncation_reasons) ? payload.truncation_reasons.filter((reason) => typeof reason === "string") : [];
2672
+ const notes = [];
2673
+ if (payload.has_more === true) notes.push("more results exist");
2674
+ if (truncations.length > 0) notes.push(`truncated: ${truncations.join(", ")}`);
2675
+ if (notes.length > 0) sections.push({ heading: `Note: ${notes.join("; ")}.`, items: [] });
2676
+ const count = total !== void 0 && total !== items.length ? `${items.length} of ${total}` : `${items.length}`;
2677
+ return assemble(`# ${title} (${count})`, sections, state);
2789
2678
  }
2790
- function ownedHook(harness, timeout, additionalContextLimit) {
2791
- return {
2792
- type: "command",
2793
- command: command(harness),
2794
- ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
2795
- timeout,
2796
- statusMessage: "Syncing MindGraph session context",
2797
- ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
2798
- };
2799
- }
2800
- function desiredEntries(harness, features = {}) {
2801
- const common = {
2802
- SessionStart: [
2803
- {
2804
- matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
2805
- // B7 live acceptance caught cold SessionStart calls crossing the
2806
- // inherited 20s edge in both harnesses: the hooks correctly failed
2807
- // open, but no brief reached the model. The same installed runner
2808
- // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
2809
- // The declared Codex context limit must not undercut the brief's own
2810
- // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
2811
- // truncation the renderer knew nothing about.
2812
- hooks: [
2813
- ownedHook(
2814
- harness,
2815
- 30,
2816
- harness === "codex" ? 1e4 : void 0
2817
- )
2818
- ]
2819
- }
2820
- ],
2821
- PreToolUse: [
2822
- {
2823
- matcher: "mcp__mindgraph__.*",
2824
- // Repository resolution over a large multi-repo workspace plus three
2825
- // git probes can exceed 3s on the first (uncached) call; a killed
2826
- // PreToolUse means the call goes through untagged.
2827
- hooks: [ownedHook(harness, 5)]
2828
- }
2829
- ],
2830
- // PostToolUse awaits the lease renewal round-trip, which fires exactly
2831
- // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
2832
- // Claude Code: only the tools the hook acts on (MindGraph calls, edits,
2833
- // and the shell/agent calls that keep a lease alive). Read, Grep and Glob
2834
- // are most of a session's tool calls and the hook did nothing useful for
2835
- // them but start a process (roadmap H8). Codex names its tools
2836
- // differently, so it keeps the catch-all.
2837
- PostToolUse: [
2838
- {
2839
- matcher: harness === "claude-code" ? CLAUDE_POST_TOOL_MATCHER : ".*",
2840
- hooks: [ownedHook(harness, 10)]
2841
- }
2842
- ],
2843
- Stop: [{ hooks: [ownedHook(harness, 5)] }],
2844
- // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
2845
- // session close) — the SAME cold-tenant latency SessionStart needs 30s
2846
- // for. Claude honors the configured budget; Codex clamps SessionEnd to a
2847
- // 3s platform cap regardless of this value (see codex-hooks.ts wire
2848
- // map), so the codex adapter instead makes at most ONE call there.
2849
- SessionEnd: [
2850
- { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
2851
- ]
2852
- };
2853
- if (harness !== "claude-code") return common;
2854
- return {
2855
- ...common,
2856
- TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
2857
- TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }],
2858
- // Bounded recall per prompt: the core gives up at 4s and fails open, so
2859
- // a 5s budget never holds the prompt longer than that.
2860
- ...features.promptRecall !== false ? { UserPromptSubmit: [{ hooks: [ownedHook(harness, 5)] }] } : {},
2861
- // Opt-in transcript capture before compaction (cold-tenant ingest).
2862
- ...features.capture === true ? { PreCompact: [{ hooks: [ownedHook(harness, 30)] }] } : {}
2863
- };
2864
- }
2865
- function hookCommand(value) {
2866
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2867
- const command2 = value.command;
2868
- return typeof command2 === "string" ? command2 : void 0;
2869
- }
2870
- function entryHasOwnedHook(value) {
2871
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2872
- const hooks = value.hooks;
2873
- return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
2874
- }
2875
- function writeSettings(file, settings) {
2876
- import_node_fs6.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
2877
- const temporary = `${file}.${process.pid}.${import_node_crypto6.default.randomUUID()}.tmp`;
2878
- import_node_fs6.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2879
- `, {
2880
- mode: 384
2679
+ function renderJobs(response) {
2680
+ const jobs = Array.isArray(response) ? response : obj(response)?.jobs;
2681
+ if (!Array.isArray(jobs)) return void 0;
2682
+ if (jobs.length === 0) return "No ingestion jobs.";
2683
+ const state = { bounded: false, omitted: 0 };
2684
+ const sorted = jobs.map((raw) => obj(raw)).filter((job) => Boolean(job)).sort((a, b) => (num(b.created_at) ?? 0) - (num(a.created_at) ?? 0));
2685
+ const shown = sorted.slice(0, JOBS_RENDER_MAX);
2686
+ if (shown.length < sorted.length) state.bounded = true;
2687
+ const items = shown.map((job) => {
2688
+ const progress = obj(job.progress);
2689
+ const failure = obj(job.error_details);
2690
+ const message = str(failure?.message) ?? str(job.error);
2691
+ const done = num(progress?.processed_chunks);
2692
+ const totalChunks = num(progress?.total_chunks);
2693
+ const bits = [
2694
+ str(job.status) || "?",
2695
+ done !== void 0 && totalChunks !== void 0 ? `${done}/${totalChunks} chunks` : void 0,
2696
+ num(progress?.nodes_created) !== void 0 ? `${num(progress?.nodes_created)} nodes` : void 0,
2697
+ num(job.queue_position) !== void 0 ? `queue #${num(job.queue_position)}` : void 0,
2698
+ message ? `error: ${clip(message, 160, state)}` : void 0,
2699
+ str(failure?.code) ? `code: ${clip(failure?.code, 80, state)}` : void 0,
2700
+ num(failure?.status) !== void 0 ? `status: ${num(failure?.status)}` : void 0,
2701
+ typeof failure?.retriable === "boolean" ? `retriable: ${failure.retriable}` : void 0
2702
+ ].filter(Boolean);
2703
+ return `- [${str(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
2881
2704
  });
2882
- import_node_fs6.default.renameSync(temporary, file);
2883
- }
2884
- function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir, features = {}) {
2885
- const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
2886
- const settings = readSettings(file);
2887
- const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2888
- let added = 0;
2889
- let updated = 0;
2890
- const desired = desiredEntries(harness, features);
2891
- for (const optional of ["UserPromptSubmit", "PreCompact"]) {
2892
- if (optional in desired || !Array.isArray(hooks[optional])) continue;
2893
- const kept = hooks[optional].filter(
2894
- (entry) => !entryHasOwnedHook(entry)
2895
- );
2896
- if (kept.length > 0) hooks[optional] = kept;
2897
- else delete hooks[optional];
2898
- }
2899
- for (const [event, wanted] of Object.entries(desired)) {
2900
- const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
2901
- const kept = current.filter((entry) => !entryHasOwnedHook(entry));
2902
- const hadOwned = kept.length !== current.length;
2903
- for (const entry of wanted) {
2904
- kept.push(entry);
2905
- if (hadOwned) updated += 1;
2906
- else added += 1;
2907
- }
2908
- hooks[event] = kept;
2909
- }
2910
- settings.hooks = hooks;
2911
- writeSettings(file, settings);
2912
- return { path: file, added, updated };
2705
+ return assemble(
2706
+ `# Ingestion jobs (${shown.length}${shown.length < sorted.length ? ` of ${sorted.length}` : ""})`,
2707
+ [{ heading: "", items }],
2708
+ state
2709
+ );
2913
2710
  }
2914
- function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
2915
- const file = settingsPath(harness, scope, projectDir, codexHomeDir);
2916
- if (!import_node_fs6.default.existsSync(file)) return { path: file, removed: 0 };
2917
- const settings = readSettings(file);
2918
- const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2919
- let removed = 0;
2920
- for (const [event, value] of Object.entries(hooks)) {
2921
- if (!Array.isArray(value)) continue;
2922
- const retainedEntries = [];
2923
- for (const rawEntry of value) {
2924
- if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
2925
- retainedEntries.push(rawEntry);
2926
- continue;
2927
- }
2928
- const entry = { ...rawEntry };
2929
- if (!Array.isArray(entry.hooks)) {
2930
- retainedEntries.push(entry);
2931
- continue;
2932
- }
2933
- const retainedHooks = entry.hooks.filter((hook) => {
2934
- const owned = hookCommand(hook)?.includes(OWNER_MARKER) || false;
2935
- if (owned) removed += 1;
2936
- return !owned;
2937
- });
2938
- if (retainedHooks.length > 0) {
2939
- entry.hooks = retainedHooks;
2940
- retainedEntries.push(entry);
2941
- }
2711
+ function renderSignals(response) {
2712
+ const payload = obj(response);
2713
+ if (!payload) return void 0;
2714
+ const state = { bounded: false, omitted: 0 };
2715
+ const sections = [];
2716
+ for (const [key, value] of Object.entries(payload)) {
2717
+ if (!Array.isArray(value) || value.length === 0) continue;
2718
+ const items = value.map((raw) => obj(raw)).filter((item) => Boolean(item)).map((item) => {
2719
+ const fromLabel = str(item.from_label);
2720
+ const toLabel = str(item.to_label);
2721
+ const anyLabelKey = Object.keys(item).find(
2722
+ (k) => k.endsWith("_label") && str(item[k])
2723
+ );
2724
+ const label = str(item.label) ?? str(item.title) ?? str(item.name) ?? (fromLabel && toLabel ? `${fromLabel} \u2194 ${toLabel}` : void 0) ?? (anyLabelKey ? str(item[anyLabelKey]) : void 0) ?? str(item.uid) ?? str(item.target_uid) ?? "(item)";
2725
+ const uid = str(item.uid) ?? str(item.target_uid);
2726
+ const numbers = Object.entries(item).filter(([, v]) => typeof v === "number").slice(0, 4).map(([k, v]) => `${k} ${Math.round(v * 100) / 100}`);
2727
+ const lists = Object.entries(item).filter(([, v]) => Array.isArray(v) && v.length > 0).slice(0, 2).map(([k, v]) => `${k}: ${v.length}`);
2728
+ const meta = [...numbers, ...lists].join(", ");
2729
+ return `- ${clip(label, 160, state)}${uid && uid !== label ? ` [${uid}]` : ""}${meta ? ` (${meta})` : ""}`;
2730
+ });
2731
+ if (items.length > 0) {
2732
+ sections.push({ heading: `## ${key.replace(/_/g, " ")}`, items });
2942
2733
  }
2943
- if (retainedEntries.length > 0) hooks[event] = retainedEntries;
2944
- else delete hooks[event];
2945
2734
  }
2946
- settings.hooks = hooks;
2947
- writeSettings(file, settings);
2948
- return { path: file, removed };
2949
- }
2950
- function installClaudeHooks(scope, projectDir = process.cwd(), features = {}) {
2951
- return installHooks("claude-code", scope, projectDir, void 0, void 0, features);
2952
- }
2953
- function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
2954
- return uninstallHooks("claude-code", scope, projectDir);
2955
- }
2956
- function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2957
- return installHooks("codex", scope, projectDir, codexHomeDir);
2958
- }
2959
- function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2960
- return uninstallHooks("codex", scope, projectDir, codexHomeDir);
2735
+ if (sections.length === 0) return void 0;
2736
+ return assemble("# Synthesis signals", sections, state);
2961
2737
  }
2962
- var import_node_fs6, import_node_os5, import_node_path5, import_node_crypto6, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE, CLAUDE_POST_TOOL_MATCHER;
2963
- var init_hook_installer = __esm({
2964
- "src/hook-installer.ts"() {
2738
+ var LABEL_MAX, SUMMARY_MAX, QUOTE_MAX, ARTICLE_MAX, CHUNK_MAX, RELATIONSHIP_MAX, RENDER_CHAR_BUDGET, MIN_RENDER_CHAR_BUDGET, BOUNDED_MARKER_RESERVE, CURATION_EDGE_TYPES, INFRA_NODE_TYPES, JOBS_RENDER_MAX;
2739
+ var init_render = __esm({
2740
+ "src/render.ts"() {
2965
2741
  "use strict";
2966
- import_node_fs6 = __toESM(require("fs"));
2967
- import_node_os5 = __toESM(require("os"));
2968
- import_node_path5 = __toESM(require("path"));
2969
- import_node_crypto6 = __toESM(require("crypto"));
2970
- init_hook_env();
2971
- OWNER_MARKER = "--owner mindgraph";
2972
- RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
2973
- RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
2974
- CLAUDE_POST_TOOL_MATCHER = "mcp__mindgraph__.*|Edit|MultiEdit|Write|NotebookEdit|Bash|Task|Agent";
2742
+ LABEL_MAX = 200;
2743
+ SUMMARY_MAX = 400;
2744
+ QUOTE_MAX = 180;
2745
+ ARTICLE_MAX = 6e3;
2746
+ CHUNK_MAX = 6e3;
2747
+ RELATIONSHIP_MAX = 40;
2748
+ RENDER_CHAR_BUDGET = 24e3;
2749
+ MIN_RENDER_CHAR_BUDGET = 512;
2750
+ BOUNDED_MARKER_RESERVE = 220;
2751
+ CURATION_EDGE_TYPES = /* @__PURE__ */ new Set(["POSSIBLE_DUPLICATE", "PossibleDuplicate"]);
2752
+ INFRA_NODE_TYPES = /* @__PURE__ */ new Set(["Chunk", "Document", "Article"]);
2753
+ JOBS_RENDER_MAX = 20;
2975
2754
  }
2976
2755
  });
2977
2756
 
2978
- // src/render.ts
2979
- function obj(value) {
2980
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2981
- }
2982
- function str2(value) {
2983
- return typeof value === "string" && value ? value : void 0;
2984
- }
2985
- function num(value) {
2986
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2757
+ // src/error-detail.ts
2758
+ function errorDetail(e) {
2759
+ if (e instanceof MindGraphError && e.body !== void 0) {
2760
+ const detail = typeof e.body === "string" ? e.body : JSON.stringify(e.body);
2761
+ return `${e.message} \u2014 ${detail}`;
2762
+ }
2763
+ return e instanceof Error ? e.message : String(e);
2987
2764
  }
2988
- function edgeTypeName(value) {
2989
- return str2(value) ?? str2(obj(value)?.Custom);
2765
+ function conflictState(e) {
2766
+ if (!(e instanceof MindGraphError) || !e.body || typeof e.body !== "object") {
2767
+ return {};
2768
+ }
2769
+ const body = e.body;
2770
+ const lifted = {};
2771
+ for (const key of CONFLICT_STATE_KEYS) {
2772
+ if (body[key] !== void 0) lifted[key] = body[key];
2773
+ }
2774
+ return lifted;
2990
2775
  }
2991
- function renderBudget(requested) {
2992
- if (requested === void 0 || !Number.isFinite(requested)) return RENDER_CHAR_BUDGET;
2993
- return Math.min(
2994
- RENDER_CHAR_BUDGET,
2995
- Math.max(MIN_RENDER_CHAR_BUDGET, Math.floor(requested))
2996
- );
2997
- }
2998
- function clip(value, max, state) {
2999
- const text2 = str2(value);
3000
- if (!text2) return void 0;
3001
- if (text2.length <= max) return text2;
3002
- state.bounded = true;
3003
- return `${text2.slice(0, max - 1)}\u2026`;
3004
- }
3005
- function isNodeShaped(value) {
3006
- const node = obj(value);
3007
- return Boolean(
3008
- node && str2(node.uid) && (str2(node.label) || str2(node.node_type) || obj(node.node_type))
3009
- );
3010
- }
3011
- function nodeDisplayType(node) {
3012
- const props = obj(node.props);
3013
- const plain = str2(node.node_type);
3014
- return (plain && plain !== "Custom" ? plain : void 0) ?? str2(obj(node.node_type)?.Custom) ?? str2(props?.object_type) ?? str2(props?.type_name) ?? str2(obj(props?.data)?.domain_type) ?? plain ?? "Node";
3015
- }
3016
- function nodeFields(node) {
3017
- const props = obj(node.props);
3018
- return obj(props?.fields) ?? obj(obj(props?.data)?.fields);
2776
+ function toolError(e, fallbackCode) {
2777
+ const body = e instanceof MindGraphError && e.body && typeof e.body === "object" && !Array.isArray(e.body) ? e.body : {};
2778
+ const code = typeof body.code === "string" ? body.code : fallbackCode;
2779
+ const payload = {
2780
+ error: errorDetail(e),
2781
+ ...conflictState(e),
2782
+ ...e instanceof MindGraphError ? { status: e.status } : {},
2783
+ ...code !== void 0 ? { code } : {},
2784
+ ...typeof body.retriable === "boolean" ? { retriable: body.retriable } : {}
2785
+ };
2786
+ return {
2787
+ content: [{ type: "text", text: JSON.stringify(payload) }],
2788
+ structuredContent: payload,
2789
+ isError: true
2790
+ };
3019
2791
  }
3020
- function unwrapSearchResult(value) {
3021
- if (isNodeShaped(value)) return value;
3022
- const wrapper = obj(value);
3023
- const inner = obj(wrapper?.node) ?? obj(wrapper?.object);
3024
- if (inner && isNodeShaped(inner)) {
3025
- const score = num(wrapper.score);
3026
- return score !== void 0 ? { ...inner, retrieval_score: score } : inner;
2792
+ var CONFLICT_STATE_KEYS;
2793
+ var init_error_detail = __esm({
2794
+ "src/error-detail.ts"() {
2795
+ "use strict";
2796
+ init_dist();
2797
+ CONFLICT_STATE_KEYS = [
2798
+ "current_version",
2799
+ "current_epoch",
2800
+ "lease_expires_at",
2801
+ "lease_owner_agent_id"
2802
+ ];
3027
2803
  }
3028
- return void 0;
3029
- }
3030
- function statusTag(node) {
3031
- const tags = [];
3032
- if (node.superseded === true) {
3033
- tags.push(
3034
- node.superseded_by ? `SUPERSEDED by ${str2(node.superseded_by)}` : "SUPERSEDED"
3035
- );
2804
+ });
2805
+
2806
+ // src/code-tool.ts
2807
+ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cwd: invocationCwd(args) })) {
2808
+ const codeClient = client2;
2809
+ const action = args.action;
2810
+ const refs = Array.isArray(args.code_refs) ? args.code_refs : [];
2811
+ const anchorUids = Array.isArray(args.anchor_uids) ? args.anchor_uids : [];
2812
+ const agentId = typeof args.agent_id === "string" ? args.agent_id : "mcp";
2813
+ const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : void 0;
2814
+ const options = {
2815
+ repo: typeof args.repo === "string" ? args.repo : void 0,
2816
+ fallbackSpaceUid
2817
+ };
2818
+ const limit = typeof args.limit === "number" ? args.limit : void 0;
2819
+ try {
2820
+ switch (action) {
2821
+ case "anchor": {
2822
+ if (refs.length === 0) return err("missing_code_refs", "code_refs is required for anchor");
2823
+ const resolution = await adapter.resolveRefs(refs, options);
2824
+ const resolved = resolution.results.flatMap(
2825
+ (result) => result.resolved && result.repository ? [{ ref: result.resolved, repository: result.repository }] : []
2826
+ );
2827
+ const unresolved = resolution.results.filter((result) => !result.resolved);
2828
+ if (resolved.length === 0) {
2829
+ return ok({ ...resolution, anchors: [], wrote: false });
2830
+ }
2831
+ const missingSpace = resolved.find(
2832
+ ({ repository }) => typeof args.repo_space_uid !== "string" && !repository.spaceUid
2833
+ );
2834
+ if (missingSpace) {
2835
+ return err(
2836
+ "missing_repository_space",
2837
+ `repo_space_uid, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID is required for ${missingSpace.repository.repoId}`
2838
+ );
2839
+ }
2840
+ const repositoryAnchors = /* @__PURE__ */ new Map();
2841
+ for (const { repository } of resolved) {
2842
+ if (repositoryAnchors.has(repository.repoRoot)) continue;
2843
+ const spaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : repository.spaceUid;
2844
+ const anchored = await anchorRepository(
2845
+ codeClient,
2846
+ repository,
2847
+ spaceUid,
2848
+ agentId
2849
+ );
2850
+ repositoryAnchors.set(repository.repoRoot, {
2851
+ ...anchored,
2852
+ repository
2853
+ });
2854
+ }
2855
+ const anchors = [];
2856
+ for (const { ref, repository } of resolved) {
2857
+ const repositoryAnchor = repositoryAnchors.get(repository.repoRoot);
2858
+ if (!repositoryAnchor.uid) continue;
2859
+ const spaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : repository.spaceUid;
2860
+ const anchor = await anchorCodeRef(
2861
+ codeClient,
2862
+ ref,
2863
+ repositoryAnchor.uid,
2864
+ spaceUid,
2865
+ agentId
2866
+ );
2867
+ anchors.push(anchor);
2868
+ }
2869
+ return ok({
2870
+ availability: resolution.availability,
2871
+ repository: repositoryAnchors.values().next().value,
2872
+ repositories: [...repositoryAnchors.values()],
2873
+ anchors,
2874
+ unresolved,
2875
+ wrote: anchors.some((anchor) => anchor.created)
2876
+ });
2877
+ }
2878
+ case "recall": {
2879
+ const materialized = new Set(anchorUids);
2880
+ let resolution;
2881
+ if (refs.length > 0) {
2882
+ resolution = await adapter.resolveRefs(refs, options);
2883
+ for (const result of resolution.results) {
2884
+ if (!result.resolved) continue;
2885
+ const uid = await resolveMaterializedUid(codeClient, result.resolved, agentId);
2886
+ if (uid) materialized.add(uid);
2887
+ }
2888
+ }
2889
+ if (materialized.size === 0) {
2890
+ return ok({
2891
+ availability: resolution?.availability,
2892
+ anchors: [],
2893
+ context: [],
2894
+ unresolved: resolution?.results.filter((result) => !result.resolved) ?? []
2895
+ });
2896
+ }
2897
+ const context = await Promise.all(
2898
+ [...materialized].map(async (uid) => ({
2899
+ anchor_uid: uid,
2900
+ graph: await codeClient.traverse({
2901
+ action: "neighborhood",
2902
+ start_uid: uid,
2903
+ depth: 2,
2904
+ max_nodes: Math.min(limit ?? 20, 20)
2905
+ })
2906
+ }))
2907
+ );
2908
+ return ok({
2909
+ availability: resolution?.availability,
2910
+ anchors: [...materialized],
2911
+ context,
2912
+ unresolved: resolution?.results.filter((result) => !result.resolved) ?? []
2913
+ });
2914
+ }
2915
+ case "expand": {
2916
+ const target = await resolveSingleTarget(
2917
+ codeClient,
2918
+ adapter,
2919
+ refs,
2920
+ anchorUids,
2921
+ options
2922
+ );
2923
+ if ("error" in target) return target.error;
2924
+ const expanded = await adapter.expand(target.ref, limit);
2925
+ const joined = await joinMaterialized(
2926
+ codeClient,
2927
+ [...expanded.callers, ...expanded.callees],
2928
+ agentId,
2929
+ Math.min(limit ?? 20, 20)
2930
+ );
2931
+ return ok({ anchor: target.ref, ...expanded, materialized: joined });
2932
+ }
2933
+ case "affected": {
2934
+ const targetUid = typeof args.task_uid === "string" ? args.task_uid : typeof args.execution_uid === "string" ? args.execution_uid : void 0;
2935
+ let graphContext;
2936
+ const graphRefs = [];
2937
+ let unavailableRepositories = [];
2938
+ if (targetUid) {
2939
+ graphContext = await codeClient.traverse({
2940
+ action: "neighborhood",
2941
+ start_uid: targetUid,
2942
+ depth: 2,
2943
+ max_nodes: 50
2944
+ });
2945
+ const fromGraph = await codeRefsFromGraphContext(
2946
+ codeClient,
2947
+ adapter,
2948
+ graphContext
2949
+ );
2950
+ graphRefs.push(...fromGraph.refs);
2951
+ unavailableRepositories = fromGraph.unavailable;
2952
+ }
2953
+ if (refs.length > 0 || anchorUids.length > 0) {
2954
+ const target = await resolveSingleTarget(
2955
+ codeClient,
2956
+ adapter,
2957
+ refs,
2958
+ anchorUids,
2959
+ options
2960
+ );
2961
+ if ("error" in target) return target.error;
2962
+ graphRefs.unshift(target.ref);
2963
+ }
2964
+ const unique = uniqueRefs(graphRefs);
2965
+ if (unique.length === 0) {
2966
+ return ok({
2967
+ target_uid: targetUid,
2968
+ graph_context: graphContext,
2969
+ affected: [],
2970
+ degradation: unavailableRepositories.length > 0 ? `attached code anchors reference repositories unavailable here: ${unavailableRepositories.join(", ")}` : "no materialized code anchors were attached",
2971
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
2972
+ });
2973
+ }
2974
+ const impacts = await Promise.all(
2975
+ unique.map(async (ref) => ({ anchor: ref, ...await adapter.affected(ref, limit) }))
2976
+ );
2977
+ const structural = uniqueRefs(impacts.flatMap((impact) => impact.affected));
2978
+ const joined = await joinMaterialized(
2979
+ codeClient,
2980
+ structural,
2981
+ agentId,
2982
+ Math.min(limit ?? 50, 50)
2983
+ );
2984
+ return ok({
2985
+ target_uid: targetUid,
2986
+ graph_context: graphContext,
2987
+ impacts,
2988
+ materialized: joined,
2989
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
2990
+ });
2991
+ }
2992
+ default:
2993
+ return err("unknown_action", `unknown mindgraph_code action: ${action}`);
2994
+ }
2995
+ } catch (cause) {
2996
+ if (cause instanceof UnknownRepositoryError) {
2997
+ return err("unknown_repository", cause.message);
2998
+ }
2999
+ return toolError(cause, "code_tool_failed");
3036
3000
  }
3037
- const truth = str2(node.truth_status) ?? str2(obj(node.props)?.truth_status);
3038
- if (truth && truth !== "accepted") tags.push(`truth: ${truth}`);
3039
- if (node.currently_valid === false) tags.push("OUTSIDE ITS VALIDITY WINDOW");
3040
- if (node.valid_at_time === false) tags.push("NOT VALID AT THE REQUESTED TIME");
3041
- return tags.length > 0 ? ` [${tags.join("; ")}]` : "";
3042
3001
  }
3043
- function firstQuote(node, state) {
3044
- const chunks = Array.isArray(node.source_chunks) ? node.source_chunks : [];
3045
- for (const raw of chunks) {
3046
- const chunk = obj(raw);
3047
- const quote = str2(chunk?.quote) ?? str2(obj(chunk?.anchor)?.exact);
3048
- if (quote) return clip(quote, QUOTE_MAX, state);
3002
+ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
3003
+ if (result.isError || !Array.isArray(args.code_refs) || args.code_refs.length === 0) {
3004
+ return result;
3005
+ }
3006
+ const original = JSON.parse(result.content[0]?.text ?? "null");
3007
+ const targetUids = primaryUids(original);
3008
+ if (targetUids.length === 0) {
3009
+ return ok({
3010
+ result: original,
3011
+ code_anchors: {
3012
+ wrote: false,
3013
+ caveat: "the write response exposed no primary UID to attach"
3014
+ }
3015
+ });
3016
+ }
3017
+ const anchorResult = await handleCodeTool(
3018
+ client2,
3019
+ {
3020
+ action: "anchor",
3021
+ code_refs: args.code_refs,
3022
+ repo: args.repo,
3023
+ repo_space_uid: args.repo_space_uid,
3024
+ agent_id: args.agent_id,
3025
+ // Keeps the default adapter anchored to the live session directory
3026
+ // rather than wherever the MCP server process was launched.
3027
+ invocation_context: args.invocation_context
3028
+ },
3029
+ adapter
3030
+ );
3031
+ const anchors = JSON.parse(anchorResult.content[0]?.text ?? "null");
3032
+ if (!anchorResult.isError) {
3033
+ const codeClient = client2;
3034
+ const anchorUids = (anchors.anchors ?? []).flatMap(
3035
+ (anchor) => anchor.uid ? [anchor.uid] : []
3036
+ );
3037
+ const relateFailures = [];
3038
+ for (const targetUid of targetUids) {
3039
+ for (const anchorUid of anchorUids) {
3040
+ try {
3041
+ await codeClient.entity({
3042
+ action: "relate",
3043
+ source_uid: targetUid,
3044
+ target_uid: anchorUid,
3045
+ edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
3046
+ agent_id: args.agent_id
3047
+ });
3048
+ } catch (cause) {
3049
+ relateFailures.push(
3050
+ `${targetUid} -> ${anchorUid}: ${errorDetail(cause)}`
3051
+ );
3052
+ }
3053
+ }
3054
+ }
3055
+ if (relateFailures.length > 0) {
3056
+ return ok({
3057
+ result: original,
3058
+ code_anchors: {
3059
+ ...anchors,
3060
+ caveat: `anchors written, but ${relateFailures.length} relate edge(s) failed: ${relateFailures.join("; ")}`
3061
+ }
3062
+ });
3063
+ }
3049
3064
  }
3050
- return void 0;
3065
+ return ok({ result: original, code_anchors: anchors });
3051
3066
  }
3052
- function sourceTag(node) {
3053
- const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
3054
- const first = obj(docs[0]);
3055
- const title = str2(first?.title);
3056
- if (!title) return void 0;
3057
- const extra = docs.length > 1 ? ` (+${docs.length - 1} more)` : "";
3058
- return `from: ${title}${extra}`;
3067
+ async function anchorRepository(client2, repository, spaceUid, agentId) {
3068
+ return client2.entity({
3069
+ action: "create",
3070
+ label: repository.repoId,
3071
+ props: {
3072
+ entity_type: "repository",
3073
+ canonical_name: repository.repoId,
3074
+ identifiers: { repo_id: repository.repoId }
3075
+ },
3076
+ identity: {
3077
+ namespace: "external.code",
3078
+ key_version: 1,
3079
+ key: repositoryIdentityKey(repository)
3080
+ },
3081
+ identity_space_uid: spaceUid,
3082
+ agent_id: agentId
3083
+ });
3059
3084
  }
3060
- function layer7Fields(node, state) {
3061
- const fields = nodeFields(node);
3062
- if (!fields) return void 0;
3063
- const pairs = Object.entries(fields).filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
3064
- if (pairs.length === 0) return void 0;
3065
- return clip(pairs.join("; "), SUMMARY_MAX, state);
3085
+ async function anchorCodeRef(client2, ref, repositoryUid, spaceUid, agentId) {
3086
+ const response = await client2.entity({
3087
+ action: "create",
3088
+ label: ref.qualifiedName,
3089
+ props: {
3090
+ entity_type: ref.kind === "file" ? "code_file" : "code_symbol",
3091
+ canonical_name: ref.qualifiedName,
3092
+ identifiers: {
3093
+ repo_id: ref.repoId,
3094
+ path: ref.path,
3095
+ qualified_name: ref.qualifiedName
3096
+ },
3097
+ attributes: { code_ref: storedCodeRef(ref) }
3098
+ },
3099
+ identity: {
3100
+ namespace: "external.code",
3101
+ key_version: 1,
3102
+ key: codeRefIdentityKey(ref)
3103
+ },
3104
+ identity_space_uid: spaceUid,
3105
+ agent_id: agentId
3106
+ });
3107
+ if (response.uid && response.created !== false && response.status !== "existing") {
3108
+ await client2.entity({
3109
+ action: "relate",
3110
+ source_uid: response.uid,
3111
+ target_uid: repositoryUid,
3112
+ edge_type: "PART_OF",
3113
+ agent_id: agentId
3114
+ });
3115
+ }
3116
+ return { ...response, ref };
3066
3117
  }
3067
- function nodeSummaryText(node, state) {
3068
- const props = obj(node.props);
3069
- return clip(node.summary, SUMMARY_MAX, state) ?? clip(props?.content, SUMMARY_MAX, state) ?? clip(props?.description, SUMMARY_MAX, state) ?? clip(props?.definition, SUMMARY_MAX, state);
3118
+ async function resolveMaterializedUid(client2, ref, agentId) {
3119
+ const response = await client2.entity({
3120
+ action: "resolve_identity",
3121
+ identity: {
3122
+ namespace: "external.code",
3123
+ key_version: 1,
3124
+ key: codeRefIdentityKey(ref)
3125
+ },
3126
+ agent_id: agentId
3127
+ });
3128
+ return response.status === "existing" ? response.uid : void 0;
3070
3129
  }
3071
- function nodeLines(node, state) {
3072
- const label = clip(node.label, LABEL_MAX, state) || "(unlabeled)";
3073
- const type = nodeDisplayType(node);
3074
- const confidence = num(node.confidence);
3075
- const score = num(node.retrieval_score);
3076
- const parenthetical = [
3077
- type,
3078
- confidence !== void 0 ? `confidence ${confidence}` : void 0,
3079
- score !== void 0 ? `score ${Math.round(score * 100) / 100}` : void 0
3080
- ].filter(Boolean).join(", ");
3081
- const head = `- **${label}** [${str2(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
3082
- const summary = nodeSummaryText(node, state);
3083
- const lines = [summary && summary !== label ? `${head}: ${summary}` : head];
3084
- const fields = layer7Fields(node, state);
3085
- if (fields) lines.push(` - fields: ${fields}`);
3086
- const quote = firstQuote(node, state);
3087
- if (quote) lines.push(` - source quote: "${quote}"`);
3088
- const source = sourceTag(node);
3089
- if (source) lines.push(` - ${source}`);
3090
- const believers = Array.isArray(node.believed_by) ? node.believed_by : [];
3091
- if (believers.length > 0) {
3092
- const rendered2 = believers.slice(0, 3).map((raw) => {
3093
- const believer = obj(raw);
3094
- const conf = num(believer?.confidence);
3095
- return `${str2(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
3096
- }).join(", ");
3097
- lines.push(` - believed by: ${rendered2}`);
3130
+ async function codeRefFromAnchor(client2, adapter, uid) {
3131
+ const node = await client2.getNode(uid);
3132
+ const attributes = node.props?.attributes;
3133
+ if (!attributes || typeof attributes !== "object") return void 0;
3134
+ const codeRef = attributes.code_ref;
3135
+ if (!isStoredCodeRef(codeRef)) return void 0;
3136
+ try {
3137
+ const repository = await adapter.resolveRepository(codeRef.repoId);
3138
+ return { ref: { ...codeRef, repoRoot: repository.repoRoot } };
3139
+ } catch (cause) {
3140
+ if (cause instanceof UnknownRepositoryError) {
3141
+ return { missingRepo: codeRef.repoId };
3142
+ }
3143
+ throw cause;
3098
3144
  }
3099
- return lines.join("\n");
3100
3145
  }
3101
- function groupedNodeSections(nodes, state, headingPrefix = "") {
3102
- const groups = /* @__PURE__ */ new Map();
3103
- for (const node of nodes) {
3104
- const type = nodeDisplayType(node);
3105
- const group = groups.get(type) ?? [];
3106
- group.push(node);
3107
- groups.set(type, group);
3146
+ async function resolveSingleTarget(client2, adapter, refs, anchorUids, options) {
3147
+ if (refs.length > 1 || anchorUids.length > 1) {
3148
+ return { error: err("ambiguous_target", "expand/affected accepts exactly one code target") };
3108
3149
  }
3109
- const sections = [];
3110
- let first = true;
3111
- for (const [type, group] of groups) {
3112
- sections.push({
3113
- heading: `${first ? headingPrefix : ""}### ${type}`,
3114
- items: group.map((node) => nodeLines(node, state))
3115
- });
3116
- first = false;
3150
+ if (anchorUids.length === 1) {
3151
+ const outcome = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
3152
+ if (!outcome) {
3153
+ return { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
3154
+ }
3155
+ if ("missingRepo" in outcome) {
3156
+ return {
3157
+ error: err(
3158
+ "repository_unavailable",
3159
+ `repository ${outcome.missingRepo} is not checked out here or declared in .mindgraph/workspace.json; expand/affected need its local index`
3160
+ )
3161
+ };
3162
+ }
3163
+ return { ref: outcome.ref };
3117
3164
  }
3118
- return sections;
3119
- }
3120
- function relationshipItems(edges, labelByUid, state, options) {
3121
- const lines = [];
3122
- for (const raw of edges) {
3123
- const edge = obj(raw);
3124
- if (!edge) continue;
3125
- const type = edgeTypeName(edge.edge_type) || "?";
3126
- const from = str2(edge.from_uid);
3127
- const to = str2(edge.to_uid);
3128
- if (CURATION_EDGE_TYPES.has(type)) continue;
3129
- if (!from || !to || from === to) continue;
3130
- const fromLabel = labelByUid.get(from) ?? (options?.fallbackToUid ? `[${from}]` : void 0);
3131
- const toLabel = labelByUid.get(to) ?? (options?.fallbackToUid ? `[${to}]` : void 0);
3132
- if (!fromLabel || !toLabel) continue;
3133
- lines.push(`- ${fromLabel} \u2014${type}\u2192 ${toLabel}`);
3165
+ if (refs.length !== 1) {
3166
+ return { error: err("missing_code_ref", "one code_ref or anchor_uid is required") };
3134
3167
  }
3135
- if (lines.length > RELATIONSHIP_MAX) {
3136
- state.bounded = true;
3137
- const extra = lines.length - RELATIONSHIP_MAX;
3138
- state.omitted += extra;
3139
- return [...lines.slice(0, RELATIONSHIP_MAX), `- (+${extra} more relationships)`];
3168
+ const resolution = await adapter.resolveRefs(refs, options);
3169
+ const result = resolution.results[0];
3170
+ if (!result.resolved) {
3171
+ return {
3172
+ error: ok({
3173
+ availability: resolution.availability,
3174
+ result,
3175
+ wrote: false
3176
+ })
3177
+ };
3140
3178
  }
3141
- return lines;
3179
+ return { ref: result.resolved };
3142
3180
  }
3143
- function assemble(header, sections, state, maxChars) {
3144
- const budget = renderBudget(maxChars);
3145
- const packingBudget = Math.max(header.length, budget - BOUNDED_MARKER_RESERVE);
3146
- const lines = [header];
3147
- let used = header.length;
3148
- for (const section of sections) {
3149
- if (section.items.length === 0 && !section.heading) continue;
3150
- const headingCost = section.heading.length + 2;
3151
- if (used + headingCost > packingBudget) {
3152
- state.bounded = true;
3153
- state.omitted += Math.max(1, section.items.length);
3154
- continue;
3155
- }
3156
- let sectionUsed = 0;
3157
- const kept = [];
3158
- let dropped = 0;
3159
- for (const item of section.items) {
3160
- if (used + headingCost + sectionUsed + item.length + 1 > packingBudget) {
3161
- dropped += 1;
3162
- continue;
3163
- }
3164
- kept.push(item);
3165
- sectionUsed += item.length + 1;
3166
- }
3167
- if (kept.length === 0 && section.items.length > 0) {
3168
- state.bounded = true;
3169
- state.omitted += dropped;
3170
- continue;
3171
- }
3172
- if (dropped > 0) {
3173
- state.bounded = true;
3174
- state.omitted += dropped;
3175
- const marker = `(+${dropped} more not shown)`;
3176
- if (used + headingCost + sectionUsed + marker.length + 1 <= packingBudget) {
3177
- kept.push(marker);
3178
- sectionUsed += marker.length + 1;
3181
+ async function joinMaterialized(client2, refs, agentId, limit) {
3182
+ const joined = [];
3183
+ for (const ref of uniqueRefs(refs).slice(0, limit)) {
3184
+ const uid = await resolveMaterializedUid(client2, ref, agentId);
3185
+ if (!uid) continue;
3186
+ joined.push({
3187
+ ref,
3188
+ anchor_uid: uid,
3189
+ graph: await client2.traverse({
3190
+ action: "neighborhood",
3191
+ start_uid: uid,
3192
+ depth: 2,
3193
+ max_nodes: 20
3194
+ })
3195
+ });
3196
+ }
3197
+ return joined;
3198
+ }
3199
+ async function codeRefsFromGraphContext(client2, adapter, context) {
3200
+ const uids = /* @__PURE__ */ new Set();
3201
+ collectUids(context, uids);
3202
+ if (uids.size === 0) return { refs: [], unavailable: [] };
3203
+ const nodes = client2.getNodesBatch ? await client2.getNodesBatch([...uids]) : await Promise.all([...uids].map((uid) => client2.getNode(uid).catch(() => void 0)));
3204
+ const stored = nodes.flatMap((node) => {
3205
+ if (!node) return [];
3206
+ const attributes = node.props?.attributes;
3207
+ if (!attributes || typeof attributes !== "object") return [];
3208
+ const codeRef = attributes.code_ref;
3209
+ return isStoredCodeRef(codeRef) ? [codeRef] : [];
3210
+ });
3211
+ const refs = [];
3212
+ const unavailable = /* @__PURE__ */ new Set();
3213
+ for (const ref of stored) {
3214
+ try {
3215
+ const repository = await adapter.resolveRepository(ref.repoId);
3216
+ refs.push({ ...ref, repoRoot: repository.repoRoot });
3217
+ } catch (cause) {
3218
+ if (cause instanceof UnknownRepositoryError) {
3219
+ unavailable.add(ref.repoId);
3220
+ } else {
3221
+ throw cause;
3179
3222
  }
3180
3223
  }
3181
- lines.push(
3182
- section.items.length === 0 ? section.heading : section.heading ? `${section.heading}
3183
- ${kept.join("\n")}` : kept.join("\n")
3184
- );
3185
- used += headingCost + sectionUsed;
3186
3224
  }
3187
- if (state.bounded) {
3188
- lines.push(
3189
- `\u2026[rendered context bounded to ${budget} chars; ${state.omitted} item(s) omitted \u2014 fetch full node content by uid, narrow the query, raise max_output_chars, or pass format: "json" for the raw response]`
3190
- );
3225
+ return { refs, unavailable: [...unavailable].sort() };
3226
+ }
3227
+ function collectUids(value, uids) {
3228
+ if (Array.isArray(value)) {
3229
+ value.forEach((item) => collectUids(item, uids));
3230
+ return;
3231
+ }
3232
+ if (!value || typeof value !== "object") return;
3233
+ for (const [key, child] of Object.entries(value)) {
3234
+ if (typeof child === "string" && ["uid", "node_uid", "neighbor_uid", "from_uid", "to_uid"].includes(key)) {
3235
+ uids.add(child);
3236
+ } else {
3237
+ collectUids(child, uids);
3238
+ }
3191
3239
  }
3192
- return lines.join("\n\n").slice(0, budget);
3193
3240
  }
3194
- function policySections(payload, state) {
3195
- const policies = Array.isArray(payload.applicable_policies) ? payload.applicable_policies : [];
3196
- if (policies.length === 0) return [];
3197
- const items = policies.map((raw) => obj(raw)).filter((policy) => Boolean(policy)).map((policy) => {
3198
- const rules = Array.isArray(policy.prose_rules) ? policy.prose_rules.map(String).join("; ") : policy.prose_rules;
3199
- return `- ${str2(policy.name) || "(unnamed policy)"}: ${clip(rules, SUMMARY_MAX, state) || "(structured rules apply)"}`;
3241
+ function isStoredCodeRef(value) {
3242
+ if (!value || typeof value !== "object") return false;
3243
+ const ref = value;
3244
+ return typeof ref.repoId === "string" && typeof ref.path === "string" && typeof ref.language === "string" && typeof ref.kind === "string" && typeof ref.qualifiedName === "string" && (typeof ref.signature === "string" || ref.signature === null) && typeof ref.startLine === "number" && typeof ref.endLine === "number";
3245
+ }
3246
+ function storedCodeRef(ref) {
3247
+ const { repoRoot: _localOnly, ...stored } = ref;
3248
+ return stored;
3249
+ }
3250
+ function uniqueRefs(refs) {
3251
+ const seen = /* @__PURE__ */ new Set();
3252
+ return refs.filter((ref) => {
3253
+ const key = `${ref.repoId}:${ref.path}:${ref.qualifiedName}:${ref.signature ?? ""}`;
3254
+ if (seen.has(key)) return false;
3255
+ seen.add(key);
3256
+ return true;
3200
3257
  });
3201
- return [{ heading: "## Applicable policies", items }];
3202
3258
  }
3203
- function hiddenSections(payload) {
3204
- const hidden = payload.hidden_item_count;
3205
- if (hidden === void 0 || hidden === null) return [];
3259
+ function primaryUids(value) {
3260
+ if (!value || typeof value !== "object") return [];
3261
+ const object2 = value;
3262
+ const keys = [
3263
+ "uid",
3264
+ "claim_uid",
3265
+ "goal_uid",
3266
+ "project_uid",
3267
+ "milestone_uid",
3268
+ "decision_uid",
3269
+ "task_uid",
3270
+ "plan_uid",
3271
+ "step_uid",
3272
+ "execution_uid",
3273
+ "lesson_uid"
3274
+ ];
3206
3275
  return [
3207
- {
3208
- heading: `Note: ${hidden} item(s) were hidden by access scoping.`,
3209
- items: []
3210
- }
3276
+ ...new Set(
3277
+ keys.flatMap((key) => typeof object2[key] === "string" ? [object2[key]] : [])
3278
+ )
3211
3279
  ];
3212
3280
  }
3213
- function renderContext(response, maxChars) {
3214
- const payload = obj(response);
3215
- if (!payload) return void 0;
3216
- if (typeof payload.context_text === "string" && payload.context_text.length > 0) {
3217
- const text2 = payload.context_text;
3218
- if (maxChars !== void 0 && text2.length > maxChars) {
3219
- return `${text2.slice(0, maxChars)}
3220
- [truncated to ${maxChars} characters]`;
3221
- }
3222
- return text2;
3281
+ var ok, err, CODE_TOOL;
3282
+ var init_code_tool = __esm({
3283
+ "src/code-tool.ts"() {
3284
+ "use strict";
3285
+ init_error_detail();
3286
+ init_codegraph();
3287
+ ok = (data) => ({
3288
+ content: [{ type: "text", text: JSON.stringify(data) }]
3289
+ });
3290
+ err = (code, message, details) => ({
3291
+ content: [
3292
+ {
3293
+ type: "text",
3294
+ text: JSON.stringify({ error: message, code, details })
3295
+ }
3296
+ ],
3297
+ isError: true
3298
+ });
3299
+ CODE_TOOL = {
3300
+ name: "mindgraph_code",
3301
+ description: "Resolve typed code references through the local codegraph index and join them to persistent MindGraph memory/work. anchor creates exact repository/code anchors; recall reads attached graph context; expand reads callers/callees; affected reads impact for a code ref or Task/Execution. Ambiguous refs return candidates without writing. Refs with a path route to the repository defining that file (nearest declared root in .mindgraph/workspace.json); bare symbols resolve against the current repository only \u2014 pass path or repo to target a sibling.",
3302
+ inputSchema: {
3303
+ type: "object",
3304
+ properties: {
3305
+ action: {
3306
+ type: "string",
3307
+ enum: ["anchor", "recall", "expand", "affected"]
3308
+ },
3309
+ code_refs: {
3310
+ type: "array",
3311
+ maxItems: 50,
3312
+ items: {
3313
+ oneOf: [
3314
+ { type: "string" },
3315
+ {
3316
+ type: "object",
3317
+ properties: {
3318
+ repo: { type: "string" },
3319
+ path: { type: "string" },
3320
+ line: { type: "integer", minimum: 1 },
3321
+ symbol: { type: "string" },
3322
+ kind: { type: "string" },
3323
+ language: { type: "string" },
3324
+ signature: { type: "string" }
3325
+ },
3326
+ additionalProperties: false
3327
+ }
3328
+ ]
3329
+ }
3330
+ },
3331
+ anchor_uids: {
3332
+ type: "array",
3333
+ maxItems: 50,
3334
+ items: { type: "string" },
3335
+ description: "Already-materialized code anchor UIDs (codegraph is not required for recall)"
3336
+ },
3337
+ repo: {
3338
+ type: "string",
3339
+ description: "Configured repository id or repository root"
3340
+ },
3341
+ repo_space_uid: {
3342
+ type: "string",
3343
+ description: "Writable shared Project/repository Space for new anchors (anchor requires this, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID)"
3344
+ },
3345
+ task_uid: { type: "string" },
3346
+ execution_uid: { type: "string" },
3347
+ limit: { type: "integer", minimum: 1, maximum: 50 },
3348
+ agent_id: { type: "string" }
3349
+ },
3350
+ required: ["action"]
3351
+ },
3352
+ annotations: {
3353
+ readOnlyHint: false,
3354
+ destructiveHint: false,
3355
+ idempotentHint: true,
3356
+ openWorldHint: true
3357
+ }
3358
+ };
3223
3359
  }
3224
- const graph = obj(payload.graph);
3225
- const rawNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
3226
- const shaped = rawNodes.filter(isNodeShaped);
3227
- const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str2(node.node_type) || ""));
3228
- const infraOnly = shaped.length - nodes.length;
3229
- const articles = Array.isArray(payload.articles) ? payload.articles : [];
3230
- const chunks = Array.isArray(payload.chunks) ? payload.chunks : [];
3231
- const state = { bounded: false, omitted: 0 };
3232
- const sections = [];
3233
- if (articles.length > 0) {
3234
- sections.push({
3235
- heading: "## Knowledge articles",
3236
- items: articles.map((raw) => obj(raw)).filter((article) => Boolean(article)).map(
3237
- (article) => `### ${str2(article.label) || "(untitled)"} [${str2(article.uid) || "?"}]
3238
- ${clip(article.content, ARTICLE_MAX, state) || ""}`
3360
+ });
3361
+
3362
+ // src/workspace-scope.ts
3363
+ function readScopeCache(cache) {
3364
+ if (!cache) return {};
3365
+ try {
3366
+ const parsed = JSON.parse(
3367
+ import_node_fs3.default.readFileSync(import_node_path3.default.join(cache.dir, SCOPE_CACHE_FILE), "utf8")
3368
+ );
3369
+ if (parsed.v !== 1 || parsed.tenant !== cache.tenant || !parsed.entries) return {};
3370
+ const now = (cache.now ?? Date.now)();
3371
+ return Object.fromEntries(
3372
+ Object.entries(parsed.entries).filter(
3373
+ ([, entry]) => typeof entry?.uid === "string" && typeof entry.at === "number" && now - entry.at >= 0 && now - entry.at < SCOPE_CACHE_TTL_MS
3239
3374
  )
3240
- });
3375
+ );
3376
+ } catch {
3377
+ return {};
3241
3378
  }
3242
- if (nodes.length > 0) {
3243
- sections.push(...groupedNodeSections(nodes, state, "## Knowledge graph\n\n"));
3244
- const labelByUid = /* @__PURE__ */ new Map();
3245
- for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3246
- const relationships = relationshipItems(
3247
- Array.isArray(graph?.edges) ? graph.edges : [],
3248
- labelByUid,
3249
- state
3379
+ }
3380
+ function writeScopeCache(cache, entries) {
3381
+ if (!cache) return;
3382
+ try {
3383
+ import_node_fs3.default.mkdirSync(cache.dir, { recursive: true, mode: 448 });
3384
+ const file = import_node_path3.default.join(cache.dir, SCOPE_CACHE_FILE);
3385
+ const temporary = `${file}.${process.pid}.tmp`;
3386
+ import_node_fs3.default.writeFileSync(
3387
+ temporary,
3388
+ JSON.stringify({ v: 1, tenant: cache.tenant, entries }),
3389
+ { mode: 384 }
3250
3390
  );
3251
- if (relationships.length > 0) {
3252
- sections.push({ heading: "## Relationships", items: relationships });
3253
- }
3254
- } else if (infraOnly > 0) {
3255
- sections.push({
3256
- heading: `Note: ${infraOnly} document/chunk node(s) matched \u2014 use action "document_index" or include_chunks for their content.`,
3257
- items: []
3258
- });
3391
+ import_node_fs3.default.renameSync(temporary, file);
3392
+ } catch {
3259
3393
  }
3260
- if (chunks.length > 0) {
3261
- sections.push({
3262
- heading: "## Source excerpts",
3263
- items: chunks.map((raw) => obj(raw)).filter((chunk) => Boolean(chunk)).map(
3264
- (chunk) => `- [${str2(chunk.chunk_uid) || "?"}] from "${str2(chunk.document_title) || "unknown document"}": "${clip(chunk.content, CHUNK_MAX, state) || ""}"`
3265
- )
3266
- });
3394
+ }
3395
+ function record(value) {
3396
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3397
+ }
3398
+ async function resolveRepositoryScope(cwd, client2, agentId, options = {}) {
3399
+ if (!client2.entity) return { uids: [], names: {}, failures: [] };
3400
+ const adapter = new CodegraphAdapter({
3401
+ cwd,
3402
+ workspaceFile: options.workspaceFile,
3403
+ childWorkspaces: true
3404
+ });
3405
+ const configured = await adapter.resolveRepositories();
3406
+ const repositories = configured.length > 0 ? configured : [await adapter.resolveRepository()];
3407
+ const failures = [];
3408
+ const names = {};
3409
+ const cached = readScopeCache(options.cache);
3410
+ const fresh = {};
3411
+ const stamp = (options.cache?.now ?? Date.now)();
3412
+ const resolved = await Promise.all(
3413
+ repositories.map(async (repository) => {
3414
+ const remembered = cached[repository.repoId];
3415
+ if (remembered) {
3416
+ names[remembered.uid] = repository.repoId;
3417
+ return remembered.uid;
3418
+ }
3419
+ try {
3420
+ const payload = record(
3421
+ await client2.entity({
3422
+ action: "resolve_identity",
3423
+ identity: {
3424
+ namespace: "external.code",
3425
+ key_version: 1,
3426
+ key: repositoryIdentityKey(repository)
3427
+ },
3428
+ agent_id: agentId
3429
+ })
3430
+ );
3431
+ if (payload?.status === "existing" && typeof payload.uid === "string" && payload.uid) {
3432
+ names[payload.uid] = repository.repoId;
3433
+ fresh[repository.repoId] = { uid: payload.uid, at: stamp };
3434
+ return payload.uid;
3435
+ }
3436
+ if (payload?.status === "absent") {
3437
+ const anchored = await anchorRepository(
3438
+ client2,
3439
+ repository,
3440
+ repository.spaceUid ?? `space:agent:${agentId}`,
3441
+ agentId
3442
+ );
3443
+ if (anchored?.uid) {
3444
+ names[anchored.uid] = repository.repoId;
3445
+ fresh[repository.repoId] = { uid: anchored.uid, at: stamp };
3446
+ return anchored.uid;
3447
+ }
3448
+ failures.push(
3449
+ `${repository.repoId}: anchor ${anchored?.status ?? "failed"}`
3450
+ );
3451
+ return void 0;
3452
+ }
3453
+ failures.push(
3454
+ `${repository.repoId}: ${typeof payload?.status === "string" && payload.status ? payload.status : "unresolved"}`
3455
+ );
3456
+ return void 0;
3457
+ } catch (error2) {
3458
+ failures.push(
3459
+ `${repository.repoId}: ${error2?.message || "resolve failed"}`
3460
+ );
3461
+ return void 0;
3462
+ }
3463
+ })
3464
+ );
3465
+ if (Object.keys(fresh).length > 0) {
3466
+ writeScopeCache(options.cache, { ...cached, ...fresh });
3267
3467
  }
3268
- sections.push(...policySections(payload, state));
3269
- sections.push(...hiddenSections(payload));
3270
- if (sections.length === 0) {
3271
- return "No results found for this search.";
3468
+ return {
3469
+ uids: [...new Set(resolved.filter((uid) => Boolean(uid)))],
3470
+ names,
3471
+ failures
3472
+ };
3473
+ }
3474
+ var import_node_fs3, import_node_path3, WORKSPACE_RESUME_LIMIT, SCOPE_CACHE_TTL_MS, SCOPE_CACHE_FILE;
3475
+ var init_workspace_scope = __esm({
3476
+ "src/workspace-scope.ts"() {
3477
+ "use strict";
3478
+ import_node_fs3 = __toESM(require("fs"));
3479
+ import_node_path3 = __toESM(require("path"));
3480
+ init_codegraph();
3481
+ init_code_tool();
3482
+ WORKSPACE_RESUME_LIMIT = 9;
3483
+ SCOPE_CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
3484
+ SCOPE_CACHE_FILE = "repository-scopes.json";
3272
3485
  }
3273
- if (nodes.length === 0 && articles.length === 0 && chunks.length === 0) {
3274
- return assemble(
3275
- "# Retrieved context\n\nNo graph results matched this search.",
3276
- sections,
3277
- state,
3278
- maxChars
3279
- );
3486
+ });
3487
+
3488
+ // src/call-ledger.ts
3489
+ function callsDir(options) {
3490
+ return import_node_path4.default.join(
3491
+ options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path4.default.join(import_node_os3.default.homedir(), ".mindgraph", "runtime"),
3492
+ "calls"
3493
+ );
3494
+ }
3495
+ function stableJson(value) {
3496
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
3497
+ if (value && typeof value === "object") {
3498
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
3280
3499
  }
3281
- return assemble("# Retrieved context", sections, state, maxChars);
3500
+ return JSON.stringify(value) ?? "null";
3282
3501
  }
3283
- function resumeEntry(raw, state, fallbackType) {
3284
- const wrapper = obj(raw);
3285
- if (!wrapper) return void 0;
3286
- const value = obj(wrapper.node) ?? wrapper;
3287
- if (isNodeShaped(value)) return nodeLines(value, state);
3288
- const uid = str2(value.uid);
3289
- const props = obj(value.props);
3290
- const label = clip(value.label, LABEL_MAX, state) ?? clip(value.name, LABEL_MAX, state) ?? clip(value.title, LABEL_MAX, state) ?? clip(value.action, LABEL_MAX, state) ?? fallbackType;
3291
- const status = str2(value.status) ?? str2(props?.status);
3292
- const summary = clip(value.summary, SUMMARY_MAX, state) ?? clip(value.description, SUMMARY_MAX, state) ?? clip(value.content, SUMMARY_MAX, state) ?? clip(value.outcome_summary, SUMMARY_MAX, state) ?? clip(props?.summary, SUMMARY_MAX, state);
3293
- const details = [
3294
- status ? `status ${status}` : void 0,
3295
- str2(value.path) ? `path ${str2(value.path)}` : void 0,
3296
- str2(value.symbol) ? `symbol ${str2(value.symbol)}` : void 0
3297
- ].filter(Boolean);
3298
- const head = `- **${label}**${uid ? ` [${uid}]` : ""}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
3299
- return summary && summary !== label ? `${head}: ${summary}` : head;
3502
+ function bareToolName(name) {
3503
+ const cut = name.lastIndexOf("__");
3504
+ return cut === -1 ? name : name.slice(cut + 2);
3300
3505
  }
3301
- function resumeSection(heading, values, state, fallbackType) {
3302
- const items = values.map((value) => resumeEntry(value, state, fallbackType)).filter((value) => Boolean(value));
3303
- return items.length > 0 ? { heading, items } : void 0;
3506
+ function callKey(toolName, toolInput) {
3507
+ return import_node_crypto4.default.createHash("sha256").update(`${bareToolName(toolName)}\0${stableJson(toolInput)}`).digest("hex");
3304
3508
  }
3305
- function renderResumeContext(response, maxChars) {
3306
- const payload = obj(response);
3307
- if (!payload) return void 0;
3308
- const state = { bounded: false, omitted: 0 };
3309
- const status = str2(payload.status);
3310
- if (!payload.task && status === "no_eligible_work") {
3311
- const scopedOut = Array.isArray(payload.scoped_out_tasks) ? payload.scoped_out_tasks.length : 0;
3312
- const note = scopedOut > 0 ? ` ${scopedOut} task(s) exist outside the requested scope.` : "";
3313
- return assemble(
3314
- "# Resume context",
3315
- [{ heading: `No eligible work was found in the requested scope.${note}`, items: [] }],
3316
- state,
3317
- maxChars
3318
- );
3509
+ function sessionKey(sessionId) {
3510
+ return import_node_crypto4.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
3511
+ }
3512
+ function prune(dir, now) {
3513
+ let names;
3514
+ try {
3515
+ names = import_node_fs4.default.readdirSync(dir);
3516
+ } catch {
3517
+ return;
3319
3518
  }
3320
- const sections = [];
3321
- const push = (section) => {
3322
- if (section) sections.push(section);
3323
- };
3324
- push(resumeSection("## Current task", [payload.task], state, "Task"));
3325
- push(
3326
- resumeSection(
3327
- "## Goal and project",
3328
- [payload.goal, payload.project].filter(Boolean),
3329
- state,
3330
- "Scope"
3331
- )
3332
- );
3333
- push(resumeSection("## Plan", [payload.plan], state, "Plan"));
3334
- push(
3335
- resumeSection(
3336
- "## Next steps",
3337
- Array.isArray(payload.next_steps) ? payload.next_steps : [],
3338
- state,
3339
- "Next step"
3340
- )
3341
- );
3342
- push(
3343
- resumeSection(
3344
- "## Blockers",
3345
- Array.isArray(payload.blockers) ? payload.blockers : [],
3346
- state,
3347
- "Blocker"
3348
- )
3349
- );
3350
- const knowledge = obj(payload.knowledge);
3351
- push(
3352
- resumeSection(
3353
- "## Relevant lessons and risks",
3354
- [
3355
- ...Array.isArray(knowledge?.lessons) ? knowledge.lessons : [],
3356
- ...Array.isArray(knowledge?.risks) ? knowledge.risks : []
3357
- ],
3358
- state,
3359
- "Knowledge"
3360
- )
3361
- );
3362
- push(
3363
- resumeSection(
3364
- "## Active execution",
3365
- [payload.active_execution].filter(Boolean),
3366
- state,
3367
- "Execution"
3368
- )
3369
- );
3370
- push(
3371
- resumeSection(
3372
- "## Recent executions",
3373
- Array.isArray(payload.recent_executions) ? payload.recent_executions : [],
3374
- state,
3375
- "Execution"
3376
- )
3377
- );
3378
- push(
3379
- resumeSection(
3380
- "## Code targets",
3381
- Array.isArray(payload.code_targets) ? payload.code_targets : [],
3382
- state,
3383
- "Code target"
3384
- )
3385
- );
3386
- push(resumeSection("## Lease", [payload.lease].filter(Boolean), state, "Lease"));
3387
- const warning = clip(payload.warning, SUMMARY_MAX, state);
3388
- if (warning) sections.push({ heading: "## Warning", items: [`- ${warning}`] });
3389
- const selectionReason = clip(payload.selection_reason, SUMMARY_MAX, state);
3390
- if (selectionReason) {
3391
- sections.push({ heading: "## Selection", items: [`- ${selectionReason}`] });
3519
+ for (const name of names) {
3520
+ const file = import_node_path4.default.join(dir, name);
3521
+ try {
3522
+ if (now - import_node_fs4.default.statSync(file).mtimeMs > TTL_MS) import_node_fs4.default.unlinkSync(file);
3523
+ } catch {
3524
+ }
3392
3525
  }
3393
- if (sections.length === 0) return void 0;
3394
- return assemble("# Resume context", sections, state, maxChars);
3395
3526
  }
3396
- function renderNodeList(response, title, limit, maxChars) {
3397
- let candidates = [];
3398
- if (Array.isArray(response)) {
3399
- candidates = response;
3400
- } else {
3401
- const payload = obj(response);
3402
- if (!payload) return void 0;
3403
- for (const key of ["nodes", "results", "items", "goals", "questions", "claims", "preferences", "documents"]) {
3404
- const candidate = payload[key];
3405
- if (Array.isArray(candidate) && candidate.some((entry) => unwrapSearchResult(entry))) {
3406
- candidates = candidate;
3407
- break;
3527
+ function recordPendingCall(toolName, toolInput, harnessSessionId, fills, options = {}) {
3528
+ const now = (options.now ?? Date.now)();
3529
+ const dir = callsDir(options);
3530
+ try {
3531
+ import_node_fs4.default.mkdirSync(dir, { recursive: true, mode: 448 });
3532
+ prune(dir, now);
3533
+ const record2 = {
3534
+ harnessSessionId,
3535
+ projectDir: options.projectDir ?? process.env.CLAUDE_PROJECT_DIR,
3536
+ fills,
3537
+ createdAt: now
3538
+ };
3539
+ const file = import_node_path4.default.join(
3540
+ dir,
3541
+ `${callKey(toolName, toolInput)}.${sessionKey(harnessSessionId)}.json`
3542
+ );
3543
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
3544
+ import_node_fs4.default.writeFileSync(temporary, JSON.stringify(record2), { mode: 384 });
3545
+ import_node_fs4.default.renameSync(temporary, file);
3546
+ } catch {
3547
+ }
3548
+ }
3549
+ function resolvePendingCall(toolName, toolInput, options = {}) {
3550
+ const now = (options.now ?? Date.now)();
3551
+ const dir = callsDir(options);
3552
+ const prefix = `${callKey(toolName, toolInput)}.`;
3553
+ let names;
3554
+ try {
3555
+ names = import_node_fs4.default.readdirSync(dir).filter((name) => name.startsWith(prefix) && name.endsWith(".json"));
3556
+ } catch {
3557
+ return void 0;
3558
+ }
3559
+ let matches = [];
3560
+ for (const name of names) {
3561
+ try {
3562
+ const record2 = JSON.parse(
3563
+ import_node_fs4.default.readFileSync(import_node_path4.default.join(dir, name), "utf8")
3564
+ );
3565
+ if (typeof record2.createdAt === "number" && now - record2.createdAt <= TTL_MS && record2.fills && typeof record2.fills === "object") {
3566
+ matches.push(record2);
3408
3567
  }
3568
+ } catch {
3409
3569
  }
3410
3570
  }
3411
- let nodes = candidates.map(unwrapSearchResult).filter((node) => Boolean(node));
3412
- if (nodes.length === 0) return void 0;
3413
- const total = nodes.length;
3414
- const state = { bounded: false, omitted: 0 };
3415
- if (limit !== void 0 && limit > 0 && nodes.length > limit) {
3416
- state.omitted += nodes.length - limit;
3417
- nodes = nodes.slice(0, limit);
3418
- state.bounded = true;
3571
+ if (matches.length > 1) {
3572
+ const projectDir = options.projectDir ?? process.env.CLAUDE_PROJECT_DIR;
3573
+ matches = projectDir ? matches.filter((record2) => record2.projectDir === projectDir) : [];
3419
3574
  }
3420
- const sections = groupedNodeSections(nodes, state);
3421
- const shown = nodes.length;
3422
- return assemble(
3423
- `# ${title} (${shown === total ? total : `${shown} of ${total}`})`,
3424
- sections,
3425
- state,
3426
- maxChars
3575
+ return matches.length === 1 ? matches[0].fills : void 0;
3576
+ }
3577
+ var import_node_crypto4, import_node_fs4, import_node_os3, import_node_path4, TTL_MS;
3578
+ var init_call_ledger = __esm({
3579
+ "src/call-ledger.ts"() {
3580
+ "use strict";
3581
+ import_node_crypto4 = __toESM(require("crypto"));
3582
+ import_node_fs4 = __toESM(require("fs"));
3583
+ import_node_os3 = __toESM(require("os"));
3584
+ import_node_path4 = __toESM(require("path"));
3585
+ TTL_MS = 10 * 6e4;
3586
+ }
3587
+ });
3588
+
3589
+ // src/hook-installer.ts
3590
+ function storedFeatures(homeDir) {
3591
+ const { promptRecall: promptRecall2, capture } = loadHookEnv(
3592
+ import_node_path6.default.join(homeDir || import_node_os5.default.homedir(), ".mindgraph")
3427
3593
  );
3594
+ return { promptRecall: promptRecall2, capture };
3595
+ }
3596
+ function command(harness) {
3597
+ return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
3598
+ }
3599
+ function windowsCommand(harness) {
3600
+ return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
3601
+ }
3602
+ function installHookRunner(sourceFile, homeDir, version) {
3603
+ const target = import_node_path6.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_RELATIVE);
3604
+ import_node_fs7.default.mkdirSync(import_node_path6.default.dirname(target), { recursive: true });
3605
+ const temporary = `${target}.${process.pid}.${import_node_crypto6.default.randomUUID()}.tmp`;
3606
+ import_node_fs7.default.copyFileSync(sourceFile, temporary);
3607
+ import_node_fs7.default.renameSync(temporary, target);
3608
+ const versionPath = import_node_path6.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_VERSION_RELATIVE);
3609
+ if (version) {
3610
+ import_node_fs7.default.writeFileSync(versionPath, `${version}
3611
+ `);
3612
+ } else {
3613
+ import_node_fs7.default.rmSync(versionPath, { force: true });
3614
+ }
3615
+ return target;
3616
+ }
3617
+ function installedHookRunnerVersion(homeDir) {
3618
+ try {
3619
+ const raw = import_node_fs7.default.readFileSync(
3620
+ import_node_path6.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_VERSION_RELATIVE),
3621
+ "utf8"
3622
+ ).trim();
3623
+ return raw || void 0;
3624
+ } catch {
3625
+ return void 0;
3626
+ }
3627
+ }
3628
+ function parseSemver(value) {
3629
+ if (!value) return void 0;
3630
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
3631
+ if (!match) return void 0;
3632
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
3428
3633
  }
3429
- function renderTraversal(response, maxChars) {
3430
- const directSteps = Array.isArray(response) ? response : void 0;
3431
- const payload = obj(response);
3432
- if (!payload && !directSteps) return void 0;
3433
- const body = payload ?? {};
3434
- const container = obj(body.graph) ?? body;
3435
- const rawNodes = Array.isArray(container.nodes) ? container.nodes : [];
3436
- const nodes = rawNodes.filter(isNodeShaped);
3437
- const paths = Array.isArray(body.paths) ? body.paths : [];
3438
- const hasSteps = directSteps !== void 0 || Object.hasOwn(body, "steps");
3439
- const rawSteps = directSteps ?? (Array.isArray(body.steps) ? body.steps : []);
3440
- if (nodes.length === 0 && paths.length === 0 && !hasSteps) return void 0;
3441
- const state = { bounded: false, omitted: 0 };
3442
- const sections = [];
3443
- const labelByUid = /* @__PURE__ */ new Map();
3444
- for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3445
- if (paths.length > 0) {
3446
- const items = paths.map((raw) => obj(raw)).filter((path9) => Boolean(path9)).map((path9, index) => {
3447
- const uids = Array.isArray(path9.node_uids) ? path9.node_uids.map(String) : [];
3448
- const labels = Array.isArray(path9.labels) ? path9.labels.map(String) : Array.isArray(path9.nodes) ? path9.nodes.map((hop) => {
3449
- const hopNode = obj(hop);
3450
- return str2(hopNode?.label) || labelByUid.get(str2(hopNode?.uid) || "") || str2(hop) || "?";
3451
- }) : [];
3452
- const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
3453
- const cost = num(path9.cost) ?? num(path9.path_cost);
3454
- const confidence = num(path9.path_confidence);
3455
- const meta = [
3456
- cost !== void 0 ? `cost ${cost}` : void 0,
3457
- confidence !== void 0 ? `confidence ${confidence}` : void 0
3458
- ].filter(Boolean);
3459
- const head = `${index + 1}. ${route}${meta.length > 0 ? ` (${meta.join(", ")})` : ""}`;
3460
- return labels.length > 0 && uids.length > 0 ? `${head}
3461
- uids: ${uids.join(" \u2192 ")}` : head;
3462
- });
3463
- sections.push({ heading: "## Paths", items });
3634
+ function semverNewer(a, b) {
3635
+ for (let index = 0; index < 3; index += 1) {
3636
+ if (a[index] !== b[index]) return a[index] > b[index];
3464
3637
  }
3465
- if (hasSteps) {
3466
- const stepItems = rawSteps.map((raw) => obj(raw)).filter((step) => Boolean(step)).map((step) => {
3467
- const depth = num(step.depth);
3468
- const uid = str2(step.node_uid) ?? str2(step.uid) ?? str2(step.edge_to_uid) ?? "?";
3469
- const parent = str2(step.parent_uid) ?? str2(step.edge_from_uid);
3470
- const label = str2(step.label) || "(unlabeled)";
3471
- const relation = edgeTypeName(step.edge_type);
3472
- const role = str2(step.traversal_role);
3473
- const type = str2(step.node_type) ?? str2(obj(step.node_type)?.Custom);
3474
- const cost = num(step.path_cost);
3475
- const confidence = num(step.path_confidence);
3476
- const prefix = [
3477
- depth !== void 0 ? `depth ${depth}` : void 0,
3478
- role,
3479
- relation ? `\u2014${relation}\u2192` : void 0
3480
- ].filter(Boolean).join(" ");
3481
- const meta = [
3482
- type,
3483
- parent ? `parent ${parent}` : void 0,
3484
- cost !== void 0 ? `cost ${cost}` : void 0,
3485
- confidence !== void 0 ? `confidence ${confidence}` : void 0
3486
- ].filter(Boolean).join(", ");
3487
- return `- ${prefix ? `${prefix}: ` : ""}**${label}** [${uid}]${meta ? ` (${meta})` : ""}`;
3488
- });
3489
- if (stepItems.length > 0) {
3490
- sections.push({ heading: "## Traversal steps", items: stepItems });
3491
- } else {
3492
- const start = str2(body.start_uid) || "?";
3493
- const end = str2(body.end_uid);
3494
- const mode2 = str2(body.mode);
3495
- sections.push({
3496
- heading: mode2 === "path" && end ? `No path found from [${start}] to [${end}].` : `No traversal steps returned from [${start}].`,
3497
- items: []
3498
- });
3638
+ return false;
3639
+ }
3640
+ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
3641
+ try {
3642
+ if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
3643
+ return { refreshed: false, reason: "disabled" };
3644
+ }
3645
+ const home = homeDir || import_node_os5.default.homedir();
3646
+ if (!import_node_fs7.default.existsSync(import_node_path6.default.join(home, RUNNER_RELATIVE))) {
3647
+ return { refreshed: false, reason: "no_runner" };
3648
+ }
3649
+ if (!import_node_fs7.default.existsSync(sourceBundle)) {
3650
+ return { refreshed: false, reason: "no_source" };
3651
+ }
3652
+ const server = parseSemver(serverVersion);
3653
+ if (!server) return { refreshed: false, reason: "unparseable" };
3654
+ const runnerVersion = installedHookRunnerVersion(home);
3655
+ if (runnerVersion !== void 0) {
3656
+ const pinned = parseSemver(runnerVersion);
3657
+ if (!pinned) {
3658
+ return { refreshed: false, reason: "unparseable", runnerVersion };
3659
+ }
3660
+ if (!semverNewer(server, pinned)) {
3661
+ return {
3662
+ refreshed: false,
3663
+ reason: runnerVersion === serverVersion ? "current" : "downgrade",
3664
+ runnerVersion
3665
+ };
3666
+ }
3499
3667
  }
3668
+ installHookRunner(sourceBundle, home, serverVersion);
3669
+ return { refreshed: true, reason: "updated", runnerVersion };
3670
+ } catch {
3671
+ return { refreshed: false, reason: "error" };
3500
3672
  }
3501
- if (nodes.length > 0) {
3502
- sections.push(...groupedNodeSections(nodes, state, "## Nodes\n\n"));
3503
- const relationships = relationshipItems(
3504
- Array.isArray(container.edges) ? container.edges : [],
3505
- labelByUid,
3506
- state
3507
- );
3508
- if (relationships.length > 0) {
3509
- sections.push({ heading: "## Relationships", items: relationships });
3673
+ }
3674
+ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
3675
+ const refreshed = [];
3676
+ const scopes = ["user", "project"];
3677
+ for (const scope of scopes) {
3678
+ try {
3679
+ const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
3680
+ if (!import_node_fs7.default.existsSync(file)) continue;
3681
+ if (!import_node_fs7.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
3682
+ installHooks(
3683
+ "claude-code",
3684
+ scope,
3685
+ projectDir,
3686
+ void 0,
3687
+ homeDir,
3688
+ storedFeatures(homeDir)
3689
+ );
3690
+ refreshed.push(file);
3691
+ } catch {
3510
3692
  }
3511
3693
  }
3512
- const mode = str2(body.mode);
3513
- return assemble(
3514
- `# Graph traversal${mode ? ` (${mode})` : ""}`,
3515
- sections,
3516
- state,
3517
- maxChars
3518
- );
3694
+ return refreshed;
3519
3695
  }
3520
- function neighborLine(value) {
3521
- const node = obj(value);
3522
- if (!node) return void 0;
3523
- const label = str2(node.label) || "(unlabeled)";
3524
- const uid = str2(node.uid);
3525
- const type = str2(node.node_type);
3526
- return `- ${label}${uid ? ` [${uid}]` : ""}${type ? ` (${type})` : ""}`;
3696
+ function versionSkewNote(serverVersion, homeDir) {
3697
+ const runnerVersion = installedHookRunnerVersion(homeDir);
3698
+ if (!runnerVersion || runnerVersion === serverVersion) return void 0;
3699
+ return `NOTE: the installed MindGraph hook runner is v${runnerVersion} but this MCP server is v${serverVersion} \u2014 hook behavior and tool contracts may be out of sync. Tell the user to run: npx -y mindgraph-mcp@latest install-hooks (refreshes the pinned runner).`;
3527
3700
  }
3528
- function renderOntologyAnswer(response) {
3529
- const payload = obj(response);
3530
- if (!payload) return void 0;
3531
- const objects = Array.isArray(payload.objects) ? payload.objects.filter(isNodeShaped) : isNodeShaped(payload.object) ? [payload.object] : [];
3532
- const cognitive = obj(payload.cognitive_context);
3533
- const provenance = Array.isArray(payload.provenance) ? payload.provenance : [];
3534
- const hasQuerySignature = Array.isArray(payload.objects) || obj(payload.confidence) !== void 0;
3535
- const cognitiveEmpty = !cognitive || Object.values(cognitive).every((v) => !Array.isArray(v) || v.length === 0);
3536
- if (objects.length === 0 && cognitiveEmpty && provenance.length === 0) {
3537
- return hasQuerySignature ? "No matching domain objects." : void 0;
3538
- }
3539
- const state = { bounded: false, omitted: 0 };
3540
- const sections = [];
3541
- if (objects.length > 0) {
3542
- sections.push(...groupedNodeSections(objects, state));
3701
+ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path6.default.join(import_node_os5.default.homedir(), ".codex"), homeDir) {
3702
+ if (harness === "codex") {
3703
+ return scope === "user" ? import_node_path6.default.join(codexHomeDir, "hooks.json") : import_node_path6.default.join(projectDir, ".codex", "hooks.json");
3543
3704
  }
3544
- const labelByUid = /* @__PURE__ */ new Map();
3545
- for (const node of objects) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3546
- const graph = obj(payload.graph);
3547
- for (const raw of Array.isArray(graph?.nodes) ? graph.nodes : []) {
3548
- const node = obj(raw);
3549
- const uid = str2(node?.uid);
3550
- if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str2(node?.label) || "?");
3705
+ return scope === "user" ? import_node_path6.default.join(homeDir || import_node_os5.default.homedir(), ".claude", "settings.json") : import_node_path6.default.join(projectDir, ".claude", "settings.json");
3706
+ }
3707
+ function readSettings(file) {
3708
+ if (!import_node_fs7.default.existsSync(file)) return {};
3709
+ return JSON.parse(import_node_fs7.default.readFileSync(file, "utf8"));
3710
+ }
3711
+ function ownedHook(harness, timeout, additionalContextLimit) {
3712
+ return {
3713
+ type: "command",
3714
+ command: command(harness),
3715
+ ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
3716
+ timeout,
3717
+ statusMessage: "Syncing MindGraph session context",
3718
+ ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
3719
+ };
3720
+ }
3721
+ function desiredEntries(harness, features = {}) {
3722
+ const common = {
3723
+ SessionStart: [
3724
+ {
3725
+ matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
3726
+ // B7 live acceptance caught cold SessionStart calls crossing the
3727
+ // inherited 20s edge in both harnesses: the hooks correctly failed
3728
+ // open, but no brief reached the model. The same installed runner
3729
+ // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
3730
+ // The declared Codex context limit must not undercut the brief's own
3731
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
3732
+ // truncation the renderer knew nothing about.
3733
+ hooks: [
3734
+ ownedHook(
3735
+ harness,
3736
+ 30,
3737
+ harness === "codex" ? 1e4 : void 0
3738
+ )
3739
+ ]
3740
+ }
3741
+ ],
3742
+ PreToolUse: [
3743
+ {
3744
+ matcher: "mcp__mindgraph__.*",
3745
+ // Repository resolution over a large multi-repo workspace plus three
3746
+ // git probes can exceed 3s on the first (uncached) call; a killed
3747
+ // PreToolUse means the call goes through untagged.
3748
+ hooks: [ownedHook(harness, 5)]
3749
+ }
3750
+ ],
3751
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
3752
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
3753
+ // Claude Code: only the tools the hook acts on (MindGraph calls, edits,
3754
+ // and the shell/agent calls that keep a lease alive). Read, Grep and Glob
3755
+ // are most of a session's tool calls and the hook did nothing useful for
3756
+ // them but start a process (roadmap H8). Codex names its tools
3757
+ // differently, so it keeps the catch-all.
3758
+ PostToolUse: [
3759
+ {
3760
+ matcher: harness === "claude-code" ? CLAUDE_POST_TOOL_MATCHER : ".*",
3761
+ hooks: [ownedHook(harness, 10)]
3762
+ }
3763
+ ],
3764
+ Stop: [{ hooks: [ownedHook(harness, 5)] }],
3765
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
3766
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
3767
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
3768
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
3769
+ // map), so the codex adapter instead makes at most ONE call there.
3770
+ SessionEnd: [
3771
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
3772
+ ]
3773
+ };
3774
+ if (harness !== "claude-code") return common;
3775
+ return {
3776
+ ...common,
3777
+ TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
3778
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }],
3779
+ // Bounded recall per prompt: the core gives up at 4s and fails open, so
3780
+ // a 5s budget never holds the prompt longer than that.
3781
+ ...features.promptRecall !== false ? { UserPromptSubmit: [{ hooks: [ownedHook(harness, 5)] }] } : {},
3782
+ // Opt-in transcript capture before compaction (cold-tenant ingest).
3783
+ ...features.capture === true ? { PreCompact: [{ hooks: [ownedHook(harness, 30)] }] } : {}
3784
+ };
3785
+ }
3786
+ function hookCommand(value) {
3787
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3788
+ const command2 = value.command;
3789
+ return typeof command2 === "string" ? command2 : void 0;
3790
+ }
3791
+ function entryHasOwnedHook(value) {
3792
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3793
+ const hooks = value.hooks;
3794
+ return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
3795
+ }
3796
+ function writeSettings(file, settings) {
3797
+ import_node_fs7.default.mkdirSync(import_node_path6.default.dirname(file), { recursive: true });
3798
+ const temporary = `${file}.${process.pid}.${import_node_crypto6.default.randomUUID()}.tmp`;
3799
+ import_node_fs7.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
3800
+ `, {
3801
+ mode: 384
3802
+ });
3803
+ import_node_fs7.default.renameSync(temporary, file);
3804
+ }
3805
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir, features = {}) {
3806
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
3807
+ const settings = readSettings(file);
3808
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
3809
+ let added = 0;
3810
+ let updated = 0;
3811
+ const desired = desiredEntries(harness, features);
3812
+ for (const optional of ["UserPromptSubmit", "PreCompact"]) {
3813
+ if (optional in desired || !Array.isArray(hooks[optional])) continue;
3814
+ const kept = hooks[optional].filter(
3815
+ (entry) => !entryHasOwnedHook(entry)
3816
+ );
3817
+ if (kept.length > 0) hooks[optional] = kept;
3818
+ else delete hooks[optional];
3551
3819
  }
3552
- if (cognitive) {
3553
- for (const rawNodes of Object.values(cognitive)) {
3554
- if (!Array.isArray(rawNodes)) continue;
3555
- for (const raw of rawNodes) {
3556
- const node = obj(raw);
3557
- const uid = str2(node?.uid);
3558
- if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str2(node?.label) || "?");
3559
- }
3820
+ for (const [event, wanted] of Object.entries(desired)) {
3821
+ const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
3822
+ const kept = current.filter((entry) => !entryHasOwnedHook(entry));
3823
+ const hadOwned = kept.length !== current.length;
3824
+ for (const entry of wanted) {
3825
+ kept.push(entry);
3826
+ if (hadOwned) updated += 1;
3827
+ else added += 1;
3560
3828
  }
3829
+ hooks[event] = kept;
3561
3830
  }
3562
- const relations = Array.isArray(payload.relations) ? payload.relations : [];
3563
- const relationLines = relationshipItems(relations, labelByUid, state, { fallbackToUid: true });
3564
- if (relationLines.length > 0) {
3565
- sections.push({ heading: "## Relations", items: relationLines });
3566
- }
3567
- if (cognitive) {
3568
- for (const [category, rawNodes] of Object.entries(cognitive)) {
3569
- if (!Array.isArray(rawNodes) || rawNodes.length === 0) continue;
3570
- const items = rawNodes.map(neighborLine).filter((line) => Boolean(line));
3571
- if (items.length > 0) {
3572
- sections.push({ heading: `### Cognitive context \u2014 ${category}`, items });
3831
+ settings.hooks = hooks;
3832
+ writeSettings(file, settings);
3833
+ return { path: file, added, updated };
3834
+ }
3835
+ function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
3836
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir);
3837
+ if (!import_node_fs7.default.existsSync(file)) return { path: file, removed: 0 };
3838
+ const settings = readSettings(file);
3839
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
3840
+ let removed = 0;
3841
+ for (const [event, value] of Object.entries(hooks)) {
3842
+ if (!Array.isArray(value)) continue;
3843
+ const retainedEntries = [];
3844
+ for (const rawEntry of value) {
3845
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
3846
+ retainedEntries.push(rawEntry);
3847
+ continue;
3848
+ }
3849
+ const entry = { ...rawEntry };
3850
+ if (!Array.isArray(entry.hooks)) {
3851
+ retainedEntries.push(entry);
3852
+ continue;
3853
+ }
3854
+ const retainedHooks = entry.hooks.filter((hook) => {
3855
+ const owned = hookCommand(hook)?.includes(OWNER_MARKER) || false;
3856
+ if (owned) removed += 1;
3857
+ return !owned;
3858
+ });
3859
+ if (retainedHooks.length > 0) {
3860
+ entry.hooks = retainedHooks;
3861
+ retainedEntries.push(entry);
3573
3862
  }
3574
3863
  }
3864
+ if (retainedEntries.length > 0) hooks[event] = retainedEntries;
3865
+ else delete hooks[event];
3575
3866
  }
3576
- if (provenance.length > 0) {
3577
- const items = provenance.map((raw) => obj(raw)).filter((entry) => Boolean(entry)).map((entry) => {
3578
- const span = clip(entry.text_span, QUOTE_MAX, state) || "";
3579
- const by = str2(entry.ingested_by_name);
3580
- const suffix = `${by ? ` (ingested by ${by})` : ""} [source ${str2(entry.source_uid) || "?"}]`;
3581
- return str2(entry.span_kind) === "anchor" ? `- "${span}"${suffix}` : `- context: ${span}${suffix}`;
3582
- });
3583
- sections.push({ heading: "## Sources", items });
3584
- }
3585
- const confidence = num(obj(payload.confidence)?.overall);
3586
- const notes = [];
3587
- if (confidence !== void 0) notes.push(`overall confidence ${confidence}`);
3588
- if (payload.has_more === true) notes.push("more results exist (has_more)");
3589
- if (payload.seed_cap_hit === true) notes.push("seed cap hit \u2014 narrow the query for a complete set");
3590
- if (notes.length > 0) sections.push({ heading: `Note: ${notes.join("; ")}.`, items: [] });
3591
- return assemble(
3592
- `# Ontology results (${objects.length} object${objects.length === 1 ? "" : "s"})`,
3593
- sections,
3594
- state
3595
- );
3867
+ settings.hooks = hooks;
3868
+ writeSettings(file, settings);
3869
+ return { path: file, removed };
3596
3870
  }
3597
- function renderObjectList(response, title) {
3598
- const payload = obj(response);
3599
- if (!payload) return void 0;
3600
- const rawItems = Array.isArray(payload.items) ? payload.items : Array.isArray(payload.rows) && payload.rows.some((row) => unwrapSearchResult(row)) ? payload.rows : void 0;
3601
- const items = (rawItems ?? []).map(unwrapSearchResult).filter((node) => Boolean(node));
3602
- if (items.length === 0) {
3603
- return rawItems ? `# ${title} (0 results)` : void 0;
3604
- }
3605
- const state = { bounded: false, omitted: 0 };
3606
- const sections = groupedNodeSections(items, state);
3607
- const total = num(payload.total_count);
3608
- const truncations = Array.isArray(payload.truncation_reasons) ? payload.truncation_reasons.filter((reason) => typeof reason === "string") : [];
3609
- const notes = [];
3610
- if (payload.has_more === true) notes.push("more results exist");
3611
- if (truncations.length > 0) notes.push(`truncated: ${truncations.join(", ")}`);
3612
- if (notes.length > 0) sections.push({ heading: `Note: ${notes.join("; ")}.`, items: [] });
3613
- const count = total !== void 0 && total !== items.length ? `${items.length} of ${total}` : `${items.length}`;
3614
- return assemble(`# ${title} (${count})`, sections, state);
3871
+ function installClaudeHooks(scope, projectDir = process.cwd(), features = {}) {
3872
+ return installHooks("claude-code", scope, projectDir, void 0, void 0, features);
3615
3873
  }
3616
- function renderJobs(response) {
3617
- const jobs = Array.isArray(response) ? response : obj(response)?.jobs;
3618
- if (!Array.isArray(jobs)) return void 0;
3619
- if (jobs.length === 0) return "No ingestion jobs.";
3620
- const state = { bounded: false, omitted: 0 };
3621
- const sorted = jobs.map((raw) => obj(raw)).filter((job) => Boolean(job)).sort((a, b) => (num(b.created_at) ?? 0) - (num(a.created_at) ?? 0));
3622
- const shown = sorted.slice(0, JOBS_RENDER_MAX);
3623
- if (shown.length < sorted.length) state.bounded = true;
3624
- const items = shown.map((job) => {
3625
- const progress = obj(job.progress);
3626
- const failure = obj(job.error_details);
3627
- const message = str2(failure?.message) ?? str2(job.error);
3628
- const done = num(progress?.processed_chunks);
3629
- const totalChunks = num(progress?.total_chunks);
3630
- const bits = [
3631
- str2(job.status) || "?",
3632
- done !== void 0 && totalChunks !== void 0 ? `${done}/${totalChunks} chunks` : void 0,
3633
- num(progress?.nodes_created) !== void 0 ? `${num(progress?.nodes_created)} nodes` : void 0,
3634
- num(job.queue_position) !== void 0 ? `queue #${num(job.queue_position)}` : void 0,
3635
- message ? `error: ${clip(message, 160, state)}` : void 0,
3636
- str2(failure?.code) ? `code: ${clip(failure?.code, 80, state)}` : void 0,
3637
- num(failure?.status) !== void 0 ? `status: ${num(failure?.status)}` : void 0,
3638
- typeof failure?.retriable === "boolean" ? `retriable: ${failure.retriable}` : void 0
3639
- ].filter(Boolean);
3640
- return `- [${str2(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
3641
- });
3642
- return assemble(
3643
- `# Ingestion jobs (${shown.length}${shown.length < sorted.length ? ` of ${sorted.length}` : ""})`,
3644
- [{ heading: "", items }],
3645
- state
3646
- );
3874
+ function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
3875
+ return uninstallHooks("claude-code", scope, projectDir);
3647
3876
  }
3648
- function renderSignals(response) {
3649
- const payload = obj(response);
3650
- if (!payload) return void 0;
3651
- const state = { bounded: false, omitted: 0 };
3652
- const sections = [];
3653
- for (const [key, value] of Object.entries(payload)) {
3654
- if (!Array.isArray(value) || value.length === 0) continue;
3655
- const items = value.map((raw) => obj(raw)).filter((item) => Boolean(item)).map((item) => {
3656
- const fromLabel = str2(item.from_label);
3657
- const toLabel = str2(item.to_label);
3658
- const anyLabelKey = Object.keys(item).find(
3659
- (k) => k.endsWith("_label") && str2(item[k])
3660
- );
3661
- const label = str2(item.label) ?? str2(item.title) ?? str2(item.name) ?? (fromLabel && toLabel ? `${fromLabel} \u2194 ${toLabel}` : void 0) ?? (anyLabelKey ? str2(item[anyLabelKey]) : void 0) ?? str2(item.uid) ?? str2(item.target_uid) ?? "(item)";
3662
- const uid = str2(item.uid) ?? str2(item.target_uid);
3663
- const numbers = Object.entries(item).filter(([, v]) => typeof v === "number").slice(0, 4).map(([k, v]) => `${k} ${Math.round(v * 100) / 100}`);
3664
- const lists = Object.entries(item).filter(([, v]) => Array.isArray(v) && v.length > 0).slice(0, 2).map(([k, v]) => `${k}: ${v.length}`);
3665
- const meta = [...numbers, ...lists].join(", ");
3666
- return `- ${clip(label, 160, state)}${uid && uid !== label ? ` [${uid}]` : ""}${meta ? ` (${meta})` : ""}`;
3667
- });
3668
- if (items.length > 0) {
3669
- sections.push({ heading: `## ${key.replace(/_/g, " ")}`, items });
3670
- }
3671
- }
3672
- if (sections.length === 0) return void 0;
3673
- return assemble("# Synthesis signals", sections, state);
3877
+ function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
3878
+ return installHooks("codex", scope, projectDir, codexHomeDir);
3674
3879
  }
3675
- var LABEL_MAX, SUMMARY_MAX, QUOTE_MAX, ARTICLE_MAX, CHUNK_MAX, RELATIONSHIP_MAX, RENDER_CHAR_BUDGET, MIN_RENDER_CHAR_BUDGET, BOUNDED_MARKER_RESERVE, CURATION_EDGE_TYPES, INFRA_NODE_TYPES, JOBS_RENDER_MAX;
3676
- var init_render = __esm({
3677
- "src/render.ts"() {
3880
+ function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
3881
+ return uninstallHooks("codex", scope, projectDir, codexHomeDir);
3882
+ }
3883
+ var import_node_fs7, import_node_os5, import_node_path6, import_node_crypto6, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE, CLAUDE_POST_TOOL_MATCHER;
3884
+ var init_hook_installer = __esm({
3885
+ "src/hook-installer.ts"() {
3678
3886
  "use strict";
3679
- LABEL_MAX = 200;
3680
- SUMMARY_MAX = 400;
3681
- QUOTE_MAX = 180;
3682
- ARTICLE_MAX = 6e3;
3683
- CHUNK_MAX = 6e3;
3684
- RELATIONSHIP_MAX = 40;
3685
- RENDER_CHAR_BUDGET = 24e3;
3686
- MIN_RENDER_CHAR_BUDGET = 512;
3687
- BOUNDED_MARKER_RESERVE = 220;
3688
- CURATION_EDGE_TYPES = /* @__PURE__ */ new Set(["POSSIBLE_DUPLICATE", "PossibleDuplicate"]);
3689
- INFRA_NODE_TYPES = /* @__PURE__ */ new Set(["Chunk", "Document", "Article"]);
3690
- JOBS_RENDER_MAX = 20;
3887
+ import_node_fs7 = __toESM(require("fs"));
3888
+ import_node_os5 = __toESM(require("os"));
3889
+ import_node_path6 = __toESM(require("path"));
3890
+ import_node_crypto6 = __toESM(require("crypto"));
3891
+ init_hook_env();
3892
+ OWNER_MARKER = "--owner mindgraph";
3893
+ RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
3894
+ RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
3895
+ CLAUDE_POST_TOOL_MATCHER = "mcp__mindgraph__.*|Edit|MultiEdit|Write|NotebookEdit|Bash|Task|Agent";
3691
3896
  }
3692
3897
  });
3693
3898
 
@@ -4583,7 +4788,8 @@ function compactItem(raw, fallbackType) {
4583
4788
  type: fallbackType ?? nodeType2(value),
4584
4789
  label: text(value.label) ?? text(value.document_title),
4585
4790
  truth_status: text(value.truth_status) ?? text(obj2(value.props)?.truth_status),
4586
- superseded: value.superseded === true ? true : void 0,
4791
+ // `true` from /retrieve, an object naming the replacement from /retrieve/context.
4792
+ superseded: value.superseded ? true : void 0,
4587
4793
  currently_valid: value.currently_valid === false ? false : void 0,
4588
4794
  valid_at_time: value.valid_at_time === false ? false : void 0,
4589
4795
  retrieval_origin: text(value.retrieval_origin),
@@ -4644,6 +4850,10 @@ function resumeCandidates(raw) {
4644
4850
  const item = compactItem(payload[key], key);
4645
4851
  if (item) items.push(item);
4646
4852
  }
4853
+ for (const value of Array.isArray(payload.other_eligible) ? payload.other_eligible : []) {
4854
+ const item = compactItem(value, "Task");
4855
+ if (item) items.push(item);
4856
+ }
4647
4857
  for (const key of ["next_steps", "recent_executions", "blockers", "code_targets"]) {
4648
4858
  for (const value of Array.isArray(payload[key]) ? payload[key] : []) {
4649
4859
  const item = compactItem(value, key.replace(/s$/, ""));
@@ -4688,9 +4898,9 @@ function latestTimestamp(value) {
4688
4898
  for (const item of current) visit(item);
4689
4899
  return;
4690
4900
  }
4691
- const record = obj2(current);
4692
- if (!record) return;
4693
- for (const [key, item] of Object.entries(record)) {
4901
+ const record2 = obj2(current);
4902
+ if (!record2) return;
4903
+ for (const [key, item] of Object.entries(record2)) {
4694
4904
  if (["updated_at", "created_at", "completed_at", "started_at", "observed_at"].includes(key) && typeof item === "string" && !Number.isNaN(Date.parse(item)) && (!latest || Date.parse(item) > Date.parse(latest))) {
4695
4905
  latest = item;
4696
4906
  }
@@ -4714,7 +4924,7 @@ function contextWarnings(raw, rendered2, available, returned) {
4714
4924
  message: `${Math.max(0, available - returned)} structured item(s) were omitted from the rendered context.`
4715
4925
  });
4716
4926
  }
4717
- if (/"superseded":true/.test(serialized)) {
4927
+ if (/"superseded":(?:true|\{)/.test(serialized)) {
4718
4928
  warnings.push({ code: "superseded_items_present", message: "Some retrieved items are superseded." });
4719
4929
  }
4720
4930
  if (/"currently_valid":false|"valid_at_time":false/.test(serialized)) {
@@ -4763,6 +4973,7 @@ async function handleMemoryTool(client2, args, config = {}) {
4763
4973
  let rendered2;
4764
4974
  let strategy;
4765
4975
  let candidates;
4976
+ let workspace;
4766
4977
  if (mode === "topic") {
4767
4978
  const query = text(args.query);
4768
4979
  if (!query) return error("mode='topic' requires a non-empty query of 1-3 discriminating keywords");
@@ -4785,10 +4996,27 @@ async function handleMemoryTool(client2, args, config = {}) {
4785
4996
  strategy = "retrieve_context";
4786
4997
  candidates = topicCandidates(raw);
4787
4998
  } else {
4999
+ if (args.scope !== void 0 && args.scope !== "workspace") {
5000
+ return error("scope must be 'workspace' when given");
5001
+ }
5002
+ if (args.scope === "workspace") {
5003
+ if (!config.resolveWorkspaceScope) {
5004
+ return error(
5005
+ "scope='workspace' needs the local MindGraph MCP server, which reads .mindgraph/workspace.json beside the checkout; this server has no working directory. Pass scope_uids instead."
5006
+ );
5007
+ }
5008
+ workspace = await config.resolveWorkspaceScope(agentId ?? "mcp");
5009
+ if (workspace.uids.length === 0) {
5010
+ return error(
5011
+ `scope='workspace' resolved no repositories${workspace.failures.length > 0 ? ` (${workspace.failures.join("; ")})` : ""}`
5012
+ );
5013
+ }
5014
+ }
4788
5015
  const scopeUids = Array.from(
4789
5016
  /* @__PURE__ */ new Set([
4790
5017
  ...Array.isArray(args.scope_uids) ? args.scope_uids.map(String) : [],
4791
- ...text(args.project_uid) ? [text(args.project_uid)] : []
5018
+ ...text(args.project_uid) ? [text(args.project_uid)] : [],
5019
+ ...workspace?.uids ?? []
4792
5020
  ])
4793
5021
  );
4794
5022
  raw = await client2.plan(
@@ -4797,11 +5025,11 @@ async function handleMemoryTool(client2, args, config = {}) {
4797
5025
  task_uid: text(args.task_uid),
4798
5026
  session_uid: text(args.session_uid),
4799
5027
  scope_uids: scopeUids.length > 0 ? scopeUids : void 0,
4800
- limit: 1,
5028
+ limit: workspace ? WORKSPACE_RESUME_LIMIT : 1,
4801
5029
  agent_id: agentId
4802
5030
  })
4803
5031
  );
4804
- rendered2 = renderResumeContext(raw, budget) ?? "No eligible work was found in the requested scope.";
5032
+ rendered2 = renderResumeContext(raw, budget, workspace?.names) ?? "No eligible work was found in the requested scope.";
4805
5033
  strategy = "resume_work";
4806
5034
  candidates = resumeCandidates(raw);
4807
5035
  }
@@ -4814,7 +5042,8 @@ async function handleMemoryTool(client2, args, config = {}) {
4814
5042
  project_uid: text(args.project_uid),
4815
5043
  task_uid: text(args.task_uid),
4816
5044
  session_uid: text(args.session_uid),
4817
- scope_uids: Array.isArray(args.scope_uids) ? args.scope_uids.map(String) : void 0
5045
+ scope_uids: Array.isArray(args.scope_uids) ? args.scope_uids.map(String) : void 0,
5046
+ workspace_repositories: workspace ? Object.values(workspace.names).sort() : void 0
4818
5047
  });
4819
5048
  const contextId = `ctx_${sha2562(
4820
5049
  stableJson2({ mode, scope, strategy, query: text(args.query), rendered_hash: renderedHash })
@@ -4855,7 +5084,15 @@ async function handleMemoryTool(client2, args, config = {}) {
4855
5084
  valid_at: text(args.valid_at),
4856
5085
  latest_source_timestamp: latestTimestamp(raw)
4857
5086
  }),
4858
- warnings: contextWarnings(raw, rendered2, candidates.length, items.length)
5087
+ warnings: [
5088
+ ...contextWarnings(raw, rendered2, candidates.length, items.length),
5089
+ ...workspace && workspace.failures.length > 0 ? [
5090
+ {
5091
+ code: "workspace_scope_partial",
5092
+ message: `${workspace.failures.length} declared repositories did not resolve and were left out: ${workspace.failures.join("; ")}`
5093
+ }
5094
+ ] : []
5095
+ ]
4859
5096
  };
4860
5097
  return {
4861
5098
  content: [{ type: "text", text: rendered2 }],
@@ -4868,6 +5105,7 @@ var init_memory_tool = __esm({
4868
5105
  "use strict";
4869
5106
  import_node_crypto7 = require("crypto");
4870
5107
  init_render();
5108
+ init_workspace_scope();
4871
5109
  RESULT_SCHEMA_VERSION = "mindgraph.memory.result.v1";
4872
5110
  DEFAULT_OUTPUT_CHARS = 8e3;
4873
5111
  MIN_OUTPUT_CHARS = 512;
@@ -4902,6 +5140,11 @@ var init_memory_tool = __esm({
4902
5140
  items: { type: "string" },
4903
5141
  description: "Optional explicit graph scopes for continuity/resume"
4904
5142
  },
5143
+ scope: {
5144
+ type: "string",
5145
+ enum: ["workspace"],
5146
+ description: "For continuity/resume on a local server: select work across every repository the working directory's .mindgraph/workspace.json declares, and list the other eligible tasks grouped by repository"
5147
+ },
4905
5148
  node_types: {
4906
5149
  type: "array",
4907
5150
  items: { type: "string" },
@@ -5136,12 +5379,12 @@ var init_resources = __esm({
5136
5379
 
5137
5380
  // src/sync-tool.ts
5138
5381
  function safePath(root, candidate) {
5139
- const absolute = import_node_path6.default.resolve(root, candidate);
5140
- const relative2 = import_node_path6.default.relative(root, absolute);
5141
- if (relative2.startsWith("..") || import_node_path6.default.isAbsolute(relative2)) {
5382
+ const absolute = import_node_path7.default.resolve(root, candidate);
5383
+ const relative2 = import_node_path7.default.relative(root, absolute);
5384
+ if (relative2.startsWith("..") || import_node_path7.default.isAbsolute(relative2)) {
5142
5385
  throw new Error(`path escapes repository root: ${candidate}`);
5143
5386
  }
5144
- if (import_node_fs7.default.existsSync(absolute)) {
5387
+ if (import_node_fs8.default.existsSync(absolute)) {
5145
5388
  if (!pathContains(realPathOf(root), realPathOf(absolute))) {
5146
5389
  throw new Error(`path escapes repository root via symlink: ${candidate}`);
5147
5390
  }
@@ -5150,25 +5393,25 @@ function safePath(root, candidate) {
5150
5393
  }
5151
5394
  function realPathOf(target) {
5152
5395
  try {
5153
- return import_node_fs7.default.realpathSync(target);
5396
+ return import_node_fs8.default.realpathSync(target);
5154
5397
  } catch {
5155
- return import_node_path6.default.resolve(target);
5398
+ return import_node_path7.default.resolve(target);
5156
5399
  }
5157
5400
  }
5158
5401
  function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
5159
- if (!import_node_fs7.default.existsSync(target)) return [];
5402
+ if (!import_node_fs8.default.existsSync(target)) return [];
5160
5403
  const real = realPathOf(target);
5161
5404
  if (!pathContains(realRoot, real)) return [];
5162
- const stat = import_node_fs7.default.statSync(target);
5405
+ const stat = import_node_fs8.default.statSync(target);
5163
5406
  if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
5164
5407
  if (!stat.isDirectory() || visited.has(real)) return [];
5165
5408
  visited.add(real);
5166
- return import_node_fs7.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path6.default.join(target, entry.name), realRoot, visited));
5409
+ return import_node_fs8.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path7.default.join(target, entry.name), realRoot, visited));
5167
5410
  }
5168
5411
  function fileDescriptor(root, absolute) {
5169
- const content = import_node_fs7.default.readFileSync(absolute, "utf8");
5412
+ const content = import_node_fs8.default.readFileSync(absolute, "utf8");
5170
5413
  return {
5171
- logical_path: import_node_path6.default.relative(root, absolute).split(import_node_path6.default.sep).join("/"),
5414
+ logical_path: import_node_path7.default.relative(root, absolute).split(import_node_path7.default.sep).join("/"),
5172
5415
  content_hash: import_node_crypto8.default.createHash("sha256").update(content).digest("hex"),
5173
5416
  bytes: Buffer.byteLength(content),
5174
5417
  content
@@ -5238,7 +5481,7 @@ async function handleSyncTool(client2, args) {
5238
5481
  }
5239
5482
  if (action === "status" && request.logical_path && request.file_present !== false) {
5240
5483
  const absolute = safePath(root, String(request.logical_path));
5241
- request.file_present = import_node_fs7.default.existsSync(absolute);
5484
+ request.file_present = import_node_fs8.default.existsSync(absolute);
5242
5485
  if (request.file_present && !request.content_hash) {
5243
5486
  request.content_hash = fileDescriptor(root, absolute).content_hash;
5244
5487
  }
@@ -5282,13 +5525,13 @@ async function scanRepository(remote, repository, requested, provider, agentId)
5282
5525
  files: results
5283
5526
  };
5284
5527
  }
5285
- var import_node_crypto8, import_node_fs7, import_node_path6, ok3, err4, SYNC_TOOL;
5528
+ var import_node_crypto8, import_node_fs8, import_node_path7, ok3, err4, SYNC_TOOL;
5286
5529
  var init_sync_tool = __esm({
5287
5530
  "src/sync-tool.ts"() {
5288
5531
  "use strict";
5289
5532
  import_node_crypto8 = __toESM(require("crypto"));
5290
- import_node_fs7 = __toESM(require("fs"));
5291
- import_node_path6 = __toESM(require("path"));
5533
+ import_node_fs8 = __toESM(require("fs"));
5534
+ import_node_path7 = __toESM(require("path"));
5292
5535
  init_codegraph();
5293
5536
  init_error_detail();
5294
5537
  ok3 = (data) => ({
@@ -7869,14 +8112,14 @@ var init_tools = __esm({
7869
8112
  // src/server.ts
7870
8113
  function buildMindGraphServer(wiring) {
7871
8114
  const { client: client2, hosted } = wiring;
7872
- const server2 = new import_server.Server(
8115
+ const server = new import_server.Server(
7873
8116
  { name: "mindgraph", version: wiring.serverVersion },
7874
8117
  {
7875
8118
  capabilities: { tools: {}, resources: {}, prompts: {} },
7876
8119
  instructions: wiring.instructions
7877
8120
  }
7878
8121
  );
7879
- server2.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
8122
+ server.setRequestHandler("tools/list", async () => {
7880
8123
  const generated = await getGeneratedTools(client2, wiring.generatedTools);
7881
8124
  const tools = [...toolsForProfile(wiring.profile), ...generated.tools];
7882
8125
  if (!hosted) return { tools };
@@ -7887,7 +8130,7 @@ function buildMindGraphServer(wiring) {
7887
8130
  ]
7888
8131
  };
7889
8132
  });
7890
- server2.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
8133
+ server.setRequestHandler("tools/call", async (request) => {
7891
8134
  const { name, arguments: args } = request.params;
7892
8135
  const rawArgs = {
7893
8136
  ...args || {},
@@ -7934,20 +8177,20 @@ function buildMindGraphServer(wiring) {
7934
8177
  }
7935
8178
  return handleTool(client2, name, toolArgs, wiring.memory);
7936
8179
  });
7937
- server2.setRequestHandler(import_types.ListPromptsRequestSchema, async () => {
8180
+ server.setRequestHandler("prompts/list", async () => {
7938
8181
  return { prompts: PROMPTS };
7939
8182
  });
7940
- server2.setRequestHandler(import_types.GetPromptRequestSchema, async (request) => {
8183
+ server.setRequestHandler("prompts/get", async (request) => {
7941
8184
  const { name, arguments: args } = request.params;
7942
8185
  return getPrompt(name, args);
7943
8186
  });
7944
- server2.setRequestHandler(import_types.ListResourcesRequestSchema, async () => {
8187
+ server.setRequestHandler("resources/list", async () => {
7945
8188
  return { resources: RESOURCES };
7946
8189
  });
7947
- server2.setRequestHandler(import_types.ListResourceTemplatesRequestSchema, async () => {
8190
+ server.setRequestHandler("resources/templates/list", async () => {
7948
8191
  return { resourceTemplates: RESOURCE_TEMPLATES };
7949
8192
  });
7950
- server2.setRequestHandler(import_types.ReadResourceRequestSchema, async (request) => {
8193
+ server.setRequestHandler("resources/read", async (request) => {
7951
8194
  return readGovernedResource(
7952
8195
  client2,
7953
8196
  request.params.uri,
@@ -7955,15 +8198,13 @@ function buildMindGraphServer(wiring) {
7955
8198
  wiring.memory
7956
8199
  );
7957
8200
  });
7958
- return server2;
8201
+ return server;
7959
8202
  }
7960
- var import_server, import_webStandardStreamableHttp, import_types;
8203
+ var import_server;
7961
8204
  var init_server = __esm({
7962
8205
  "src/server.ts"() {
7963
8206
  "use strict";
7964
- import_server = require("@modelcontextprotocol/sdk/server/index.js");
7965
- import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
7966
- import_types = require("@modelcontextprotocol/sdk/types.js");
8207
+ import_server = require("@modelcontextprotocol/server");
7967
8208
  init_generated_tools();
7968
8209
  init_governance();
7969
8210
  init_hosted_tools();
@@ -7976,22 +8217,24 @@ var init_server = __esm({
7976
8217
  // src/index.ts
7977
8218
  var src_exports = {};
7978
8219
  async function main() {
7979
- const transport = new import_stdio.StdioServerTransport();
7980
- await server.connect(transport);
8220
+ (0, import_stdio.serveStdio)(buildServer, {
8221
+ onerror: (error2) => console.error("MindGraph MCP transport error:", error2.message)
8222
+ });
7981
8223
  console.error(
7982
- `MindGraph MCP server v${"0.24.0"} running on stdio (${BASE_URL})`
8224
+ `MindGraph MCP server v${"0.26.0"} running on stdio (${BASE_URL})`
7983
8225
  );
7984
8226
  }
7985
- var import_stdio, import_node_path7, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, GOVERNANCE_CONFIG, MEMORY_RUNTIME_CONFIG, server;
8227
+ var import_stdio, import_node_path8, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, GOVERNANCE_CONFIG, MEMORY_RUNTIME_CONFIG, buildServer;
7986
8228
  var init_src = __esm({
7987
8229
  "src/index.ts"() {
7988
8230
  "use strict";
7989
8231
  init_call_ledger();
7990
- import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
8232
+ import_stdio = require("@modelcontextprotocol/server/stdio");
7991
8233
  init_dist();
7992
8234
  init_hook_env();
7993
8235
  init_server();
7994
- import_node_path7 = __toESM(require("path"));
8236
+ init_workspace_scope();
8237
+ import_node_path8 = __toESM(require("path"));
7995
8238
  init_hook_installer();
7996
8239
  API_KEY = process.env.MINDGRAPH_API_KEY;
7997
8240
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
@@ -8092,7 +8335,7 @@ Capture knowledge when the user shares something worth remembering:
8092
8335
  }
8093
8336
  RUNNER_REFRESH = (() => {
8094
8337
  try {
8095
- return refreshHookRunner("0.24.0", import_node_path7.default.join(__dirname, "cli.js"));
8338
+ return refreshHookRunner("0.26.0", import_node_path8.default.join(__dirname, "cli.js"));
8096
8339
  } catch {
8097
8340
  return void 0;
8098
8341
  }
@@ -8106,12 +8349,12 @@ Capture knowledge when the user shares something worth remembering:
8106
8349
  }
8107
8350
  })();
8108
8351
  console.error(
8109
- `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.24.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
8352
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.26.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
8110
8353
  );
8111
8354
  }
8112
8355
  HOOK_SKEW_NOTE = (() => {
8113
8356
  try {
8114
- return versionSkewNote("0.24.0");
8357
+ return versionSkewNote("0.26.0");
8115
8358
  } catch {
8116
8359
  return void 0;
8117
8360
  }
@@ -8132,16 +8375,21 @@ ${HOOK_SKEW_NOTE}`;
8132
8375
  }
8133
8376
  };
8134
8377
  MEMORY_RUNTIME_CONFIG = {
8135
- serverVersion: "0.24.0",
8378
+ serverVersion: "0.26.0",
8136
8379
  baseUrl: BASE_URL,
8137
8380
  profile: PROFILE,
8138
8381
  harness: HARNESS,
8139
8382
  orgId: ORG_ID,
8140
- agentId: AGENT_ID
8383
+ agentId: AGENT_ID,
8384
+ resolveWorkspaceScope: (agentId) => resolveRepositoryScope(
8385
+ process.env.CLAUDE_PROJECT_DIR || process.cwd(),
8386
+ client,
8387
+ agentId
8388
+ )
8141
8389
  };
8142
- server = buildMindGraphServer({
8390
+ buildServer = () => buildMindGraphServer({
8143
8391
  client,
8144
- serverVersion: "0.24.0",
8392
+ serverVersion: "0.26.0",
8145
8393
  instructions: INSTRUCTIONS,
8146
8394
  profile: PROFILE,
8147
8395
  agentId: AGENT_ID,
@@ -8157,8 +8405,8 @@ ${HOOK_SKEW_NOTE}`;
8157
8405
  });
8158
8406
 
8159
8407
  // src/cli.ts
8160
- var fs8 = __toESM(require("fs"));
8161
- var path8 = __toESM(require("path"));
8408
+ var fs9 = __toESM(require("fs"));
8409
+ var path9 = __toESM(require("path"));
8162
8410
  var os5 = __toESM(require("os"));
8163
8411
  var crypto6 = __toESM(require("crypto"));
8164
8412
  var import_child_process = require("child_process");
@@ -8643,18 +8891,19 @@ async function skillsPush(api, skillPath) {
8643
8891
 
8644
8892
  // src/hook-core.ts
8645
8893
  var import_node_crypto5 = __toESM(require("crypto"));
8646
- var import_node_fs5 = __toESM(require("fs"));
8894
+ var import_node_fs6 = __toESM(require("fs"));
8647
8895
  var import_node_os4 = __toESM(require("os"));
8648
- var import_node_path4 = __toESM(require("path"));
8896
+ var import_node_path5 = __toESM(require("path"));
8649
8897
  var import_node_child_process2 = require("child_process");
8650
8898
  init_codegraph();
8651
- init_code_tool();
8899
+ init_render();
8900
+ init_workspace_scope();
8652
8901
  init_call_ledger();
8653
8902
  init_error_detail();
8654
8903
  init_hook_env();
8655
8904
 
8656
8905
  // src/hook-memory.ts
8657
- var import_node_fs4 = __toESM(require("fs"));
8906
+ var import_node_fs5 = __toESM(require("fs"));
8658
8907
  var PROMPT_RECALL_LIMIT = 5;
8659
8908
  var PROMPT_RECALL_MAX_CHARS = 4e3;
8660
8909
  var MIN_PROMPT_RECALL_CHARS = 12;
@@ -8724,18 +8973,18 @@ function recallTermOverlap(query, text2) {
8724
8973
  function relevant(query, text2) {
8725
8974
  if (query === void 0) return true;
8726
8975
  const { matched, usable } = recallTermOverlap(query, text2);
8727
- return usable > 0 && matched >= Math.min(2, usable);
8976
+ return usable >= 2 && matched >= 2;
8728
8977
  }
8729
8978
  var rec = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
8730
- var str = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8979
+ var str2 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8731
8980
  var oneLine = (value, max) => {
8732
8981
  const flat = value.replace(/\s+/g, " ").trim();
8733
8982
  return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
8734
8983
  };
8735
8984
  var INFRA_TYPES = /* @__PURE__ */ new Set(["Chunk", "Document", "Article", "Session"]);
8736
8985
  function nodeType(node) {
8737
- const plain = str(node.node_type);
8738
- return (plain && plain !== "Custom" ? plain : void 0) ?? str(rec(node.node_type)?.Custom) ?? plain ?? "Node";
8986
+ const plain = str2(node.node_type);
8987
+ return (plain && plain !== "Custom" ? plain : void 0) ?? str2(rec(node.node_type)?.Custom) ?? plain ?? "Node";
8739
8988
  }
8740
8989
  function renderPromptRecall(response, query) {
8741
8990
  const payload = rec(response);
@@ -8748,16 +8997,16 @@ function renderPromptRecall(response, query) {
8748
8997
  const type = nodeType(node);
8749
8998
  if (INFRA_TYPES.has(type)) continue;
8750
8999
  const props = rec(node.props);
8751
- const label = str(node.label) ?? str(props?.label) ?? str(props?.name);
9000
+ const label = str2(node.label) ?? str2(props?.label) ?? str2(props?.name);
8752
9001
  if (!label) continue;
8753
- const summary = str(node.summary) ?? str(props?.summary) ?? str(props?.description) ?? str(props?.content);
9002
+ const summary = str2(node.summary) ?? str2(props?.summary) ?? str2(props?.description) ?? str2(props?.content);
8754
9003
  const informative = summary && summary !== label ? summary : void 0;
8755
9004
  if (!informative && type === "Entity") continue;
8756
9005
  if (!relevant(query, `${label} ${informative ?? ""}`)) continue;
8757
9006
  const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
8758
- const source = str(rec(docs[0])?.title);
9007
+ const source = str2(rec(docs[0])?.title);
8759
9008
  let line = `- [${type}] ${oneLine(label, 140)}`;
8760
- if (node.superseded === true) line += " (superseded)";
9009
+ if (node.superseded) line += " (superseded)";
8761
9010
  if (summary && summary !== label) line += ` \u2014 ${oneLine(summary, 240)}`;
8762
9011
  if (source) line += ` (source: ${oneLine(source, 100)})`;
8763
9012
  lines.push(line);
@@ -8766,9 +9015,9 @@ function renderPromptRecall(response, query) {
8766
9015
  for (const raw of articles) {
8767
9016
  const article = rec(raw);
8768
9017
  if (!article || lines.length >= PROMPT_RECALL_LIMIT) continue;
8769
- const label = str(article.label);
9018
+ const label = str2(article.label);
8770
9019
  if (!label) continue;
8771
- const content = str(article.content);
9020
+ const content = str2(article.content);
8772
9021
  if (!relevant(query, `${label} ${content ?? ""}`)) continue;
8773
9022
  lines.push(`- [Article] ${oneLine(label, 140)}${content ? ` \u2014 ${oneLine(content, 240)}` : ""}`);
8774
9023
  }
@@ -8819,7 +9068,7 @@ function renderTranscript(turns) {
8819
9068
  return start >= 0 ? tail.slice(start + 2) : tail;
8820
9069
  }
8821
9070
  function readTranscriptSince(file, offset = 0) {
8822
- const buffer = import_node_fs4.default.readFileSync(file);
9071
+ const buffer = import_node_fs5.default.readFileSync(file);
8823
9072
  const start = offset > buffer.length ? 0 : offset;
8824
9073
  const lastNewline = buffer.lastIndexOf(10);
8825
9074
  if (lastNewline < start) return { text: "", nextOffset: start };
@@ -8863,33 +9112,33 @@ function defaultLedger(sessionId, options) {
8863
9112
  };
8864
9113
  }
8865
9114
  function runtimeDir(options) {
8866
- return options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path4.default.join(import_node_os4.default.homedir(), ".mindgraph", "runtime");
9115
+ return options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path5.default.join(import_node_os4.default.homedir(), ".mindgraph", "runtime");
8867
9116
  }
8868
9117
  function ledgerPath(sessionId, options) {
8869
9118
  const harness = "harness" in options ? options.harness : "claude-code";
8870
9119
  const key = import_node_crypto5.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
8871
- return import_node_path4.default.join(runtimeDir(options), `${key}.json`);
9120
+ return import_node_path5.default.join(runtimeDir(options), `${key}.json`);
8872
9121
  }
8873
9122
  function waitBriefly(ms) {
8874
9123
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
8875
9124
  }
8876
9125
  function withLedger(sessionId, options, update) {
8877
9126
  const file = ledgerPath(sessionId, options);
8878
- import_node_fs5.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true, mode: 448 });
9127
+ import_node_fs6.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true, mode: 448 });
8879
9128
  try {
8880
- import_node_fs5.default.chmodSync(import_node_path4.default.dirname(file), 448);
9129
+ import_node_fs6.default.chmodSync(import_node_path5.default.dirname(file), 448);
8881
9130
  } catch {
8882
9131
  }
8883
9132
  const lock = `${file}.lock`;
8884
9133
  let lockFd;
8885
9134
  for (let attempt = 0; attempt < 400; attempt++) {
8886
9135
  try {
8887
- lockFd = import_node_fs5.default.openSync(lock, "wx", 384);
9136
+ lockFd = import_node_fs6.default.openSync(lock, "wx", 384);
8888
9137
  break;
8889
9138
  } catch (error2) {
8890
9139
  if (error2.code !== "EEXIST") throw error2;
8891
9140
  try {
8892
- if (Date.now() - import_node_fs5.default.statSync(lock).mtimeMs > 3e3) import_node_fs5.default.unlinkSync(lock);
9141
+ if (Date.now() - import_node_fs6.default.statSync(lock).mtimeMs > 3e3) import_node_fs6.default.unlinkSync(lock);
8893
9142
  } catch {
8894
9143
  }
8895
9144
  waitBriefly(10);
@@ -8903,25 +9152,25 @@ function withLedger(sessionId, options, update) {
8903
9152
  try {
8904
9153
  ledger = {
8905
9154
  ...ledger,
8906
- ...JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"))
9155
+ ...JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"))
8907
9156
  };
8908
9157
  } catch {
8909
9158
  }
8910
9159
  const result = update(ledger);
8911
9160
  ledger.updatedAt = Date.now();
8912
9161
  const temporary = `${file}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
8913
- import_node_fs5.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
9162
+ import_node_fs6.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
8914
9163
  `, { mode: 384 });
8915
- import_node_fs5.default.renameSync(temporary, file);
9164
+ import_node_fs6.default.renameSync(temporary, file);
8916
9165
  try {
8917
- import_node_fs5.default.chmodSync(file, 384);
9166
+ import_node_fs6.default.chmodSync(file, 384);
8918
9167
  } catch {
8919
9168
  }
8920
9169
  return result;
8921
9170
  } finally {
8922
- import_node_fs5.default.closeSync(lockFd);
9171
+ import_node_fs6.default.closeSync(lockFd);
8923
9172
  try {
8924
- import_node_fs5.default.unlinkSync(lock);
9173
+ import_node_fs6.default.unlinkSync(lock);
8925
9174
  } catch {
8926
9175
  }
8927
9176
  }
@@ -9031,6 +9280,11 @@ function renderBrief(brief, meta) {
9031
9280
  );
9032
9281
  const description = clipTracked(props?.description, 400);
9033
9282
  if (description) lines.push(` ${description}`);
9283
+ const taskScopes = Array.isArray(brief?.task_scope_uids) ? brief.task_scope_uids.map(String) : [];
9284
+ const taskRepository = taskScopes.map((uid) => meta.repositoryNames?.[uid]).find(Boolean);
9285
+ if (taskRepository && Object.keys(meta.repositoryNames ?? {}).length > 1) {
9286
+ lines.push(`Repository: ${taskRepository}`);
9287
+ }
9034
9288
  const reason = string(brief?.selection_reason);
9035
9289
  if (reason) lines.push(`Selection: ${reason}`);
9036
9290
  const lease = object(brief?.claim) || object(brief?.lease);
@@ -9076,6 +9330,8 @@ ${blockerLines.join("\n")}`);
9076
9330
  }
9077
9331
  }
9078
9332
  const optional = [];
9333
+ const otherWork = renderOtherEligibleWork(brief, meta.repositoryNames);
9334
+ if (otherWork) optional.push(otherWork);
9079
9335
  const knowledge = Array.isArray(brief?.knowledge) ? brief.knowledge : [];
9080
9336
  if (knowledge.length > 5) bounded = true;
9081
9337
  const knowledgeLines = knowledge.slice(0, 5).map((item) => nodeLine(item, 350)).filter(Boolean);
@@ -9112,62 +9368,6 @@ ${knowledgeLines.join("\n")}`);
9112
9368
  return rendered2.length <= BRIEF_CHAR_BUDGET ? rendered2 : `${rendered2.slice(0, BRIEF_CHAR_BUDGET - 40)}
9113
9369
  \u2026[bounded work brief truncated]`;
9114
9370
  }
9115
- async function repositoryScopeUids(input, client2, options, agentId) {
9116
- if (!client2.entity) return { uids: [], failures: [] };
9117
- const adapter = new CodegraphAdapter({
9118
- cwd: input.cwd,
9119
- workspaceFile: options.workspaceFile
9120
- });
9121
- const configured = await adapter.resolveRepositories();
9122
- const repositories = configured.length > 0 ? configured : [await adapter.resolveRepository()];
9123
- const failures = [];
9124
- const resolved = await Promise.all(
9125
- repositories.map(async (repository) => {
9126
- try {
9127
- const payload = object(
9128
- await client2.entity({
9129
- action: "resolve_identity",
9130
- identity: {
9131
- namespace: "external.code",
9132
- key_version: 1,
9133
- key: repositoryIdentityKey(repository)
9134
- },
9135
- agent_id: agentId
9136
- })
9137
- );
9138
- if (payload?.status === "existing" && string(payload.uid)) {
9139
- return string(payload.uid);
9140
- }
9141
- if (payload?.status === "absent") {
9142
- const anchored = await anchorRepository(
9143
- client2,
9144
- repository,
9145
- repository.spaceUid ?? `space:agent:${agentId}`,
9146
- agentId
9147
- );
9148
- if (anchored?.uid) return anchored.uid;
9149
- failures.push(
9150
- `${repository.repoId}: anchor ${anchored?.status ?? "failed"}`
9151
- );
9152
- return void 0;
9153
- }
9154
- failures.push(
9155
- `${repository.repoId}: ${string(payload?.status) || "unresolved"}`
9156
- );
9157
- return void 0;
9158
- } catch (error2) {
9159
- failures.push(
9160
- `${repository.repoId}: ${error2?.message || "resolve failed"}`
9161
- );
9162
- return void 0;
9163
- }
9164
- })
9165
- );
9166
- return {
9167
- uids: [...new Set(resolved.filter((uid) => Boolean(uid)))],
9168
- failures
9169
- };
9170
- }
9171
9371
  function isOwnPriorWork(brief, agentId, harness) {
9172
9372
  const reason = string(brief?.selection_reason);
9173
9373
  if (reason === "owned_live_lease" || reason === "same_agent_session_rebind") {
@@ -9191,30 +9391,30 @@ function hasMindgraphServer(servers) {
9191
9391
  return false;
9192
9392
  }
9193
9393
  function pathIsWithin(ancestor, descendant) {
9194
- const base = import_node_path4.default.resolve(ancestor);
9195
- const target = import_node_path4.default.resolve(descendant);
9196
- return target === base || target.startsWith(base + import_node_path4.default.sep);
9394
+ const base = import_node_path5.default.resolve(ancestor);
9395
+ const target = import_node_path5.default.resolve(descendant);
9396
+ return target === base || target.startsWith(base + import_node_path5.default.sep);
9197
9397
  }
9198
9398
  function claudeMcpRegistrationNote(input, options) {
9199
9399
  if (options.harness !== "claude-code") return void 0;
9200
9400
  if (process.env.MINDGRAPH_SKIP_MCP_REGISTRATION_CHECK === "1") return void 0;
9201
9401
  try {
9202
- const configPath = options.claudeConfigPath || import_node_path4.default.join(import_node_os4.default.homedir(), ".claude.json");
9203
- if (!import_node_fs5.default.existsSync(configPath)) return void 0;
9204
- const config = JSON.parse(import_node_fs5.default.readFileSync(configPath, "utf8"));
9402
+ const configPath = options.claudeConfigPath || import_node_path5.default.join(import_node_os4.default.homedir(), ".claude.json");
9403
+ if (!import_node_fs6.default.existsSync(configPath)) return void 0;
9404
+ const config = JSON.parse(import_node_fs6.default.readFileSync(configPath, "utf8"));
9205
9405
  if (hasMindgraphServer(config.mcpServers)) return void 0;
9206
- const cwd = import_node_path4.default.resolve(input.cwd || ".");
9406
+ const cwd = import_node_path5.default.resolve(input.cwd || ".");
9207
9407
  for (const [projectDir, project] of Object.entries(config.projects || {})) {
9208
9408
  if (!pathIsWithin(projectDir, cwd)) continue;
9209
9409
  if (hasMindgraphServer(project?.mcpServers)) return void 0;
9210
9410
  }
9211
- for (let dir = cwd; ; dir = import_node_path4.default.dirname(dir)) {
9212
- const candidate = import_node_path4.default.join(dir, ".mcp.json");
9213
- if (import_node_fs5.default.existsSync(candidate)) {
9214
- const parsed = JSON.parse(import_node_fs5.default.readFileSync(candidate, "utf8"));
9411
+ for (let dir = cwd; ; dir = import_node_path5.default.dirname(dir)) {
9412
+ const candidate = import_node_path5.default.join(dir, ".mcp.json");
9413
+ if (import_node_fs6.default.existsSync(candidate)) {
9414
+ const parsed = JSON.parse(import_node_fs6.default.readFileSync(candidate, "utf8"));
9215
9415
  if (hasMindgraphServer(parsed.mcpServers)) return void 0;
9216
9416
  }
9217
- if (dir === import_node_path4.default.dirname(dir)) break;
9417
+ if (dir === import_node_path5.default.dirname(dir)) break;
9218
9418
  }
9219
9419
  } catch {
9220
9420
  return void 0;
@@ -9244,7 +9444,10 @@ async function sessionStart(input, client2, options) {
9244
9444
  return void 0;
9245
9445
  });
9246
9446
  }
9247
- const scope = await repositoryScopeUids(input, client2, options, agentId);
9447
+ const scope = await resolveRepositoryScope(input.cwd, client2, agentId, {
9448
+ workspaceFile: options.workspaceFile,
9449
+ cache: options.scopeCacheTenant ? { dir: runtimeDir(options), tenant: options.scopeCacheTenant } : void 0
9450
+ });
9248
9451
  if (scope.failures.length > 0) {
9249
9452
  console.error(
9250
9453
  `mindgraph-hook: ${scope.failures.length} repository scope(s) failed to resolve (${scope.failures.join("; ")}); resume runs ${scope.uids.length > 0 ? "narrowed" : "UNSCOPED"}`
@@ -9256,9 +9459,17 @@ async function sessionStart(input, client2, options) {
9256
9459
  action: "resume_work",
9257
9460
  session_uid: sessionUid,
9258
9461
  ...scopeUids.length > 0 ? { scope_uids: scopeUids } : {},
9462
+ // Above several repositories one task is not the picture: ask for the
9463
+ // work ranked behind it too. An older server ignores the field.
9464
+ ...scopeUids.length > 1 ? { limit: WORKSPACE_RESUME_LIMIT } : {},
9259
9465
  agent_id: agentId
9260
9466
  })
9261
9467
  );
9468
+ const workspaceList = {
9469
+ task_scope_uids: brief?.task_scope_uids,
9470
+ other_eligible: brief?.other_eligible,
9471
+ eligible_total: brief?.eligible_total
9472
+ };
9262
9473
  const task = object(brief?.task);
9263
9474
  const lease = object(brief?.lease);
9264
9475
  const prior = readLedger(input.session_id, options);
@@ -9297,7 +9508,7 @@ async function sessionStart(input, client2, options) {
9297
9508
  agent_id: agentId
9298
9509
  })
9299
9510
  );
9300
- if (reResumed) brief = reResumed;
9511
+ if (reResumed) brief = { ...reResumed, ...workspaceList };
9301
9512
  } catch {
9302
9513
  }
9303
9514
  if (brief) brief.claim = claimed;
@@ -9308,7 +9519,8 @@ async function sessionStart(input, client2, options) {
9308
9519
  const briefText = renderBrief(brief || { status: "no_eligible_work" }, {
9309
9520
  claimed: Boolean(claimed) || stillHeld,
9310
9521
  claimFailed: claimFailed && !stillHeld,
9311
- scopeNote
9522
+ scopeNote,
9523
+ repositoryNames: scope.names
9312
9524
  });
9313
9525
  const registrationNote = claudeMcpRegistrationNote(input, options);
9314
9526
  if (registrationNote) {
@@ -9755,7 +9967,7 @@ function previewCapture(transcriptPath, sessionId, options = {}) {
9755
9967
  let ledger = {};
9756
9968
  try {
9757
9969
  ledger = JSON.parse(
9758
- import_node_fs5.default.readFileSync(
9970
+ import_node_fs6.default.readFileSync(
9759
9971
  ledgerPath(sessionId, { ...options, harness: "claude-code" }),
9760
9972
  "utf8"
9761
9973
  )
@@ -9776,13 +9988,13 @@ function readHookStats(sessionId, options = {}) {
9776
9988
  let best;
9777
9989
  let names = [];
9778
9990
  try {
9779
- names = import_node_fs5.default.readdirSync(dir).filter((name) => /^[0-9a-f]{64}\.json$/.test(name));
9991
+ names = import_node_fs6.default.readdirSync(dir).filter((name) => /^[0-9a-f]{64}\.json$/.test(name));
9780
9992
  } catch {
9781
9993
  return void 0;
9782
9994
  }
9783
9995
  for (const name of names) {
9784
9996
  try {
9785
- const ledger = JSON.parse(import_node_fs5.default.readFileSync(import_node_path4.default.join(dir, name), "utf8"));
9997
+ const ledger = JSON.parse(import_node_fs6.default.readFileSync(import_node_path5.default.join(dir, name), "utf8"));
9786
9998
  if (!ledger.harnessSessionId || !ledger.hookStats) continue;
9787
9999
  if (sessionId && ledger.harnessSessionId !== sessionId) continue;
9788
10000
  const updatedAt = Number(ledger.updatedAt) || 0;
@@ -9861,15 +10073,15 @@ async function runCodexHook(input, client2, options = {}) {
9861
10073
  init_hook_installer();
9862
10074
  function recordHookHealth(harness, error2) {
9863
10075
  try {
9864
- const dir = process.env.MINDGRAPH_RUNTIME_DIR || path8.join(os5.homedir(), ".mindgraph", "runtime");
9865
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
9866
- const file = path8.join(dir, "hook-health.json");
10076
+ const dir = process.env.MINDGRAPH_RUNTIME_DIR || path9.join(os5.homedir(), ".mindgraph", "runtime");
10077
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
10078
+ const file = path9.join(dir, "hook-health.json");
9867
10079
  let failures = 0;
9868
10080
  try {
9869
- failures = Number(JSON.parse(fs8.readFileSync(file, "utf8")).consecutiveFailures) || 0;
10081
+ failures = Number(JSON.parse(fs9.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9870
10082
  } catch {
9871
10083
  }
9872
- fs8.writeFileSync(
10084
+ fs9.writeFileSync(
9873
10085
  file,
9874
10086
  `${JSON.stringify({
9875
10087
  harness,
@@ -9885,14 +10097,14 @@ function recordHookHealth(harness, error2) {
9885
10097
  }
9886
10098
  function clearHookHealth(harness) {
9887
10099
  try {
9888
- const file = path8.join(
9889
- process.env.MINDGRAPH_RUNTIME_DIR || path8.join(os5.homedir(), ".mindgraph", "runtime"),
10100
+ const file = path9.join(
10101
+ process.env.MINDGRAPH_RUNTIME_DIR || path9.join(os5.homedir(), ".mindgraph", "runtime"),
9890
10102
  "hook-health.json"
9891
10103
  );
9892
- if (!fs8.existsSync(file)) return;
9893
- const current = JSON.parse(fs8.readFileSync(file, "utf8"));
10104
+ if (!fs9.existsSync(file)) return;
10105
+ const current = JSON.parse(fs9.readFileSync(file, "utf8"));
9894
10106
  if (!Number(current.consecutiveFailures)) return;
9895
- fs8.writeFileSync(
10107
+ fs9.writeFileSync(
9896
10108
  file,
9897
10109
  `${JSON.stringify({
9898
10110
  harness,
@@ -9908,7 +10120,7 @@ function clearHookHealth(harness) {
9908
10120
  function getClaudeDesktopConfigPath() {
9909
10121
  switch (process.platform) {
9910
10122
  case "darwin":
9911
- return path8.join(
10123
+ return path9.join(
9912
10124
  os5.homedir(),
9913
10125
  "Library",
9914
10126
  "Application Support",
@@ -9916,13 +10128,13 @@ function getClaudeDesktopConfigPath() {
9916
10128
  "claude_desktop_config.json"
9917
10129
  );
9918
10130
  case "win32":
9919
- return path8.join(
9920
- process.env.APPDATA || path8.join(os5.homedir(), "AppData", "Roaming"),
10131
+ return path9.join(
10132
+ process.env.APPDATA || path9.join(os5.homedir(), "AppData", "Roaming"),
9921
10133
  "Claude",
9922
10134
  "claude_desktop_config.json"
9923
10135
  );
9924
10136
  case "linux":
9925
- return path8.join(
10137
+ return path9.join(
9926
10138
  os5.homedir(),
9927
10139
  ".config",
9928
10140
  "Claude",
@@ -9933,16 +10145,16 @@ function getClaudeDesktopConfigPath() {
9933
10145
  }
9934
10146
  }
9935
10147
  function readJsonFile(filePath) {
9936
- if (!fs8.existsSync(filePath)) return {};
9937
- const content = fs8.readFileSync(filePath, "utf-8");
10148
+ if (!fs9.existsSync(filePath)) return {};
10149
+ const content = fs9.readFileSync(filePath, "utf-8");
9938
10150
  return JSON.parse(content);
9939
10151
  }
9940
10152
  function writeJsonFile(filePath, data) {
9941
- const dir = path8.dirname(filePath);
9942
- if (!fs8.existsSync(dir)) {
9943
- fs8.mkdirSync(dir, { recursive: true });
10153
+ const dir = path9.dirname(filePath);
10154
+ if (!fs9.existsSync(dir)) {
10155
+ fs9.mkdirSync(dir, { recursive: true });
9944
10156
  }
9945
- fs8.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
10157
+ fs9.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9946
10158
  }
9947
10159
  function getMcpServerEntry(apiKey, baseUrl) {
9948
10160
  const env = { MINDGRAPH_API_KEY: apiKey };
@@ -10029,7 +10241,7 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
10029
10241
  }
10030
10242
  function uninstallClaudeDesktop() {
10031
10243
  const configPath = getClaudeDesktopConfigPath();
10032
- if (!fs8.existsSync(configPath)) {
10244
+ if (!fs9.existsSync(configPath)) {
10033
10245
  console.log("Claude Desktop config not found. Nothing to remove.");
10034
10246
  return;
10035
10247
  }
@@ -10056,7 +10268,7 @@ function uninstallClaudeCode() {
10056
10268
  }
10057
10269
  function printStatus() {
10058
10270
  const desktopPath = getClaudeDesktopConfigPath();
10059
- if (fs8.existsSync(desktopPath)) {
10271
+ if (fs9.existsSync(desktopPath)) {
10060
10272
  const config = readJsonFile(desktopPath);
10061
10273
  const servers = config.mcpServers;
10062
10274
  if (servers && "mindgraph" in servers) {
@@ -10074,12 +10286,12 @@ function printStatus() {
10074
10286
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
10075
10287
  );
10076
10288
  try {
10077
- const healthFile = path8.join(
10078
- process.env.MINDGRAPH_RUNTIME_DIR || path8.join(os5.homedir(), ".mindgraph", "runtime"),
10289
+ const healthFile = path9.join(
10290
+ process.env.MINDGRAPH_RUNTIME_DIR || path9.join(os5.homedir(), ".mindgraph", "runtime"),
10079
10291
  "hook-health.json"
10080
10292
  );
10081
- if (fs8.existsSync(healthFile)) {
10082
- const health = JSON.parse(fs8.readFileSync(healthFile, "utf8"));
10293
+ if (fs9.existsSync(healthFile)) {
10294
+ const health = JSON.parse(fs9.readFileSync(healthFile, "utf8"));
10083
10295
  const failures = Number(health.consecutiveFailures) || 0;
10084
10296
  console.log(
10085
10297
  failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
@@ -10108,21 +10320,21 @@ function printClaudeHookFeatures(features) {
10108
10320
  );
10109
10321
  }
10110
10322
  function latestTranscript(cwd) {
10111
- const dir = path8.join(
10323
+ const dir = path9.join(
10112
10324
  os5.homedir(),
10113
10325
  ".claude",
10114
10326
  "projects",
10115
10327
  cwd.replace(/[^A-Za-z0-9]/g, "-")
10116
10328
  );
10117
10329
  try {
10118
- return fs8.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path8.join(dir, name)).sort((a, b) => fs8.statSync(b).mtimeMs - fs8.statSync(a).mtimeMs)[0];
10330
+ return fs9.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path9.join(dir, name)).sort((a, b) => fs9.statSync(b).mtimeMs - fs9.statSync(a).mtimeMs)[0];
10119
10331
  } catch {
10120
10332
  return void 0;
10121
10333
  }
10122
10334
  }
10123
10335
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
10124
10336
  if (withHooks) {
10125
- const runner = installHookRunner(__filename, void 0, "0.24.0");
10337
+ const runner = installHookRunner(__filename, void 0, "0.26.0");
10126
10338
  console.log(`Installed pinned hook runner at ${runner}`);
10127
10339
  const features = claudeHookFeatures();
10128
10340
  const result = installClaudeHooks("user", void 0, features);
@@ -10450,7 +10662,7 @@ ${total} skill(s) synced.`);
10450
10662
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
10451
10663
  throw new Error("--harness must be claude-code or codex");
10452
10664
  }
10453
- const runner = installHookRunner(__filename, void 0, "0.24.0");
10665
+ const runner = installHookRunner(__filename, void 0, "0.26.0");
10454
10666
  console.log(`Installed pinned hook runner at ${runner}`);
10455
10667
  const features = hookHarness === "claude-code" ? claudeHookFeatures() : void 0;
10456
10668
  const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir, features);
@@ -10525,7 +10737,14 @@ ${total} skill(s) synced.`);
10525
10737
  // No harness-name fallback here: hook-core derives a stable
10526
10738
  // per-user default shared by both harnesses, which is what lets a
10527
10739
  // task claimed under Codex rebind under Claude Code.
10528
- agentId: process.env.MINDGRAPH_AGENT_ID || stored.agentId
10740
+ agentId: process.env.MINDGRAPH_AGENT_ID || stored.agentId,
10741
+ scopeCacheTenant: crypto6.createHash("sha256").update(
10742
+ [
10743
+ hookBaseUrl || "https://api.mindgraph.cloud",
10744
+ hookApiKey,
10745
+ process.env.MINDGRAPH_ORG_ID || stored.orgId || ""
10746
+ ].join("\n")
10747
+ ).digest("hex").slice(0, 16)
10529
10748
  };
10530
10749
  const output = hookHarness === "codex" ? await runCodexHook(
10531
10750
  input,
@@ -10583,13 +10802,13 @@ ${total} skill(s) synced.`);
10583
10802
  process.exit(1);
10584
10803
  }
10585
10804
  const transcript = positionals[1] || latestTranscript(process.cwd());
10586
- if (!transcript || !fs8.existsSync(transcript)) {
10805
+ if (!transcript || !fs9.existsSync(transcript)) {
10587
10806
  console.error(
10588
10807
  "No transcript found. Pass a Claude Code transcript path (~/.claude/projects/<dir>/<session>.jsonl)."
10589
10808
  );
10590
10809
  process.exit(1);
10591
10810
  }
10592
- const sessionId = parseArgs(process.argv).sessionId || path8.basename(transcript, ".jsonl");
10811
+ const sessionId = parseArgs(process.argv).sessionId || path9.basename(transcript, ".jsonl");
10593
10812
  const preview = previewCapture(transcript, sessionId, {
10594
10813
  agentId: process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId
10595
10814
  });