@graph-tl/graph 0.1.10 → 0.1.14

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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-WKOEKYTF.js";
5
5
  import {
6
6
  handleAgentConfig
7
- } from "./chunk-ILTJI4ZN.js";
7
+ } from "./chunk-JRMFXD5I.js";
8
8
  import {
9
9
  EngineError,
10
10
  ValidationError,
@@ -19,6 +19,7 @@ import {
19
19
  getNodeOrThrow,
20
20
  getProjectRoot,
21
21
  getProjectSummary,
22
+ getSubtreeProgress,
22
23
  listProjects,
23
24
  logEvent,
24
25
  optionalBoolean,
@@ -28,7 +29,7 @@ import {
28
29
  requireString,
29
30
  setDbPath,
30
31
  updateNode
31
- } from "./chunk-TWT5GUXW.js";
32
+ } from "./chunk-CCGKUMCW.js";
32
33
 
33
34
  // src/server.ts
34
35
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -54,15 +55,17 @@ function handleOpen(input, agent) {
54
55
  root = createNode({
55
56
  project,
56
57
  summary: goal ?? project,
57
- discovery: "pending",
58
+ discovery: input?.skip_discovery ? "done" : "pending",
58
59
  agent
59
60
  });
60
61
  isNew = true;
61
62
  }
62
63
  const summary = getProjectSummary(project);
63
64
  const result = { project, root, summary };
64
- if (isNew) {
65
+ if (isNew && root.discovery === "pending") {
65
66
  result.hint = `New project created. Discovery is pending \u2014 interview the user to understand scope and goals, then set discovery to "done" via graph_update before decomposing with graph_plan.`;
67
+ } else if (isNew) {
68
+ result.hint = `New project created. Ready to decompose \u2014 use graph_plan to add tasks.`;
66
69
  } else if (root.discovery === "pending") {
67
70
  result.hint = `Discovery is still pending on this project. Complete the discovery interview, then set discovery to "done" via graph_update.`;
68
71
  } else if (summary.actionable > 0) {
@@ -216,7 +219,7 @@ function handlePlan(input, agent) {
216
219
  const refs = /* @__PURE__ */ new Set();
217
220
  for (const node of nodes) {
218
221
  if (refs.has(node.ref)) {
219
- throw new Error(`Duplicate ref in batch: ${node.ref}`);
222
+ throw new EngineError("duplicate_ref", `Duplicate ref in batch: ${node.ref}`);
220
223
  }
221
224
  refs.add(node.ref);
222
225
  }
@@ -230,7 +233,8 @@ function handlePlan(input, agent) {
230
233
  if (existing) {
231
234
  parentId = existing.id;
232
235
  } else {
233
- throw new Error(
236
+ throw new EngineError(
237
+ "invalid_parent_ref",
234
238
  `parent_ref "${nodeInput.parent_ref}" is neither a batch ref nor an existing node ID`
235
239
  );
236
240
  }
@@ -247,7 +251,8 @@ function handlePlan(input, agent) {
247
251
  }
248
252
  project = parentNode.project;
249
253
  } else {
250
- throw new Error(
254
+ throw new EngineError(
255
+ "missing_parent",
251
256
  `Node "${nodeInput.ref}" has no parent_ref. All planned nodes must have a parent (an existing node or a batch ref).`
252
257
  );
253
258
  }
@@ -272,7 +277,8 @@ function handlePlan(input, agent) {
272
277
  if (existing) {
273
278
  toId = existing.id;
274
279
  } else {
275
- throw new Error(
280
+ throw new EngineError(
281
+ "invalid_depends_on",
276
282
  `depends_on "${dep}" in node "${nodeInput.ref}" is neither a batch ref nor an existing node ID`
277
283
  );
278
284
  }
@@ -284,7 +290,8 @@ function handlePlan(input, agent) {
284
290
  agent
285
291
  });
286
292
  if (result.rejected) {
287
- throw new Error(
293
+ throw new EngineError(
294
+ "edge_rejected",
288
295
  `Dependency edge from "${nodeInput.ref}" to "${dep}" rejected: ${result.reason}`
289
296
  );
290
297
  }
@@ -311,6 +318,15 @@ function handleUpdate(input, agent) {
311
318
  const resolvedIds = [];
312
319
  const resolvedProjects = /* @__PURE__ */ new Set();
313
320
  for (const entry of updates) {
321
+ if (entry.expected_rev !== void 0) {
322
+ const current = getNodeOrThrow(entry.node_id);
323
+ if (current.rev !== entry.expected_rev) {
324
+ throw new EngineError(
325
+ "rev_mismatch",
326
+ `Node ${entry.node_id} has rev ${current.rev}, expected ${entry.expected_rev}. Another agent may have modified it. Re-read and retry.`
327
+ );
328
+ }
329
+ }
314
330
  let evidence = entry.add_evidence;
315
331
  if (entry.resolved_reason) {
316
332
  evidence = [...evidence ?? [], { type: "note", ref: entry.resolved_reason }];
@@ -320,6 +336,8 @@ function handleUpdate(input, agent) {
320
336
  agent,
321
337
  resolved: entry.resolved,
322
338
  discovery: entry.discovery,
339
+ blocked: entry.blocked,
340
+ blocked_reason: entry.blocked_reason,
323
341
  state: entry.state,
324
342
  summary: entry.summary,
325
343
  properties: entry.properties,
@@ -445,6 +463,7 @@ function buildNodeTree(nodeId, currentDepth, maxDepth) {
445
463
  if (children.length === 0) {
446
464
  return tree;
447
465
  }
466
+ tree.progress = getSubtreeProgress(nodeId);
448
467
  if (currentDepth < maxDepth) {
449
468
  tree.children = children.map(
450
469
  (child) => buildNodeTree(child.id, currentDepth + 1, maxDepth)
@@ -517,8 +536,10 @@ function handleQuery(input) {
517
536
  params.push(...descendantIds);
518
537
  }
519
538
  if (filter?.has_evidence_type) {
520
- conditions.push("n.evidence LIKE ?");
521
- params.push(`%"type":"${filter.has_evidence_type}"%`);
539
+ conditions.push(
540
+ `EXISTS (SELECT 1 FROM json_each(n.evidence) WHERE json_extract(value, '$.type') = ?)`
541
+ );
542
+ params.push(filter.has_evidence_type);
522
543
  }
523
544
  if (filter?.is_leaf) {
524
545
  conditions.push(
@@ -527,6 +548,7 @@ function handleQuery(input) {
527
548
  }
528
549
  if (filter?.is_actionable) {
529
550
  conditions.push("n.resolved = 0");
551
+ conditions.push("n.blocked = 0");
530
552
  conditions.push(
531
553
  "NOT EXISTS (SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0)"
532
554
  );
@@ -541,11 +563,11 @@ function handleQuery(input) {
541
563
  if (filter?.is_blocked) {
542
564
  conditions.push("n.resolved = 0");
543
565
  conditions.push(
544
- `EXISTS (
566
+ `(n.blocked = 1 OR EXISTS (
545
567
  SELECT 1 FROM edges e
546
568
  JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
547
569
  WHERE e.from_node = n.id AND e.type = 'depends_on'
548
- )`
570
+ ))`
549
571
  );
550
572
  }
551
573
  if (filter?.properties) {
@@ -637,7 +659,7 @@ function handleNext(input, agent, claimTtlMinutes = 60) {
637
659
  }
638
660
  let query = `
639
661
  SELECT n.* FROM nodes n
640
- WHERE n.project = ? AND n.resolved = 0
662
+ WHERE n.project = ? AND n.resolved = 0 AND n.blocked = 0
641
663
  ${scopeFilter}
642
664
  AND NOT EXISTS (
643
665
  SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0
@@ -716,7 +738,23 @@ function handleNext(input, agent, claimTtlMinutes = 60) {
716
738
  resolved_deps
717
739
  };
718
740
  });
719
- return { nodes: results };
741
+ const claimRows = db.prepare(
742
+ `SELECT id, summary, json_extract(properties, '$._claimed_at') as claimed_at
743
+ FROM nodes
744
+ WHERE project = ? AND resolved = 0
745
+ AND json_extract(properties, '$._claimed_by') = ?
746
+ AND json_extract(properties, '$._claimed_at') > ?
747
+ ORDER BY json_extract(properties, '$._claimed_at') DESC`
748
+ ).all(project, agent, claimCutoff);
749
+ const result = { nodes: results };
750
+ if (claimRows.length > 0) {
751
+ result.your_claims = claimRows.map((r) => ({
752
+ id: r.id,
753
+ summary: r.summary,
754
+ claimed_at: r.claimed_at
755
+ }));
756
+ }
757
+ return result;
720
758
  }
721
759
 
722
760
  // src/tools/restructure.ts
@@ -755,8 +793,12 @@ function handleMove(op, agent) {
755
793
  const db = getDb();
756
794
  const node = getNodeOrThrow(op.node_id);
757
795
  const newParent = getNodeOrThrow(op.new_parent);
796
+ if (node.project !== newParent.project) {
797
+ throw new EngineError("cross_project", `Cannot move node across projects: "${node.project}" \u2192 "${newParent.project}"`);
798
+ }
758
799
  if (wouldCreateParentCycle(op.node_id, op.new_parent)) {
759
- throw new Error(
800
+ throw new EngineError(
801
+ "cycle_detected",
760
802
  `Move would create cycle: ${op.node_id} cannot be moved under ${op.new_parent}`
761
803
  );
762
804
  }
@@ -777,6 +819,9 @@ function handleMerge(op, agent) {
777
819
  const db = getDb();
778
820
  const source = getNodeOrThrow(op.source);
779
821
  const target = getNodeOrThrow(op.target);
822
+ if (source.project !== target.project) {
823
+ throw new EngineError("cross_project", `Cannot merge nodes across projects: "${source.project}" \u2192 "${target.project}"`);
824
+ }
780
825
  const movedChildren = db.prepare("SELECT id FROM nodes WHERE parent = ?").all(op.source);
781
826
  db.prepare("UPDATE nodes SET parent = ?, updated_at = ? WHERE parent = ?").run(
782
827
  op.target,
@@ -821,9 +866,7 @@ function handleMerge(op, agent) {
821
866
  logEvent(op.target, agent, "merged", [
822
867
  { field: "merged_from", before: null, after: op.source }
823
868
  ]);
824
- logEvent(op.source, agent, "merged_into", [
825
- { field: "merged_into", before: null, after: op.target }
826
- ]);
869
+ db.prepare("DELETE FROM events WHERE node_id = ?").run(op.source);
827
870
  db.prepare("DELETE FROM edges WHERE from_node = ? OR to_node = ?").run(
828
871
  op.source,
829
872
  op.source
@@ -854,6 +897,20 @@ function handleDrop(op, agent) {
854
897
  result: `dropped ${allIds.length} node(s): ${op.reason}`
855
898
  };
856
899
  }
900
+ function handleDelete(op, agent) {
901
+ const db = getDb();
902
+ getNodeOrThrow(op.node_id);
903
+ const descendants = getAllDescendants(op.node_id);
904
+ const allIds = [op.node_id, ...descendants];
905
+ const placeholders = allIds.map(() => "?").join(",");
906
+ db.prepare(`DELETE FROM events WHERE node_id IN (${placeholders})`).run(...allIds);
907
+ db.prepare(`DELETE FROM edges WHERE from_node IN (${placeholders}) OR to_node IN (${placeholders})`).run(...allIds, ...allIds);
908
+ db.prepare(`DELETE FROM nodes WHERE id IN (${placeholders})`).run(...allIds);
909
+ return {
910
+ node_id: op.node_id,
911
+ result: `deleted ${allIds.length} node(s)`
912
+ };
913
+ }
857
914
  function handleRestructure(input, agent) {
858
915
  const operations = requireArray(input?.operations, "operations");
859
916
  for (let i = 0; i < operations.length; i++) {
@@ -868,8 +925,10 @@ function handleRestructure(input, agent) {
868
925
  } else if (op.op === "drop") {
869
926
  requireString(op.node_id, `operations[${i}].node_id`);
870
927
  requireString(op.reason, `operations[${i}].reason`);
928
+ } else if (op.op === "delete") {
929
+ requireString(op.node_id, `operations[${i}].node_id`);
871
930
  } else {
872
- throw new Error(`Unknown operation: ${op.op}`);
931
+ throw new EngineError("unknown_op", `Unknown operation: ${op.op}`);
873
932
  }
874
933
  }
875
934
  const db = getDb();
@@ -892,6 +951,10 @@ function handleRestructure(input, agent) {
892
951
  detail = handleDrop(op, agent);
893
952
  project = getNode(op.node_id)?.project ?? project;
894
953
  break;
954
+ case "delete":
955
+ project = getNode(op.node_id)?.project ?? project;
956
+ detail = handleDelete(op, agent);
957
+ break;
895
958
  default:
896
959
  throw new Error(`Unknown operation: ${op.op}`);
897
960
  }
@@ -1082,15 +1145,25 @@ function buildTree(node, currentDepth, maxDepth, stats) {
1082
1145
  resolved: node.resolved,
1083
1146
  properties: node.properties
1084
1147
  };
1085
- if (children.length === 0) return treeNode;
1148
+ let subtotal = 1;
1149
+ let subresolved = node.resolved ? 1 : 0;
1150
+ if (children.length === 0) return { treeNode, subtotal, subresolved };
1086
1151
  if (currentDepth < maxDepth) {
1087
- treeNode.children = children.map(
1088
- (child) => buildTree(child, currentDepth + 1, maxDepth, stats)
1089
- );
1152
+ treeNode.children = [];
1153
+ for (const child of children) {
1154
+ const result = buildTree(child, currentDepth + 1, maxDepth, stats);
1155
+ treeNode.children.push(result.treeNode);
1156
+ subtotal += result.subtotal;
1157
+ subresolved += result.subresolved;
1158
+ }
1090
1159
  } else {
1091
1160
  treeNode.child_count = children.length;
1161
+ const progress = getSubtreeProgress(node.id);
1162
+ subtotal = progress.total;
1163
+ subresolved = progress.resolved;
1092
1164
  }
1093
- return treeNode;
1165
+ treeNode.progress = { resolved: subresolved, total: subtotal };
1166
+ return { treeNode, subtotal, subresolved };
1094
1167
  }
1095
1168
  function handleTree(input) {
1096
1169
  const project = requireString(input?.project, "project");
@@ -1100,10 +1173,10 @@ function handleTree(input) {
1100
1173
  throw new EngineError("project_not_found", `Project not found: ${project}`);
1101
1174
  }
1102
1175
  const stats = { total: 0, resolved: 0 };
1103
- const tree = buildTree(root, 0, depth, stats);
1176
+ const { treeNode } = buildTree(root, 0, depth, stats);
1104
1177
  return {
1105
1178
  project,
1106
- tree,
1179
+ tree: treeNode,
1107
1180
  stats: {
1108
1181
  total: stats.total,
1109
1182
  resolved: stats.resolved,
@@ -1231,7 +1304,7 @@ function checkScope(_tier, scope) {
1231
1304
 
1232
1305
  // src/server.ts
1233
1306
  import { createHash } from "crypto";
1234
- import { mkdirSync, readFileSync } from "fs";
1307
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1235
1308
  import { homedir } from "os";
1236
1309
  import { join, resolve, dirname } from "path";
1237
1310
  import { fileURLToPath } from "url";
@@ -1266,6 +1339,27 @@ async function checkForUpdate() {
1266
1339
  } catch {
1267
1340
  }
1268
1341
  }
1342
+ function checkAndUpdateAgentFile() {
1343
+ try {
1344
+ const projectRoot = resolve(".");
1345
+ const agentPath = join(projectRoot, ".claude", "agents", "graph.md");
1346
+ const latest = handleAgentConfig(PKG_VERSION).agent_file;
1347
+ if (existsSync(agentPath)) {
1348
+ const current = readFileSync(agentPath, "utf-8");
1349
+ if (current === latest) return null;
1350
+ const match = current.match(/^---[\s\S]*?version:\s*(\S+)[\s\S]*?---/);
1351
+ const oldVersion = match?.[1] ?? "unknown";
1352
+ writeFileSync(agentPath, latest, "utf-8");
1353
+ return `[graph] Updated .claude/agents/graph.md (${oldVersion} \u2192 ${PKG_VERSION})`;
1354
+ } else {
1355
+ mkdirSync(dirname(agentPath), { recursive: true });
1356
+ writeFileSync(agentPath, latest, "utf-8");
1357
+ return `[graph] Created .claude/agents/graph.md`;
1358
+ }
1359
+ } catch {
1360
+ return null;
1361
+ }
1362
+ }
1269
1363
  var TOOLS = [
1270
1364
  {
1271
1365
  name: "graph_open",
@@ -1280,6 +1374,10 @@ var TOOLS = [
1280
1374
  goal: {
1281
1375
  type: "string",
1282
1376
  description: "Project goal/description. Used on creation only."
1377
+ },
1378
+ skip_discovery: {
1379
+ type: "boolean",
1380
+ description: "Skip discovery phase \u2014 create project ready for immediate planning. Default false."
1283
1381
  }
1284
1382
  }
1285
1383
  }
@@ -1381,9 +1479,12 @@ var TOOLS = [
1381
1479
  type: "object",
1382
1480
  properties: {
1383
1481
  node_id: { type: "string" },
1482
+ expected_rev: { type: "number", description: "Optimistic concurrency: reject if node's current rev doesn't match. Prevents silent overwrites by concurrent agents." },
1384
1483
  resolved: { type: "boolean" },
1385
1484
  resolved_reason: { type: "string", description: "Shorthand: auto-creates a note evidence entry. Use instead of add_evidence for simple cases." },
1386
1485
  discovery: { type: "string", description: "Discovery phase status: 'pending' or 'done'. Set to 'done' after completing discovery interview." },
1486
+ blocked: { type: "boolean", description: "Manually block/unblock a node. Blocked nodes are skipped by graph_next. Use for external blockers (e.g., waiting on domain purchase, another team)." },
1487
+ blocked_reason: { type: "string", description: "Why the node is blocked. Cleared automatically when unblocking." },
1387
1488
  state: { description: "Agent-defined state, any type" },
1388
1489
  summary: { type: "string" },
1389
1490
  properties: {
@@ -1487,7 +1588,7 @@ var TOOLS = [
1487
1588
  },
1488
1589
  {
1489
1590
  name: "graph_restructure",
1490
- description: "Modify graph structure: move (reparent), merge (combine two nodes), drop (resolve node + subtree with reason). Atomic. Reports newly_actionable nodes.",
1591
+ description: "Modify graph structure: move (reparent), merge (combine two nodes), drop (resolve node + subtree with reason), delete (permanently remove node + subtree). Atomic. Reports newly_actionable nodes.",
1491
1592
  inputSchema: {
1492
1593
  type: "object",
1493
1594
  properties: {
@@ -1498,7 +1599,7 @@ var TOOLS = [
1498
1599
  properties: {
1499
1600
  op: {
1500
1601
  type: "string",
1501
- enum: ["move", "merge", "drop"]
1602
+ enum: ["move", "merge", "drop", "delete"]
1502
1603
  },
1503
1604
  node_id: { type: "string", description: "For move and drop" },
1504
1605
  new_parent: { type: "string", description: "For move" },
@@ -1623,7 +1724,9 @@ async function startServer() {
1623
1724
  { name: "graph", version: PKG_VERSION },
1624
1725
  { capabilities: { tools: {}, resources: {} } }
1625
1726
  );
1626
- checkForUpdate();
1727
+ if (process.env.GRAPH_UPDATE_CHECK === "1") {
1728
+ checkForUpdate();
1729
+ }
1627
1730
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1628
1731
  tools: TOOLS
1629
1732
  }));
@@ -1635,7 +1738,7 @@ async function startServer() {
1635
1738
  case "graph_open": {
1636
1739
  const openArgs = args;
1637
1740
  if (openArgs?.project) {
1638
- const { getProjectRoot: getProjectRoot2 } = await import("./nodes-4OJBNDHG.js");
1741
+ const { getProjectRoot: getProjectRoot2 } = await import("./nodes-YNM6KEK2.js");
1639
1742
  if (!getProjectRoot2(openArgs.project)) {
1640
1743
  checkProjectLimit(tier);
1641
1744
  }
@@ -1646,7 +1749,7 @@ async function startServer() {
1646
1749
  case "graph_plan": {
1647
1750
  const planArgs = args;
1648
1751
  if (planArgs?.nodes?.length > 0) {
1649
- const { getNode: getNode2 } = await import("./nodes-4OJBNDHG.js");
1752
+ const { getNode: getNode2 } = await import("./nodes-YNM6KEK2.js");
1650
1753
  const firstParent = planArgs.nodes[0]?.parent_ref;
1651
1754
  if (firstParent && typeof firstParent === "string" && !planArgs.nodes.some((n) => n.ref === firstParent)) {
1652
1755
  const parentNode = getNode2(firstParent);
@@ -1694,7 +1797,7 @@ async function startServer() {
1694
1797
  result = handleTree(args);
1695
1798
  break;
1696
1799
  case "graph_agent_config":
1697
- result = handleAgentConfig();
1800
+ result = handleAgentConfig(PKG_VERSION);
1698
1801
  break;
1699
1802
  case "graph_knowledge_write":
1700
1803
  checkKnowledgeTier(tier);
@@ -1724,7 +1827,11 @@ async function startServer() {
1724
1827
  { type: "text", text: JSON.stringify(result, null, 2) }
1725
1828
  ];
1726
1829
  if (versionBanner) {
1727
- content.push({ type: "text", text: updateWarning ? `${versionBanner} \u2014 ${updateWarning}` : versionBanner });
1830
+ const agentNote = checkAndUpdateAgentFile();
1831
+ const bannerParts = [versionBanner];
1832
+ if (agentNote) bannerParts.push(agentNote);
1833
+ if (updateWarning) bannerParts.push(updateWarning);
1834
+ content.push({ type: "text", text: bannerParts.join("\n") });
1728
1835
  versionBanner = null;
1729
1836
  updateWarning = null;
1730
1837
  }
@@ -1767,7 +1874,7 @@ async function startServer() {
1767
1874
  }));
1768
1875
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
1769
1876
  try {
1770
- const { listProjects: listProjects2 } = await import("./nodes-4OJBNDHG.js");
1877
+ const { listProjects: listProjects2 } = await import("./nodes-YNM6KEK2.js");
1771
1878
  const projects = listProjects2();
1772
1879
  const resources = projects.flatMap((p) => [
1773
1880
  {
@@ -1838,4 +1945,4 @@ async function startServer() {
1838
1945
  export {
1839
1946
  startServer
1840
1947
  };
1841
- //# sourceMappingURL=server-VKTFTBXC.js.map
1948
+ //# sourceMappingURL=server-X36DXLEG.js.map