@graph-tl/graph 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -13
- package/dist/{chunk-CCGKUMCW.js → chunk-EG42TNON.js} +11 -1
- package/dist/chunk-EG42TNON.js.map +1 -0
- package/dist/{chunk-JRMFXD5I.js → chunk-O4ZUX2AB.js} +45 -9
- package/dist/chunk-O4ZUX2AB.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/{init-VII7APUJ.js → init-WSSOJX4W.js} +2 -2
- package/dist/{nodes-YNM6KEK2.js → nodes-YKHXSWC4.js} +2 -2
- package/dist/{server-X36DXLEG.js → server-77JQI7CU.js} +200 -9
- package/dist/server-77JQI7CU.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-CCGKUMCW.js.map +0 -1
- package/dist/chunk-JRMFXD5I.js.map +0 -1
- package/dist/server-X36DXLEG.js.map +0 -1
- /package/dist/{init-VII7APUJ.js.map → init-WSSOJX4W.js.map} +0 -0
- /package/dist/{nodes-YNM6KEK2.js.map → nodes-YKHXSWC4.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Graph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@graph-tl/graph)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.npmjs.com/package/@graph-tl/graph)
|
|
6
|
+
|
|
7
|
+
Graph gives agents session-to-session memory with actionable next steps.
|
|
4
8
|
|
|
5
9
|
Graph is an MCP server that gives agents persistent memory across sessions. They decompose work into dependency trees, claim tasks, record evidence of what they did, and hand off to the next agent automatically.
|
|
6
10
|
|
|
@@ -35,7 +39,7 @@ Graph gives agents what they actually need:
|
|
|
35
39
|
- **Dependencies with cycle detection** — the engine knows what's blocked and what's ready
|
|
36
40
|
- **Server-side ranking** — one call to get the highest-priority actionable task
|
|
37
41
|
- **Evidence trail** — agents record decisions, commits, and test results so the next agent inherits that knowledge
|
|
38
|
-
-
|
|
42
|
+
- **Minimal overhead** — batched operations and structured responses keep token usage low
|
|
39
43
|
|
|
40
44
|
## How it works
|
|
41
45
|
|
|
@@ -160,22 +164,14 @@ Environment variables (all optional):
|
|
|
160
164
|
|
|
161
165
|
## Token efficiency
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|---|---|---|
|
|
165
|
-
| Onboard to a 30-task project | ~500 | 1 |
|
|
166
|
-
| Plan 4 tasks with dependencies | ~220 | 1 |
|
|
167
|
-
| Get next actionable task | ~300 | 1 |
|
|
168
|
-
| Resolve + see what unblocked | ~120 | 1 |
|
|
169
|
-
| **Full claim-work-resolve cycle** | **~450** | **3** |
|
|
170
|
-
|
|
171
|
-
~90% fewer tokens and ~50% fewer round trips vs traditional tracker MCP integrations.
|
|
167
|
+
Graph is designed to minimize agent overhead. Every operation is a single MCP call with structured, compact responses — no pagination, no field filtering, no extra round trips. Batched operations like `graph_plan` and `graph_update` let agents do more per call, and `graph_onboard` delivers full project context in one shot instead of requiring a sequence of queries.
|
|
172
168
|
|
|
173
169
|
## Data & security
|
|
174
170
|
|
|
175
|
-
|
|
171
|
+
Your data stays on your machine.
|
|
176
172
|
|
|
177
173
|
- **Single SQLite file** in `~/.graph/db/` — outside your repo, nothing to gitignore
|
|
178
|
-
- **
|
|
174
|
+
- **Local-first** — stdio MCP server, no telemetry, no cloud sync. The only network activity is `npx` fetching the package
|
|
179
175
|
- **No secrets stored** — task summaries, evidence notes, and file path references only
|
|
180
176
|
- **You own your data** — back it up, delete it, move it between machines
|
|
181
177
|
|
|
@@ -108,6 +108,7 @@ function migrate(db2) {
|
|
|
108
108
|
db2.exec("ALTER TABLE nodes ADD COLUMN blocked INTEGER NOT NULL DEFAULT 0");
|
|
109
109
|
db2.exec("ALTER TABLE nodes ADD COLUMN blocked_reason TEXT DEFAULT NULL");
|
|
110
110
|
}
|
|
111
|
+
db2.exec("CREATE INDEX IF NOT EXISTS idx_nodes_blocked ON nodes(project, blocked, resolved)");
|
|
111
112
|
}
|
|
112
113
|
function checkpointDb() {
|
|
113
114
|
if (db) {
|
|
@@ -404,6 +405,15 @@ function updateNode(input) {
|
|
|
404
405
|
changes.push({ field: "discovery", before: node.discovery, after: input.discovery });
|
|
405
406
|
newDiscovery = input.discovery;
|
|
406
407
|
}
|
|
408
|
+
if (input.blocked === true && !node.blocked) {
|
|
409
|
+
const reason = input.blocked_reason ?? node.blocked_reason;
|
|
410
|
+
if (!reason) {
|
|
411
|
+
throw new EngineError(
|
|
412
|
+
"blocked_reason_required",
|
|
413
|
+
`Cannot block node ${input.node_id} without a blocked_reason. Provide blocked_reason explaining why this node is blocked.`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
407
417
|
if (input.blocked !== void 0 && input.blocked !== node.blocked) {
|
|
408
418
|
changes.push({ field: "blocked", before: node.blocked, after: input.blocked });
|
|
409
419
|
newBlocked = input.blocked;
|
|
@@ -584,4 +594,4 @@ export {
|
|
|
584
594
|
getSubtreeProgress,
|
|
585
595
|
getProjectSummary
|
|
586
596
|
};
|
|
587
|
-
//# sourceMappingURL=chunk-
|
|
597
|
+
//# sourceMappingURL=chunk-EG42TNON.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nodes.ts","../src/db.ts","../src/events.ts","../src/validate.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport { getDb } from \"./db.js\";\nimport { logEvent } from \"./events.js\";\nimport { EngineError } from \"./validate.js\";\nimport type { Node, NodeRow, Evidence, FieldChange } from \"./types.js\";\n\n// --- Row <-> Node conversion ---\n\nfunction rowToNode(row: NodeRow): Node {\n return {\n id: row.id,\n rev: row.rev,\n parent: row.parent,\n project: row.project,\n summary: row.summary,\n resolved: row.resolved === 1,\n depth: row.depth,\n discovery: row.discovery ?? null,\n blocked: row.blocked === 1,\n blocked_reason: row.blocked_reason ?? null,\n state: row.state ? JSON.parse(row.state) : null,\n properties: JSON.parse(row.properties),\n context_links: JSON.parse(row.context_links),\n evidence: JSON.parse(row.evidence),\n created_by: row.created_by,\n created_at: row.created_at,\n updated_at: row.updated_at,\n };\n}\n\n// --- Create ---\n\nexport interface CreateNodeInput {\n parent?: string;\n project: string;\n summary: string;\n discovery?: string | null;\n state?: unknown;\n properties?: Record<string, unknown>;\n context_links?: string[];\n agent: string;\n}\n\nexport function createNode(input: CreateNodeInput): Node {\n const db = getDb();\n const now = new Date().toISOString();\n const id = nanoid();\n\n // [sl:yBBVr4wcgVfWA_w8U8hQo] Compute depth from parent\n let depth = 0;\n if (input.parent) {\n const parentRow = db.prepare(\"SELECT depth FROM nodes WHERE id = ?\").get(input.parent) as { depth: number } | undefined;\n if (parentRow) depth = parentRow.depth + 1;\n }\n\n const node: Node = {\n id,\n rev: 1,\n parent: input.parent ?? null,\n project: input.project,\n summary: input.summary,\n resolved: false,\n depth,\n discovery: input.discovery ?? null,\n blocked: false,\n blocked_reason: null,\n state: input.state ?? null,\n properties: input.properties ?? {},\n context_links: input.context_links ?? [],\n evidence: [],\n created_by: input.agent,\n created_at: now,\n updated_at: now,\n };\n\n db.prepare(`\n INSERT INTO nodes (id, rev, parent, project, summary, resolved, depth, discovery, blocked, blocked_reason, state, properties, context_links, evidence, created_by, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n node.id,\n node.rev,\n node.parent,\n node.project,\n node.summary,\n 0,\n node.depth,\n node.discovery,\n 0,\n null,\n node.state !== null ? JSON.stringify(node.state) : null,\n JSON.stringify(node.properties),\n JSON.stringify(node.context_links),\n JSON.stringify(node.evidence),\n node.created_by,\n node.created_at,\n node.updated_at\n );\n\n logEvent(node.id, input.agent, \"created\", [\n { field: \"summary\", before: null, after: node.summary },\n ]);\n\n return node;\n}\n\n// --- Read ---\n\nexport function getNode(id: string): Node | null {\n const db = getDb();\n const row = db.prepare(\"SELECT * FROM nodes WHERE id = ?\").get(id) as\n | NodeRow\n | undefined;\n return row ? rowToNode(row) : null;\n}\n\nexport function getNodeOrThrow(id: string): Node {\n const node = getNode(id);\n if (!node) {\n throw new EngineError(\"node_not_found\", `Node not found: ${id}. Verify the ID is correct and the node hasn't been deleted.`);\n }\n return node;\n}\n\nexport function getChildren(parentId: string): Node[] {\n const db = getDb();\n const rows = db\n .prepare(\"SELECT * FROM nodes WHERE parent = ?\")\n .all(parentId) as NodeRow[];\n return rows.map(rowToNode);\n}\n\nexport function getAncestors(nodeId: string): Array<{ id: string; summary: string; resolved: boolean }> {\n const ancestors: Array<{ id: string; summary: string; resolved: boolean }> = [];\n let current = getNode(nodeId);\n\n while (current?.parent) {\n const parent = getNode(current.parent);\n if (!parent) break;\n ancestors.unshift({ id: parent.id, summary: parent.summary, resolved: parent.resolved });\n current = parent;\n }\n\n return ancestors;\n}\n\nexport function getProjectRoot(project: string): Node | null {\n const db = getDb();\n const row = db\n .prepare(\"SELECT * FROM nodes WHERE project = ? AND parent IS NULL\")\n .get(project) as NodeRow | undefined;\n return row ? rowToNode(row) : null;\n}\n\nexport function listProjects(): Array<{\n project: string;\n id: string;\n summary: string;\n total: number;\n resolved: number;\n unresolved: number;\n updated_at: string;\n}> {\n const db = getDb();\n\n const roots = db\n .prepare(\"SELECT * FROM nodes WHERE parent IS NULL\")\n .all() as NodeRow[];\n\n return roots.map((root) => {\n const counts = db\n .prepare(\n `SELECT\n COUNT(*) as total,\n SUM(CASE WHEN resolved = 1 THEN 1 ELSE 0 END) as resolved\n FROM nodes WHERE project = ?`\n )\n .get(root.project) as { total: number; resolved: number };\n\n return {\n project: root.project,\n id: root.id,\n summary: root.summary,\n total: counts.total,\n resolved: counts.resolved,\n unresolved: counts.total - counts.resolved,\n updated_at: root.updated_at,\n };\n });\n}\n\n// --- Update ---\n\nexport interface UpdateNodeInput {\n node_id: string;\n agent: string;\n resolved?: boolean;\n discovery?: string | null;\n blocked?: boolean;\n blocked_reason?: string | null;\n state?: unknown;\n summary?: string;\n properties?: Record<string, unknown>;\n add_context_links?: string[];\n remove_context_links?: string[];\n add_evidence?: Array<{ type: string; ref: string }>;\n}\n\nexport function updateNode(input: UpdateNodeInput): Node {\n const db = getDb();\n const node = getNodeOrThrow(input.node_id);\n const changes: FieldChange[] = [];\n const now = new Date().toISOString();\n\n let newResolved = node.resolved;\n let newDiscovery = node.discovery;\n let newBlocked = node.blocked;\n let newBlockedReason = node.blocked_reason;\n let newState = node.state;\n let newSummary = node.summary;\n let newProperties = { ...node.properties };\n let newContextLinks = [...node.context_links];\n let newEvidence = [...node.evidence];\n\n // [sl:OZ0or-q5TserCEfWUeMVv] Require evidence when resolving\n if (input.resolved === true && !node.resolved) {\n const hasExistingEvidence = node.evidence.length > 0;\n const hasNewEvidence = input.add_evidence && input.add_evidence.length > 0;\n if (!hasExistingEvidence && !hasNewEvidence) {\n throw new EngineError(\n \"evidence_required\",\n `Cannot resolve node ${input.node_id} without evidence. Add at least one add_evidence entry (type: 'git', 'note', 'test', etc.) explaining what was done.`\n );\n }\n }\n\n if (input.resolved !== undefined && input.resolved !== node.resolved) {\n changes.push({ field: \"resolved\", before: node.resolved, after: input.resolved });\n newResolved = input.resolved;\n }\n\n if (input.discovery !== undefined && input.discovery !== node.discovery) {\n changes.push({ field: \"discovery\", before: node.discovery, after: input.discovery });\n newDiscovery = input.discovery;\n }\n\n // Require blocked_reason when blocking a node\n if (input.blocked === true && !node.blocked) {\n const reason = input.blocked_reason ?? node.blocked_reason;\n if (!reason) {\n throw new EngineError(\n \"blocked_reason_required\",\n `Cannot block node ${input.node_id} without a blocked_reason. Provide blocked_reason explaining why this node is blocked.`\n );\n }\n }\n\n if (input.blocked !== undefined && input.blocked !== node.blocked) {\n changes.push({ field: \"blocked\", before: node.blocked, after: input.blocked });\n newBlocked = input.blocked;\n // Clear blocked_reason when unblocking (unless explicitly set)\n if (!input.blocked && input.blocked_reason === undefined) {\n if (node.blocked_reason !== null) {\n changes.push({ field: \"blocked_reason\", before: node.blocked_reason, after: null });\n }\n newBlockedReason = null;\n }\n }\n\n if (input.blocked_reason !== undefined && input.blocked_reason !== node.blocked_reason) {\n changes.push({ field: \"blocked_reason\", before: node.blocked_reason, after: input.blocked_reason });\n newBlockedReason = input.blocked_reason;\n }\n\n if (input.state !== undefined) {\n changes.push({ field: \"state\", before: node.state, after: input.state });\n newState = input.state;\n }\n\n if (input.summary !== undefined && input.summary !== node.summary) {\n changes.push({ field: \"summary\", before: node.summary, after: input.summary });\n newSummary = input.summary;\n }\n\n if (input.properties) {\n for (const [key, value] of Object.entries(input.properties)) {\n if (value === null) {\n if (key in newProperties) {\n changes.push({ field: `properties.${key}`, before: newProperties[key], after: null });\n delete newProperties[key];\n }\n } else {\n changes.push({ field: `properties.${key}`, before: newProperties[key] ?? null, after: value });\n newProperties[key] = value;\n }\n }\n }\n\n if (input.add_context_links) {\n for (const link of input.add_context_links) {\n if (!newContextLinks.includes(link)) {\n newContextLinks.push(link);\n changes.push({ field: \"context_links\", before: null, after: link });\n }\n }\n }\n\n if (input.remove_context_links) {\n for (const link of input.remove_context_links) {\n const idx = newContextLinks.indexOf(link);\n if (idx !== -1) {\n newContextLinks.splice(idx, 1);\n changes.push({ field: \"context_links\", before: link, after: null });\n }\n }\n }\n\n if (input.add_evidence) {\n for (const ev of input.add_evidence) {\n const evidence: Evidence = {\n type: ev.type,\n ref: ev.ref,\n agent: input.agent,\n timestamp: now,\n };\n newEvidence.push(evidence);\n changes.push({ field: \"evidence\", before: null, after: evidence });\n }\n }\n\n if (changes.length === 0) {\n return node;\n }\n\n const newRev = node.rev + 1;\n\n db.prepare(`\n UPDATE nodes SET\n rev = ?,\n resolved = ?,\n discovery = ?,\n blocked = ?,\n blocked_reason = ?,\n state = ?,\n summary = ?,\n properties = ?,\n context_links = ?,\n evidence = ?,\n updated_at = ?\n WHERE id = ?\n `).run(\n newRev,\n newResolved ? 1 : 0,\n newDiscovery,\n newBlocked ? 1 : 0,\n newBlockedReason,\n newState !== null ? JSON.stringify(newState) : null,\n newSummary,\n JSON.stringify(newProperties),\n JSON.stringify(newContextLinks),\n JSON.stringify(newEvidence),\n now,\n input.node_id\n );\n\n const action = input.resolved === true ? \"resolved\" : \"updated\";\n logEvent(input.node_id, input.agent, action, changes);\n\n return getNodeOrThrow(input.node_id);\n}\n\n// --- Progress ---\n\nexport function getSubtreeProgress(nodeId: string): { resolved: number; total: number } {\n const db = getDb();\n const row = db.prepare(\n `WITH RECURSIVE descendants(id) AS (\n SELECT id FROM nodes WHERE id = ?\n UNION ALL\n SELECT n.id FROM nodes n JOIN descendants d ON n.parent = d.id\n )\n SELECT\n COUNT(*) as total,\n SUM(CASE WHEN n.resolved = 1 THEN 1 ELSE 0 END) as resolved\n FROM descendants d JOIN nodes n ON n.id = d.id`\n ).get(nodeId) as { total: number; resolved: number };\n return { resolved: row.resolved, total: row.total };\n}\n\n// --- Query helpers ---\n\nexport function getProjectSummary(project: string): {\n total: number;\n resolved: number;\n unresolved: number;\n blocked: number;\n actionable: number;\n} {\n const db = getDb();\n\n const counts = db\n .prepare(\n `SELECT\n COUNT(*) as total,\n SUM(CASE WHEN resolved = 1 THEN 1 ELSE 0 END) as resolved\n FROM nodes WHERE project = ?`\n )\n .get(project) as { total: number; resolved: number };\n\n // Blocked: unresolved nodes that are manually blocked OR have unresolved dependencies\n const blocked = db\n .prepare(\n `SELECT COUNT(DISTINCT id) as count FROM (\n SELECT n.id FROM nodes n\n WHERE n.project = ? AND n.resolved = 0 AND n.blocked = 1\n UNION\n SELECT n.id FROM nodes n\n JOIN edges e ON e.from_node = n.id AND e.type = 'depends_on'\n JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0\n WHERE n.project = ? AND n.resolved = 0\n )`\n )\n .get(project, project) as { count: number };\n\n // Actionable: unresolved leaves (no unresolved children) with all deps resolved and not manually blocked\n const actionable = db\n .prepare(\n `SELECT COUNT(*) as count FROM nodes n\n WHERE n.project = ? AND n.resolved = 0 AND n.blocked = 0\n AND NOT EXISTS (\n SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0\n )\n AND NOT EXISTS (\n SELECT 1 FROM edges e\n JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0\n WHERE e.from_node = n.id AND e.type = 'depends_on'\n )`\n )\n .get(project) as { count: number };\n\n return {\n total: counts.total,\n resolved: counts.resolved,\n unresolved: counts.total - counts.resolved,\n blocked: blocked.count,\n actionable: actionable.count,\n };\n}\n","import Database from \"better-sqlite3\";\nimport path from \"path\";\n\nlet db: Database.Database;\nlet dbPath: string;\n\nexport function setDbPath(p: string): void {\n dbPath = p;\n}\n\nexport function getDb(): Database.Database {\n if (!db) {\n const resolvedPath = dbPath ?? path.resolve(\"graph.db\");\n db = new Database(resolvedPath);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"synchronous = FULL\");\n db.pragma(\"foreign_keys = ON\");\n migrate(db);\n }\n return db;\n}\n\nexport function initDb(p?: string): Database.Database {\n // Close existing db if any (used by tests to reset state)\n if (db) {\n db.close();\n db = undefined!;\n }\n if (p) dbPath = p;\n return getDb();\n}\n\nfunction migrate(db: Database.Database): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n rev INTEGER NOT NULL DEFAULT 1,\n parent TEXT REFERENCES nodes(id),\n project TEXT NOT NULL,\n summary TEXT NOT NULL,\n resolved INTEGER NOT NULL DEFAULT 0,\n depth INTEGER NOT NULL DEFAULT 0,\n state TEXT,\n properties TEXT NOT NULL DEFAULT '{}',\n context_links TEXT NOT NULL DEFAULT '[]',\n evidence TEXT NOT NULL DEFAULT '[]',\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n from_node TEXT NOT NULL REFERENCES nodes(id),\n to_node TEXT NOT NULL REFERENCES nodes(id),\n type TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE(from_node, to_node, type)\n );\n\n CREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n node_id TEXT NOT NULL REFERENCES nodes(id),\n agent TEXT NOT NULL,\n action TEXT NOT NULL,\n changes TEXT NOT NULL,\n timestamp TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project);\n CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent);\n CREATE INDEX IF NOT EXISTS idx_nodes_resolved ON nodes(project, resolved);\n CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node);\n CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node);\n CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(from_node, type);\n CREATE INDEX IF NOT EXISTS idx_events_node ON events(node_id);\n\n CREATE TABLE IF NOT EXISTS knowledge (\n id TEXT PRIMARY KEY,\n project TEXT NOT NULL,\n key TEXT NOT NULL,\n content TEXT NOT NULL,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(project, key)\n );\n\n CREATE INDEX IF NOT EXISTS idx_knowledge_project ON knowledge(project);\n `);\n\n // [sl:yBBVr4wcgVfWA_w8U8hQo] Migration: add depth column if it doesn't exist\n const hasDepth = db.prepare(\n \"SELECT COUNT(*) as cnt FROM pragma_table_info('nodes') WHERE name = 'depth'\"\n ).get() as { cnt: number };\n\n if (hasDepth.cnt === 0) {\n db.exec(\"ALTER TABLE nodes ADD COLUMN depth INTEGER NOT NULL DEFAULT 0\");\n // Backfill depths using recursive CTE\n db.exec(`\n WITH RECURSIVE tree(id, depth) AS (\n SELECT id, 0 FROM nodes WHERE parent IS NULL\n UNION ALL\n SELECT n.id, t.depth + 1\n FROM nodes n JOIN tree t ON n.parent = t.id\n )\n UPDATE nodes SET depth = (SELECT depth FROM tree WHERE tree.id = nodes.id)\n `);\n }\n\n // [sl:AOXqUIhpW2-gdMqWATf66] Migration: add discovery column if it doesn't exist\n const hasDiscovery = db.prepare(\n \"SELECT COUNT(*) as cnt FROM pragma_table_info('nodes') WHERE name = 'discovery'\"\n ).get() as { cnt: number };\n\n if (hasDiscovery.cnt === 0) {\n db.exec(\"ALTER TABLE nodes ADD COLUMN discovery TEXT DEFAULT NULL\");\n }\n\n // Migration: add blocked/blocked_reason columns if they don't exist\n const hasBlocked = db.prepare(\n \"SELECT COUNT(*) as cnt FROM pragma_table_info('nodes') WHERE name = 'blocked'\"\n ).get() as { cnt: number };\n\n if (hasBlocked.cnt === 0) {\n db.exec(\"ALTER TABLE nodes ADD COLUMN blocked INTEGER NOT NULL DEFAULT 0\");\n db.exec(\"ALTER TABLE nodes ADD COLUMN blocked_reason TEXT DEFAULT NULL\");\n }\n\n // Index on blocked status (must come after blocked column migration)\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_nodes_blocked ON nodes(project, blocked, resolved)\");\n}\n\nexport function checkpointDb(): void {\n if (db) {\n db.pragma(\"wal_checkpoint(TRUNCATE)\");\n }\n}\n\nexport function closeDb(): void {\n if (db) {\n checkpointDb();\n db.close();\n }\n}\n","import { nanoid } from \"nanoid\";\nimport { getDb } from \"./db.js\";\nimport type { FieldChange, Event } from \"./types.js\";\n\nconst INSERT_EVENT = `\n INSERT INTO events (id, node_id, agent, action, changes, timestamp)\n VALUES (?, ?, ?, ?, ?, ?)\n`;\n\nexport function logEvent(\n nodeId: string,\n agent: string,\n action: string,\n changes: FieldChange[]\n): Event {\n const db = getDb();\n const event: Event = {\n id: nanoid(),\n node_id: nodeId,\n agent,\n action,\n changes,\n timestamp: new Date().toISOString(),\n };\n\n db.prepare(INSERT_EVENT).run(\n event.id,\n event.node_id,\n event.agent,\n event.action,\n JSON.stringify(event.changes),\n event.timestamp\n );\n\n return event;\n}\n\nexport function getEvents(\n nodeId: string,\n limit: number = 20,\n cursor?: string\n): { events: Event[]; next_cursor: string | null } {\n const db = getDb();\n\n let query: string;\n let params: unknown[];\n\n if (cursor) {\n query = `\n SELECT * FROM events\n WHERE node_id = ? AND timestamp < ?\n ORDER BY timestamp DESC\n LIMIT ?\n `;\n params = [nodeId, cursor, limit + 1];\n } else {\n query = `\n SELECT * FROM events\n WHERE node_id = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `;\n params = [nodeId, limit + 1];\n }\n\n const rows = db.prepare(query).all(...params) as Array<{\n id: string;\n node_id: string;\n agent: string;\n action: string;\n changes: string;\n timestamp: string;\n }>;\n\n const hasMore = rows.length > limit;\n const slice = hasMore ? rows.slice(0, limit) : rows;\n\n const events: Event[] = slice.map((row) => ({\n id: row.id,\n node_id: row.node_id,\n agent: row.agent,\n action: row.action,\n changes: JSON.parse(row.changes),\n timestamp: row.timestamp,\n }));\n\n return {\n events,\n next_cursor: hasMore ? slice[slice.length - 1].timestamp : null,\n };\n}\n","export class ValidationError extends Error {\n code = \"validation_error\";\n constructor(message: string) {\n super(message);\n this.name = \"ValidationError\";\n }\n}\n\nexport class EngineError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = \"EngineError\";\n this.code = code;\n }\n}\n\nexport function requireString(value: unknown, field: string): string {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new ValidationError(`${field} is required and must be a non-empty string`);\n }\n return value.trim();\n}\n\nexport function optionalString(value: unknown, field: string): string | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value !== \"string\") {\n throw new ValidationError(`${field} must be a string`);\n }\n return value;\n}\n\nexport function requireArray<T>(value: unknown, field: string): T[] {\n if (!Array.isArray(value) || value.length === 0) {\n throw new ValidationError(`${field} is required and must be a non-empty array`);\n }\n return value as T[];\n}\n\nexport function optionalArray<T>(value: unknown, field: string): T[] | undefined {\n if (value === undefined || value === null) return undefined;\n if (!Array.isArray(value)) {\n throw new ValidationError(`${field} must be an array`);\n }\n return value as T[];\n}\n\nexport function optionalNumber(value: unknown, field: string, min?: number, max?: number): number | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value !== \"number\" || isNaN(value)) {\n throw new ValidationError(`${field} must be a number`);\n }\n if (min !== undefined && value < min) {\n throw new ValidationError(`${field} must be >= ${min}`);\n }\n if (max !== undefined && value > max) {\n throw new ValidationError(`${field} must be <= ${max}`);\n }\n return value;\n}\n\nexport function optionalBoolean(value: unknown, field: string): boolean | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value !== \"boolean\") {\n throw new ValidationError(`${field} must be a boolean`);\n }\n return value;\n}\n\nexport function requireObject(value: unknown, field: string): Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new ValidationError(`${field} is required and must be an object`);\n }\n return value as Record<string, unknown>;\n}\n"],"mappings":"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","names":["nanoid","db","db","db","nanoid"]}
|
|
@@ -23,6 +23,15 @@ Read the \`hint\` field first \u2014 it tells you exactly what to do next. Then
|
|
|
23
23
|
|
|
24
24
|
**First-run:** If the tree is empty and discovery is \`"pending"\`, this is a brand new project. Jump directly to DISCOVER below. Do not call graph_next on an empty project.
|
|
25
25
|
|
|
26
|
+
**Drift check:** After onboarding, check for work done outside the graph:
|
|
27
|
+
1. Run \`git log --oneline -10\` to see recent commits
|
|
28
|
+
2. Compare against git evidence in the graph (commit hashes from resolved tasks)
|
|
29
|
+
3. If there are commits not tracked in the graph, surface them to the user:
|
|
30
|
+
- "Found N commits not linked to any graph task: <list>"
|
|
31
|
+
- Ask: add retroactively (create node + evidence), or acknowledge and move on?
|
|
32
|
+
|
|
33
|
+
This catches work done ad-hoc or through plan files that bypassed the graph. It's cheap to run and prevents silent context loss.
|
|
34
|
+
|
|
26
35
|
## 2. DISCOVER (when discovery is pending)
|
|
27
36
|
If the project root or a task node has \`discovery: "pending"\`, you must complete discovery before decomposing it. Discovery is an interview with the user to understand what needs to happen.
|
|
28
37
|
|
|
@@ -65,29 +74,36 @@ Execute the claimed task. While working:
|
|
|
65
74
|
- Build and run tests before considering a task done
|
|
66
75
|
|
|
67
76
|
## 6. RESOLVE
|
|
68
|
-
When done, resolve the task with
|
|
77
|
+
When done, resolve the task with a structured handoff. Every resolution should answer these questions for the next agent:
|
|
78
|
+
|
|
79
|
+
- **What changed** \u2014 what was modified or created
|
|
80
|
+
- **Why** \u2014 reasoning behind the approach taken
|
|
81
|
+
- **Evidence** \u2014 commits, test results, implementation notes
|
|
82
|
+
- **Next action** \u2014 what should happen next (if applicable)
|
|
83
|
+
|
|
69
84
|
\`\`\`
|
|
70
85
|
graph_update({ updates: [{
|
|
71
86
|
node_id: "<task-id>",
|
|
72
87
|
resolved: true,
|
|
73
88
|
add_evidence: [
|
|
74
|
-
{ type: "note", ref: "
|
|
89
|
+
{ type: "note", ref: "Implemented X using Y because Z. Next: wire up the API endpoint." },
|
|
75
90
|
{ type: "git", ref: "<commit-hash> \u2014 <summary>" },
|
|
76
|
-
{ type: "test", ref: "
|
|
91
|
+
{ type: "test", ref: "All 155 tests passing" }
|
|
77
92
|
],
|
|
78
93
|
add_context_links: ["path/to/files/you/touched"]
|
|
79
94
|
}] })
|
|
80
95
|
\`\`\`
|
|
81
|
-
|
|
96
|
+
|
|
97
|
+
Evidence is mandatory. Write notes as if briefing an agent who has never seen the codebase \u2014 they should understand what was done and why without reading the code.
|
|
82
98
|
|
|
83
99
|
## 7. PAUSE
|
|
84
|
-
After resolving a task, STOP.
|
|
85
|
-
- What you just completed
|
|
86
|
-
- What the next actionable task is
|
|
87
|
-
- Wait for the user to say "continue" before claiming the next task
|
|
100
|
+
After resolving a task, STOP. Show the user the project status using \`graph_status\`, then wait for them to say "continue" before claiming the next task.
|
|
88
101
|
|
|
89
102
|
The user controls the pace. Do not auto-claim the next task.
|
|
90
103
|
|
|
104
|
+
## Presenting status
|
|
105
|
+
When showing project state to the user, always use \`graph_status({ project: "..." })\` and output the \`formatted\` field directly. This gives a consistent, readable view. Never format graph data manually \u2014 use the tool.
|
|
106
|
+
|
|
91
107
|
# Rules
|
|
92
108
|
|
|
93
109
|
- NEVER start work without a claimed task
|
|
@@ -98,6 +114,7 @@ The user controls the pace. Do not auto-claim the next task.
|
|
|
98
114
|
- ALWAYS include context_links for files you modified when resolving
|
|
99
115
|
- Parent nodes auto-resolve when all their children are resolved \u2014 you don't need to manually resolve them
|
|
100
116
|
- NEVER skip discovery on nodes with discovery:pending \u2014 the system will block you from decomposing
|
|
117
|
+
- NEVER delete resolved projects \u2014 they are the historical record. Completed projects are lightweight and preserve traceability across sessions
|
|
101
118
|
- If you're approaching context limits, ensure your current task's state is captured (update with evidence even if not fully resolved) so the next agent can pick up where you left off
|
|
102
119
|
|
|
103
120
|
# Record observations proactively
|
|
@@ -113,6 +130,25 @@ Use \`graph_plan\` to add observation nodes under the project root. Keep them li
|
|
|
113
130
|
|
|
114
131
|
Default to "if in doubt, add a node." It's cheap to create and the next session will thank you.
|
|
115
132
|
|
|
133
|
+
# Blocked status
|
|
134
|
+
|
|
135
|
+
Nodes can be manually blocked (separate from dependency-blocked). Use this for external blockers:
|
|
136
|
+
|
|
137
|
+
\`\`\`
|
|
138
|
+
graph_update({ updates: [{
|
|
139
|
+
node_id: "<id>",
|
|
140
|
+
blocked: true,
|
|
141
|
+
blocked_reason: "Waiting on API key from client"
|
|
142
|
+
}] })
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
To unblock: \`graph_update({ updates: [{ node_id: "<id>", blocked: false }] })\`
|
|
146
|
+
|
|
147
|
+
- \`blocked_reason\` is required when setting \`blocked: true\`
|
|
148
|
+
- Blocked nodes won't appear in \`graph_next\` results
|
|
149
|
+
- Unblocking auto-clears the reason
|
|
150
|
+
- Use this for things like: waiting on external input, upstream API down, needs design review
|
|
151
|
+
|
|
116
152
|
# Common mistakes to avoid
|
|
117
153
|
|
|
118
154
|
- Setting dependencies on parent nodes instead of leaf nodes
|
|
@@ -135,4 +171,4 @@ function handleAgentConfig(version) {
|
|
|
135
171
|
export {
|
|
136
172
|
handleAgentConfig
|
|
137
173
|
};
|
|
138
|
-
//# sourceMappingURL=chunk-
|
|
174
|
+
//# sourceMappingURL=chunk-O4ZUX2AB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/agent-config.ts"],"sourcesContent":["// [sl:fV9I7Vel3xT5d_Ws2YHul] Subagent delivery — free for all (retention hook)\n\nfunction agentPrompt(version: string): string {\n return `---\nname: graph\nversion: ${version}\ndescription: Use this agent for tasks tracked in Graph. Enforces the claim-work-resolve workflow — always checks graph_next before working, adds new work to the graph before executing, and resolves with evidence.\ntools: Read, Edit, Write, Bash, Glob, Grep, Task(Explore), AskUserQuestion\nmodel: sonnet\n---\n\nYou are a graph-optimized agent. You execute tasks tracked in a Graph project. Follow this workflow strictly. The human directs, you execute through the graph.\n\n# Workflow\n\n## 1. ORIENT\nOn your first call, orient yourself:\n\\`\\`\\`\ngraph_onboard({ project: \"<project-name>\" })\n\\`\\`\\`\nRead the \\`hint\\` field first — it tells you exactly what to do next. Then read the summary, evidence, knowledge, and actionable tasks.\n\n**First-run:** If the tree is empty and discovery is \\`\"pending\"\\`, this is a brand new project. Jump directly to DISCOVER below. Do not call graph_next on an empty project.\n\n**Drift check:** After onboarding, check for work done outside the graph:\n1. Run \\`git log --oneline -10\\` to see recent commits\n2. Compare against git evidence in the graph (commit hashes from resolved tasks)\n3. If there are commits not tracked in the graph, surface them to the user:\n - \"Found N commits not linked to any graph task: <list>\"\n - Ask: add retroactively (create node + evidence), or acknowledge and move on?\n\nThis catches work done ad-hoc or through plan files that bypassed the graph. It's cheap to run and prevents silent context loss.\n\n## 2. DISCOVER (when discovery is pending)\nIf the project root or a task node has \\`discovery: \"pending\"\\`, you must complete discovery before decomposing it. Discovery is an interview with the user to understand what needs to happen.\n\nUse AskUserQuestion to cover these areas (adapt to what's relevant — skip what's obvious):\n- **Scope** — What exactly needs to happen? What's explicitly out of scope?\n- **Existing patterns** — How does the codebase currently handle similar things? (explore first, then confirm)\n- **Technical approach** — What libraries, APIs, or patterns should we use?\n- **Acceptance criteria** — How will we know it's done? What does success look like?\n\nAfter the interview:\n1. Write findings as knowledge: \\`graph_knowledge_write({ project, key: \"discovery-<topic>\", content: \"...\" })\\`\n2. Flip discovery to done: \\`graph_update({ updates: [{ node_id: \"<id>\", discovery: \"done\" }] })\\`\n3. NOW decompose with graph_plan\n\nDo NOT skip discovery. If you try to add children to a node with \\`discovery: \"pending\"\\`, graph_plan will reject it.\n\n## 3. CLAIM\nGet your next task:\n\\`\\`\\`\ngraph_next({ project: \"<project-name>\", claim: true })\n\\`\\`\\`\nRead the task summary, ancestor chain (for scope), resolved dependencies (for context on what was done before you), and context links (for files to look at).\n\n## 4. PLAN\nIf you discover work that isn't in the graph, add it BEFORE executing:\n\\`\\`\\`\ngraph_plan({ nodes: [{ ref: \"new-work\", parent_ref: \"<parent-id>\", summary: \"...\" }] })\n\\`\\`\\`\nNever execute ad-hoc work. The graph is the source of truth.\n\nWhen decomposing work:\n- Set dependencies on LEAF nodes, not parent nodes. If \"Page A\" depends on \"Layout\", the dependency is from \"Page A\" to \"Layout\", not from the \"Pages\" parent to \"Layout\".\n- Keep tasks small and specific. A task should be completable in one session.\n- Parent nodes are organizational — they resolve when all children resolve. Don't put work in parent nodes.\n\n## 5. WORK\nExecute the claimed task. While working:\n- Annotate key code changes with \\`// [sl:nodeId]\\` where nodeId is the task you're working on\n- This creates a traceable link from code back to the task, its evidence, and its history\n- Build and run tests before considering a task done\n\n## 6. RESOLVE\nWhen done, resolve the task with a structured handoff. Every resolution should answer these questions for the next agent:\n\n- **What changed** — what was modified or created\n- **Why** — reasoning behind the approach taken\n- **Evidence** — commits, test results, implementation notes\n- **Next action** — what should happen next (if applicable)\n\n\\`\\`\\`\ngraph_update({ updates: [{\n node_id: \"<task-id>\",\n resolved: true,\n add_evidence: [\n { type: \"note\", ref: \"Implemented X using Y because Z. Next: wire up the API endpoint.\" },\n { type: \"git\", ref: \"<commit-hash> — <summary>\" },\n { type: \"test\", ref: \"All 155 tests passing\" }\n ],\n add_context_links: [\"path/to/files/you/touched\"]\n}] })\n\\`\\`\\`\n\nEvidence is mandatory. Write notes as if briefing an agent who has never seen the codebase — they should understand what was done and why without reading the code.\n\n## 7. PAUSE\nAfter resolving a task, STOP. Show the user the project status using \\`graph_status\\`, then wait for them to say \"continue\" before claiming the next task.\n\nThe user controls the pace. Do not auto-claim the next task.\n\n## Presenting status\nWhen showing project state to the user, always use \\`graph_status({ project: \"...\" })\\` and output the \\`formatted\\` field directly. This gives a consistent, readable view. Never format graph data manually — use the tool.\n\n# Rules\n\n- NEVER start work without a claimed task\n- NEVER resolve without evidence\n- NEVER execute ad-hoc work — add it to the graph first via graph_plan\n- NEVER auto-continue to the next task — pause and let the user decide\n- ALWAYS build and test before resolving\n- ALWAYS include context_links for files you modified when resolving\n- Parent nodes auto-resolve when all their children are resolved — you don't need to manually resolve them\n- NEVER skip discovery on nodes with discovery:pending — the system will block you from decomposing\n- NEVER delete resolved projects — they are the historical record. Completed projects are lightweight and preserve traceability across sessions\n- If you're approaching context limits, ensure your current task's state is captured (update with evidence even if not fully resolved) so the next agent can pick up where you left off\n\n# Record observations proactively\n\nGraph is the project memory across sessions. If something isn't in Graph, it's effectively forgotten. While working, record things you notice — even if they're not part of your current task:\n\n- **Warnings & errors**: CI failures, deprecation warnings, security vulnerabilities, linter issues\n- **Tech debt**: Code smells, outdated dependencies, missing tests, hardcoded values\n- **Broken things**: Flaky tests, dead links, misconfigured environments\n- **Ideas & improvements**: Performance opportunities, UX issues, missing features\n\nUse \\`graph_plan\\` to add observation nodes under the project root. Keep them lightweight — a clear summary is enough. They can always be dropped later if irrelevant.\n\nDefault to \"if in doubt, add a node.\" It's cheap to create and the next session will thank you.\n\n# Blocked status\n\nNodes can be manually blocked (separate from dependency-blocked). Use this for external blockers:\n\n\\`\\`\\`\ngraph_update({ updates: [{\n node_id: \"<id>\",\n blocked: true,\n blocked_reason: \"Waiting on API key from client\"\n}] })\n\\`\\`\\`\n\nTo unblock: \\`graph_update({ updates: [{ node_id: \"<id>\", blocked: false }] })\\`\n\n- \\`blocked_reason\\` is required when setting \\`blocked: true\\`\n- Blocked nodes won't appear in \\`graph_next\\` results\n- Unblocking auto-clears the reason\n- Use this for things like: waiting on external input, upstream API down, needs design review\n\n# Common mistakes to avoid\n\n- Setting dependencies on parent nodes instead of leaf nodes\n- Running project scaffolding tools (create-next-app, etc.) before planning in the graph\n- Resolving tasks without running tests\n- Doing work that isn't tracked in the graph\n- Continuing to the next task without pausing for user review\n- Trying to decompose a node without completing discovery first\n- Not writing knowledge entries during discovery — future agents need this context\n`;\n}\n\nexport interface AgentConfigResult {\n agent_file: string;\n install_path: string;\n instructions: string;\n}\n\nexport function handleAgentConfig(version: string): AgentConfigResult {\n return {\n agent_file: agentPrompt(version),\n install_path: \".claude/agents/graph.md\",\n instructions:\n \"Save the agent_file content to .claude/agents/graph.md in your project root. \" +\n \"Claude Code will automatically discover it and use it when tasks match the agent description.\",\n };\n}\n"],"mappings":";;;AAEA,SAAS,YAAY,SAAyB;AAC5C,SAAO;AAAA;AAAA,WAEE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2JlB;AAQO,SAAS,kBAAkB,SAAoC;AACpE,SAAO;AAAA,IACL,YAAY,YAAY,OAAO;AAAA,IAC/B,cAAc;AAAA,IACd,cACE;AAAA,EAEJ;AACF;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ if (args[0] === "activate") {
|
|
|
6
6
|
const { activate } = await import("./activate-DSDTR2EJ.js");
|
|
7
7
|
activate(args[1]);
|
|
8
8
|
} else if (args[0] === "init") {
|
|
9
|
-
const { init } = await import("./init-
|
|
9
|
+
const { init } = await import("./init-WSSOJX4W.js");
|
|
10
10
|
init();
|
|
11
11
|
} else {
|
|
12
|
-
const { startServer } = await import("./server-
|
|
12
|
+
const { startServer } = await import("./server-77JQI7CU.js");
|
|
13
13
|
startServer().catch((error) => {
|
|
14
14
|
console.error("Failed to start graph:", error);
|
|
15
15
|
process.exit(1);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleAgentConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-O4ZUX2AB.js";
|
|
5
5
|
|
|
6
6
|
// src/init.ts
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -76,4 +76,4 @@ function init() {
|
|
|
76
76
|
export {
|
|
77
77
|
init
|
|
78
78
|
};
|
|
79
|
-
//# sourceMappingURL=init-
|
|
79
|
+
//# sourceMappingURL=init-WSSOJX4W.js.map
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
getSubtreeProgress,
|
|
11
11
|
listProjects,
|
|
12
12
|
updateNode
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-EG42TNON.js";
|
|
14
14
|
export {
|
|
15
15
|
createNode,
|
|
16
16
|
getAncestors,
|
|
@@ -23,4 +23,4 @@ export {
|
|
|
23
23
|
listProjects,
|
|
24
24
|
updateNode
|
|
25
25
|
};
|
|
26
|
-
//# sourceMappingURL=nodes-
|
|
26
|
+
//# sourceMappingURL=nodes-YKHXSWC4.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-WKOEKYTF.js";
|
|
5
5
|
import {
|
|
6
6
|
handleAgentConfig
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-O4ZUX2AB.js";
|
|
8
8
|
import {
|
|
9
9
|
EngineError,
|
|
10
10
|
ValidationError,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
requireString,
|
|
30
30
|
setDbPath,
|
|
31
31
|
updateNode
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-EG42TNON.js";
|
|
33
33
|
|
|
34
34
|
// src/server.ts
|
|
35
35
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -899,7 +899,19 @@ function handleDrop(op, agent) {
|
|
|
899
899
|
}
|
|
900
900
|
function handleDelete(op, agent) {
|
|
901
901
|
const db = getDb();
|
|
902
|
-
getNodeOrThrow(op.node_id);
|
|
902
|
+
const node = getNodeOrThrow(op.node_id);
|
|
903
|
+
if (node.parent === null) {
|
|
904
|
+
const evidenceCount = db.prepare(
|
|
905
|
+
`SELECT COUNT(*) as cnt FROM nodes
|
|
906
|
+
WHERE project = ? AND evidence != '[]'`
|
|
907
|
+
).get(node.project);
|
|
908
|
+
if (evidenceCount.cnt > 0) {
|
|
909
|
+
throw new EngineError(
|
|
910
|
+
"delete_protected",
|
|
911
|
+
`Cannot delete project "${node.project}" \u2014 it contains ${evidenceCount.cnt} node(s) with evidence. Resolved projects preserve traceability across sessions. Use "drop" to mark as resolved instead.`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
903
915
|
const descendants = getAllDescendants(op.node_id);
|
|
904
916
|
const allIds = [op.node_id, ...descendants];
|
|
905
917
|
const placeholders = allIds.map(() => "?").join(",");
|
|
@@ -1024,7 +1036,7 @@ function handleOnboard(input) {
|
|
|
1024
1036
|
const topChildren = db.prepare("SELECT * FROM nodes WHERE parent = ? ORDER BY created_at ASC").all(root.id);
|
|
1025
1037
|
const tree = topChildren.map((child) => {
|
|
1026
1038
|
const grandchildren = db.prepare(
|
|
1027
|
-
`SELECT id, summary, resolved,
|
|
1039
|
+
`SELECT id, summary, resolved, blocked, blocked_reason,
|
|
1028
1040
|
(SELECT COUNT(*) FROM nodes gc WHERE gc.parent = n.id) as child_count
|
|
1029
1041
|
FROM nodes n WHERE parent = ? ORDER BY created_at ASC`
|
|
1030
1042
|
).all(child.id);
|
|
@@ -1033,10 +1045,14 @@ function handleOnboard(input) {
|
|
|
1033
1045
|
summary: child.summary,
|
|
1034
1046
|
resolved: child.resolved === 1,
|
|
1035
1047
|
discovery: child.discovery,
|
|
1048
|
+
blocked: child.blocked === 1,
|
|
1049
|
+
blocked_reason: child.blocked_reason,
|
|
1036
1050
|
children: grandchildren.map((gc) => ({
|
|
1037
1051
|
id: gc.id,
|
|
1038
1052
|
summary: gc.summary,
|
|
1039
1053
|
resolved: gc.resolved === 1,
|
|
1054
|
+
blocked: gc.blocked === 1,
|
|
1055
|
+
blocked_reason: gc.blocked_reason,
|
|
1040
1056
|
child_count: gc.child_count
|
|
1041
1057
|
}))
|
|
1042
1058
|
};
|
|
@@ -1070,7 +1086,7 @@ function handleOnboard(input) {
|
|
|
1070
1086
|
const knowledgeRows = db.prepare("SELECT key, content, updated_at FROM knowledge WHERE project = ? ORDER BY updated_at DESC").all(project);
|
|
1071
1087
|
const actionableRows = db.prepare(
|
|
1072
1088
|
`SELECT n.id, n.summary, n.properties FROM nodes n
|
|
1073
|
-
WHERE n.project = ? AND n.resolved = 0
|
|
1089
|
+
WHERE n.project = ? AND n.resolved = 0 AND n.blocked = 0
|
|
1074
1090
|
AND NOT EXISTS (
|
|
1075
1091
|
SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0
|
|
1076
1092
|
)
|
|
@@ -1185,6 +1201,165 @@ function handleTree(input) {
|
|
|
1185
1201
|
};
|
|
1186
1202
|
}
|
|
1187
1203
|
|
|
1204
|
+
// src/tools/status.ts
|
|
1205
|
+
function statusIcon(entry) {
|
|
1206
|
+
if (entry.resolved) return "x";
|
|
1207
|
+
if (entry.blocked) return "!";
|
|
1208
|
+
if (entry.dep_blocked) return "~";
|
|
1209
|
+
return " ";
|
|
1210
|
+
}
|
|
1211
|
+
function progressBar(resolved, total, width = 20) {
|
|
1212
|
+
if (total === 0) return "";
|
|
1213
|
+
const clamped = Math.min(resolved, total);
|
|
1214
|
+
const filled = Math.round(clamped / total * width);
|
|
1215
|
+
const empty = width - filled;
|
|
1216
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
1217
|
+
const pct = Math.round(resolved / total * 100);
|
|
1218
|
+
return `${bar} ${resolved}/${total} (${pct}%)`;
|
|
1219
|
+
}
|
|
1220
|
+
function timeAgo(isoDate) {
|
|
1221
|
+
const diff = Date.now() - new Date(isoDate).getTime();
|
|
1222
|
+
const mins = Math.floor(diff / 6e4);
|
|
1223
|
+
if (mins < 1) return "just now";
|
|
1224
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1225
|
+
const hours = Math.floor(mins / 60);
|
|
1226
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1227
|
+
const days = Math.floor(hours / 24);
|
|
1228
|
+
if (days === 1) return "yesterday";
|
|
1229
|
+
return `${days}d ago`;
|
|
1230
|
+
}
|
|
1231
|
+
function handleStatus(input) {
|
|
1232
|
+
const db = getDb();
|
|
1233
|
+
let project = optionalString(input?.project, "project");
|
|
1234
|
+
if (!project) {
|
|
1235
|
+
const projects = listProjects();
|
|
1236
|
+
if (projects.length === 0) {
|
|
1237
|
+
return {
|
|
1238
|
+
projects: [],
|
|
1239
|
+
hint: 'No projects yet. Create one with graph_open({ project: "my-project", goal: "..." }).'
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
if (projects.length === 1) {
|
|
1243
|
+
project = projects[0].project;
|
|
1244
|
+
} else {
|
|
1245
|
+
const lines2 = ["# All Projects", ""];
|
|
1246
|
+
for (const p of projects) {
|
|
1247
|
+
const taskCount2 = p.total > 0 ? p.total - 1 : 0;
|
|
1248
|
+
const resolvedTasks2 = Math.min(p.resolved, taskCount2);
|
|
1249
|
+
const bar = taskCount2 > 0 ? progressBar(resolvedTasks2, taskCount2) : "empty";
|
|
1250
|
+
lines2.push(`**${p.project}** ${bar}`);
|
|
1251
|
+
lines2.push(` ${p.summary}`);
|
|
1252
|
+
lines2.push("");
|
|
1253
|
+
}
|
|
1254
|
+
lines2.push("_Specify a project name for details._");
|
|
1255
|
+
return { projects, hint: lines2.join("\n") };
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const root = getProjectRoot(project);
|
|
1259
|
+
if (!root) {
|
|
1260
|
+
throw new EngineError("project_not_found", `Project not found: ${project}`);
|
|
1261
|
+
}
|
|
1262
|
+
const summary = getProjectSummary(project);
|
|
1263
|
+
const rows = db.prepare(
|
|
1264
|
+
`SELECT n.id, n.parent, n.summary, n.resolved, n.blocked, n.blocked_reason, n.depth,
|
|
1265
|
+
(SELECT COUNT(*) FROM nodes c WHERE c.parent = n.id) as child_count,
|
|
1266
|
+
(SELECT COUNT(*) FROM nodes c WHERE c.parent = n.id AND c.resolved = 1) as resolved_children,
|
|
1267
|
+
(SELECT COUNT(*) FROM edges e
|
|
1268
|
+
JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
|
|
1269
|
+
WHERE e.from_node = n.id AND e.type = 'depends_on') as unresolved_deps
|
|
1270
|
+
FROM nodes n
|
|
1271
|
+
WHERE n.project = ? AND n.parent IS NOT NULL
|
|
1272
|
+
ORDER BY n.depth ASC, n.created_at ASC`
|
|
1273
|
+
).all(project);
|
|
1274
|
+
const entries = rows.map((r) => ({
|
|
1275
|
+
id: r.id,
|
|
1276
|
+
parent: r.parent,
|
|
1277
|
+
summary: r.summary,
|
|
1278
|
+
resolved: r.resolved === 1,
|
|
1279
|
+
blocked: r.blocked === 1,
|
|
1280
|
+
blocked_reason: r.blocked_reason,
|
|
1281
|
+
depth: r.depth - 1,
|
|
1282
|
+
// relative to root
|
|
1283
|
+
child_count: r.child_count,
|
|
1284
|
+
dep_blocked: r.unresolved_deps > 0,
|
|
1285
|
+
resolved_children: r.resolved_children,
|
|
1286
|
+
total_children: r.child_count
|
|
1287
|
+
}));
|
|
1288
|
+
const lines = [];
|
|
1289
|
+
const taskCount = summary.total - 1;
|
|
1290
|
+
lines.push(`# ${project}`);
|
|
1291
|
+
lines.push("");
|
|
1292
|
+
lines.push(root.summary);
|
|
1293
|
+
lines.push("");
|
|
1294
|
+
const resolvedTasks = Math.min(summary.resolved, taskCount);
|
|
1295
|
+
if (taskCount > 0) {
|
|
1296
|
+
lines.push(progressBar(resolvedTasks, taskCount));
|
|
1297
|
+
lines.push(`${summary.actionable} actionable | ${summary.blocked} blocked | ${summary.unresolved - summary.blocked - summary.actionable} waiting`);
|
|
1298
|
+
} else {
|
|
1299
|
+
lines.push("No tasks yet");
|
|
1300
|
+
}
|
|
1301
|
+
lines.push("");
|
|
1302
|
+
if (entries.length > 0) {
|
|
1303
|
+
lines.push("## Tasks");
|
|
1304
|
+
lines.push("");
|
|
1305
|
+
for (const entry of entries) {
|
|
1306
|
+
const icon = statusIcon(entry);
|
|
1307
|
+
const prefix = " ".repeat(entry.depth);
|
|
1308
|
+
let line = `${prefix}[${icon}] ${entry.summary}`;
|
|
1309
|
+
if (entry.child_count > 0) {
|
|
1310
|
+
const pct = Math.round(entry.resolved_children / entry.total_children * 100);
|
|
1311
|
+
line += ` (${entry.resolved_children}/${entry.total_children} \u2014 ${pct}%)`;
|
|
1312
|
+
}
|
|
1313
|
+
if (entry.blocked && entry.blocked_reason) {
|
|
1314
|
+
line += `
|
|
1315
|
+
${prefix} ^ ${entry.blocked_reason}`;
|
|
1316
|
+
}
|
|
1317
|
+
lines.push(line);
|
|
1318
|
+
}
|
|
1319
|
+
lines.push("");
|
|
1320
|
+
}
|
|
1321
|
+
const recentEvents = db.prepare(
|
|
1322
|
+
`SELECT e.node_id, e.agent, e.action, e.timestamp, n.summary as node_summary
|
|
1323
|
+
FROM events e
|
|
1324
|
+
JOIN nodes n ON n.id = e.node_id
|
|
1325
|
+
WHERE n.project = ?
|
|
1326
|
+
ORDER BY e.timestamp DESC
|
|
1327
|
+
LIMIT 5`
|
|
1328
|
+
).all(project);
|
|
1329
|
+
if (recentEvents.length > 0) {
|
|
1330
|
+
lines.push("## Recent Activity");
|
|
1331
|
+
lines.push("");
|
|
1332
|
+
for (const ev of recentEvents) {
|
|
1333
|
+
lines.push(`- ${ev.action} **${ev.node_summary}** (${ev.agent}, ${timeAgo(ev.timestamp)})`);
|
|
1334
|
+
}
|
|
1335
|
+
lines.push("");
|
|
1336
|
+
}
|
|
1337
|
+
const blocked = entries.filter((e) => e.blocked && e.blocked_reason);
|
|
1338
|
+
if (blocked.length > 0) {
|
|
1339
|
+
lines.push("## Blocked");
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
for (const b of blocked) {
|
|
1342
|
+
lines.push(`- **${b.summary}** \u2014 ${b.blocked_reason}`);
|
|
1343
|
+
}
|
|
1344
|
+
lines.push("");
|
|
1345
|
+
}
|
|
1346
|
+
const knowledge = db.prepare(
|
|
1347
|
+
"SELECT key, updated_at FROM knowledge WHERE project = ? ORDER BY updated_at DESC"
|
|
1348
|
+
).all(project);
|
|
1349
|
+
if (knowledge.length > 0) {
|
|
1350
|
+
lines.push("## Knowledge");
|
|
1351
|
+
lines.push("");
|
|
1352
|
+
for (const k of knowledge) {
|
|
1353
|
+
lines.push(`- ${k.key} (${timeAgo(k.updated_at)})`);
|
|
1354
|
+
}
|
|
1355
|
+
lines.push("");
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
formatted: lines.join("\n").trimEnd(),
|
|
1359
|
+
project
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1188
1363
|
// src/tools/knowledge.ts
|
|
1189
1364
|
import { nanoid as nanoid2 } from "nanoid";
|
|
1190
1365
|
function handleKnowledgeWrite(input, agent) {
|
|
@@ -1659,6 +1834,19 @@ var TOOLS = [
|
|
|
1659
1834
|
required: ["project"]
|
|
1660
1835
|
}
|
|
1661
1836
|
},
|
|
1837
|
+
{
|
|
1838
|
+
name: "graph_status",
|
|
1839
|
+
description: "Returns a pre-formatted markdown summary of a project's current state. Use this to present project status to the user \u2014 output the `formatted` field directly. Shows task tree with status tags, actionable items, blocked items, and knowledge entries. Omit project to auto-select or get multi-project overview.",
|
|
1840
|
+
inputSchema: {
|
|
1841
|
+
type: "object",
|
|
1842
|
+
properties: {
|
|
1843
|
+
project: {
|
|
1844
|
+
type: "string",
|
|
1845
|
+
description: "Project name. Omit to auto-select (works when there's exactly one project)."
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1662
1850
|
{
|
|
1663
1851
|
name: "graph_agent_config",
|
|
1664
1852
|
description: "Returns the graph-optimized agent configuration file for Claude Code. Save the returned content to .claude/agents/graph.md to enable the graph workflow agent.",
|
|
@@ -1738,7 +1926,7 @@ async function startServer() {
|
|
|
1738
1926
|
case "graph_open": {
|
|
1739
1927
|
const openArgs = args;
|
|
1740
1928
|
if (openArgs?.project) {
|
|
1741
|
-
const { getProjectRoot: getProjectRoot2 } = await import("./nodes-
|
|
1929
|
+
const { getProjectRoot: getProjectRoot2 } = await import("./nodes-YKHXSWC4.js");
|
|
1742
1930
|
if (!getProjectRoot2(openArgs.project)) {
|
|
1743
1931
|
checkProjectLimit(tier);
|
|
1744
1932
|
}
|
|
@@ -1749,7 +1937,7 @@ async function startServer() {
|
|
|
1749
1937
|
case "graph_plan": {
|
|
1750
1938
|
const planArgs = args;
|
|
1751
1939
|
if (planArgs?.nodes?.length > 0) {
|
|
1752
|
-
const { getNode: getNode2 } = await import("./nodes-
|
|
1940
|
+
const { getNode: getNode2 } = await import("./nodes-YKHXSWC4.js");
|
|
1753
1941
|
const firstParent = planArgs.nodes[0]?.parent_ref;
|
|
1754
1942
|
if (firstParent && typeof firstParent === "string" && !planArgs.nodes.some((n) => n.ref === firstParent)) {
|
|
1755
1943
|
const parentNode = getNode2(firstParent);
|
|
@@ -1796,6 +1984,9 @@ async function startServer() {
|
|
|
1796
1984
|
case "graph_tree":
|
|
1797
1985
|
result = handleTree(args);
|
|
1798
1986
|
break;
|
|
1987
|
+
case "graph_status":
|
|
1988
|
+
result = handleStatus(args);
|
|
1989
|
+
break;
|
|
1799
1990
|
case "graph_agent_config":
|
|
1800
1991
|
result = handleAgentConfig(PKG_VERSION);
|
|
1801
1992
|
break;
|
|
@@ -1874,7 +2065,7 @@ async function startServer() {
|
|
|
1874
2065
|
}));
|
|
1875
2066
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1876
2067
|
try {
|
|
1877
|
-
const { listProjects: listProjects2 } = await import("./nodes-
|
|
2068
|
+
const { listProjects: listProjects2 } = await import("./nodes-YKHXSWC4.js");
|
|
1878
2069
|
const projects = listProjects2();
|
|
1879
2070
|
const resources = projects.flatMap((p) => [
|
|
1880
2071
|
{
|
|
@@ -1945,4 +2136,4 @@ async function startServer() {
|
|
|
1945
2136
|
export {
|
|
1946
2137
|
startServer
|
|
1947
2138
|
};
|
|
1948
|
-
//# sourceMappingURL=server-
|
|
2139
|
+
//# sourceMappingURL=server-77JQI7CU.js.map
|