@versatly/workgraph 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/dist/chunk-OJ6KOGB2.js +2638 -0
- package/dist/chunk-R2MLGBHB.js +6043 -0
- package/dist/cli.js +725 -12
- package/dist/index.d.ts +787 -11
- package/dist/index.js +25 -3
- package/dist/mcp-server.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-65ZMX2WM.js +0 -2846
- package/dist/chunk-E3QU5Y53.js +0 -1062
package/dist/chunk-65ZMX2WM.js
DELETED
|
@@ -1,2846 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __export = (target, all) => {
|
|
3
|
-
for (var name in all)
|
|
4
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// src/mcp-server.ts
|
|
8
|
-
var mcp_server_exports = {};
|
|
9
|
-
__export(mcp_server_exports, {
|
|
10
|
-
createWorkgraphMcpServer: () => createWorkgraphMcpServer,
|
|
11
|
-
startWorkgraphMcpServer: () => startWorkgraphMcpServer
|
|
12
|
-
});
|
|
13
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
|
|
17
|
-
// src/dispatch.ts
|
|
18
|
-
var dispatch_exports = {};
|
|
19
|
-
__export(dispatch_exports, {
|
|
20
|
-
createAndExecuteRun: () => createAndExecuteRun,
|
|
21
|
-
createRun: () => createRun,
|
|
22
|
-
executeRun: () => executeRun,
|
|
23
|
-
followup: () => followup,
|
|
24
|
-
listRuns: () => listRuns,
|
|
25
|
-
logs: () => logs,
|
|
26
|
-
markRun: () => markRun,
|
|
27
|
-
status: () => status,
|
|
28
|
-
stop: () => stop
|
|
29
|
-
});
|
|
30
|
-
import fs6 from "fs";
|
|
31
|
-
import path6 from "path";
|
|
32
|
-
import { randomUUID } from "crypto";
|
|
33
|
-
|
|
34
|
-
// src/ledger.ts
|
|
35
|
-
var ledger_exports = {};
|
|
36
|
-
__export(ledger_exports, {
|
|
37
|
-
activityOf: () => activityOf,
|
|
38
|
-
allClaims: () => allClaims,
|
|
39
|
-
append: () => append,
|
|
40
|
-
blame: () => blame,
|
|
41
|
-
claimsFromIndex: () => claimsFromIndex,
|
|
42
|
-
currentOwner: () => currentOwner,
|
|
43
|
-
historyOf: () => historyOf,
|
|
44
|
-
isClaimed: () => isClaimed,
|
|
45
|
-
ledgerChainStatePath: () => ledgerChainStatePath,
|
|
46
|
-
ledgerIndexPath: () => ledgerIndexPath,
|
|
47
|
-
ledgerPath: () => ledgerPath,
|
|
48
|
-
loadChainState: () => loadChainState,
|
|
49
|
-
loadIndex: () => loadIndex,
|
|
50
|
-
query: () => query,
|
|
51
|
-
readAll: () => readAll,
|
|
52
|
-
readSince: () => readSince,
|
|
53
|
-
rebuildHashChainState: () => rebuildHashChainState,
|
|
54
|
-
rebuildIndex: () => rebuildIndex,
|
|
55
|
-
recent: () => recent,
|
|
56
|
-
verifyHashChain: () => verifyHashChain
|
|
57
|
-
});
|
|
58
|
-
import fs from "fs";
|
|
59
|
-
import path from "path";
|
|
60
|
-
import crypto from "crypto";
|
|
61
|
-
var LEDGER_FILE = ".workgraph/ledger.jsonl";
|
|
62
|
-
var LEDGER_INDEX_FILE = ".workgraph/ledger-index.json";
|
|
63
|
-
var LEDGER_CHAIN_FILE = ".workgraph/ledger-chain.json";
|
|
64
|
-
var LEDGER_INDEX_VERSION = 1;
|
|
65
|
-
var LEDGER_CHAIN_VERSION = 1;
|
|
66
|
-
var LEDGER_GENESIS_HASH = "GENESIS";
|
|
67
|
-
function ledgerPath(workspacePath) {
|
|
68
|
-
return path.join(workspacePath, LEDGER_FILE);
|
|
69
|
-
}
|
|
70
|
-
function ledgerIndexPath(workspacePath) {
|
|
71
|
-
return path.join(workspacePath, LEDGER_INDEX_FILE);
|
|
72
|
-
}
|
|
73
|
-
function ledgerChainStatePath(workspacePath) {
|
|
74
|
-
return path.join(workspacePath, LEDGER_CHAIN_FILE);
|
|
75
|
-
}
|
|
76
|
-
function append(workspacePath, actor, op, target, type, data) {
|
|
77
|
-
const chainState = ensureChainState(workspacePath);
|
|
78
|
-
const baseEntry = {
|
|
79
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
80
|
-
actor,
|
|
81
|
-
op,
|
|
82
|
-
target,
|
|
83
|
-
...type ? { type } : {},
|
|
84
|
-
...data && Object.keys(data).length > 0 ? { data } : {},
|
|
85
|
-
prevHash: chainState.lastHash
|
|
86
|
-
};
|
|
87
|
-
const entry = {
|
|
88
|
-
...baseEntry,
|
|
89
|
-
hash: computeEntryHash(baseEntry)
|
|
90
|
-
};
|
|
91
|
-
const lPath = ledgerPath(workspacePath);
|
|
92
|
-
const dir = path.dirname(lPath);
|
|
93
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
94
|
-
fs.appendFileSync(lPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
95
|
-
updateIndexWithEntry(workspacePath, entry);
|
|
96
|
-
updateChainStateWithEntry(workspacePath, entry);
|
|
97
|
-
return entry;
|
|
98
|
-
}
|
|
99
|
-
function readAll(workspacePath) {
|
|
100
|
-
const lPath = ledgerPath(workspacePath);
|
|
101
|
-
if (!fs.existsSync(lPath)) return [];
|
|
102
|
-
const lines = fs.readFileSync(lPath, "utf-8").split("\n").filter(Boolean);
|
|
103
|
-
return lines.map((line) => JSON.parse(line));
|
|
104
|
-
}
|
|
105
|
-
function readSince(workspacePath, since) {
|
|
106
|
-
return readAll(workspacePath).filter((e) => e.ts >= since);
|
|
107
|
-
}
|
|
108
|
-
function loadIndex(workspacePath) {
|
|
109
|
-
const idxPath = ledgerIndexPath(workspacePath);
|
|
110
|
-
if (!fs.existsSync(idxPath)) return null;
|
|
111
|
-
try {
|
|
112
|
-
const raw = fs.readFileSync(idxPath, "utf-8");
|
|
113
|
-
return JSON.parse(raw);
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function loadChainState(workspacePath) {
|
|
119
|
-
const chainPath = ledgerChainStatePath(workspacePath);
|
|
120
|
-
if (!fs.existsSync(chainPath)) return null;
|
|
121
|
-
try {
|
|
122
|
-
const raw = fs.readFileSync(chainPath, "utf-8");
|
|
123
|
-
return JSON.parse(raw);
|
|
124
|
-
} catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function rebuildIndex(workspacePath) {
|
|
129
|
-
const index = seedIndex();
|
|
130
|
-
const entries = readAll(workspacePath);
|
|
131
|
-
for (const entry of entries) {
|
|
132
|
-
applyClaimMutation(index, entry);
|
|
133
|
-
index.lastEntryTs = entry.ts;
|
|
134
|
-
}
|
|
135
|
-
saveIndex(workspacePath, index);
|
|
136
|
-
return index;
|
|
137
|
-
}
|
|
138
|
-
function rebuildHashChainState(workspacePath) {
|
|
139
|
-
const entries = readAll(workspacePath);
|
|
140
|
-
let rollingHash = LEDGER_GENESIS_HASH;
|
|
141
|
-
for (const entry of entries) {
|
|
142
|
-
const normalized = normalizeEntryForHash(entry, rollingHash);
|
|
143
|
-
rollingHash = computeEntryHash(normalized);
|
|
144
|
-
}
|
|
145
|
-
const chainState = {
|
|
146
|
-
version: LEDGER_CHAIN_VERSION,
|
|
147
|
-
algorithm: "sha256",
|
|
148
|
-
lastHash: rollingHash,
|
|
149
|
-
count: entries.length,
|
|
150
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
151
|
-
};
|
|
152
|
-
saveChainState(workspacePath, chainState);
|
|
153
|
-
return chainState;
|
|
154
|
-
}
|
|
155
|
-
function claimsFromIndex(workspacePath) {
|
|
156
|
-
try {
|
|
157
|
-
const index = loadIndex(workspacePath);
|
|
158
|
-
if (index?.version === LEDGER_INDEX_VERSION) {
|
|
159
|
-
return new Map(Object.entries(index.claims));
|
|
160
|
-
}
|
|
161
|
-
const rebuilt = rebuildIndex(workspacePath);
|
|
162
|
-
return new Map(Object.entries(rebuilt.claims));
|
|
163
|
-
} catch {
|
|
164
|
-
return claimsFromLedger(workspacePath);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function query(workspacePath, options = {}) {
|
|
168
|
-
let entries = readAll(workspacePath);
|
|
169
|
-
if (options.actor) entries = entries.filter((entry) => entry.actor === options.actor);
|
|
170
|
-
if (options.op) entries = entries.filter((entry) => entry.op === options.op);
|
|
171
|
-
if (options.target) entries = entries.filter((entry) => entry.target === options.target);
|
|
172
|
-
if (options.targetIncludes) entries = entries.filter((entry) => entry.target.includes(options.targetIncludes));
|
|
173
|
-
if (options.type) entries = entries.filter((entry) => entry.type === options.type);
|
|
174
|
-
if (options.since) entries = entries.filter((entry) => entry.ts >= options.since);
|
|
175
|
-
if (options.until) entries = entries.filter((entry) => entry.ts <= options.until);
|
|
176
|
-
if (options.offset && options.offset > 0) entries = entries.slice(options.offset);
|
|
177
|
-
if (options.limit && options.limit >= 0) entries = entries.slice(0, options.limit);
|
|
178
|
-
return entries;
|
|
179
|
-
}
|
|
180
|
-
function blame(workspacePath, target) {
|
|
181
|
-
const history = historyOf(workspacePath, target);
|
|
182
|
-
const byActor = /* @__PURE__ */ new Map();
|
|
183
|
-
for (const entry of history) {
|
|
184
|
-
const existing = byActor.get(entry.actor) ?? {
|
|
185
|
-
actor: entry.actor,
|
|
186
|
-
count: 0,
|
|
187
|
-
ops: {},
|
|
188
|
-
lastTs: entry.ts
|
|
189
|
-
};
|
|
190
|
-
existing.count += 1;
|
|
191
|
-
existing.ops[entry.op] = (existing.ops[entry.op] ?? 0) + 1;
|
|
192
|
-
if (entry.ts > existing.lastTs) existing.lastTs = entry.ts;
|
|
193
|
-
byActor.set(entry.actor, existing);
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
target,
|
|
197
|
-
totalEntries: history.length,
|
|
198
|
-
actors: [...byActor.values()].sort((a, b) => b.count - a.count || a.actor.localeCompare(b.actor)),
|
|
199
|
-
latest: history.length > 0 ? history[history.length - 1] : null
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
function verifyHashChain(workspacePath, options = {}) {
|
|
203
|
-
const entries = readAll(workspacePath);
|
|
204
|
-
const warnings = [];
|
|
205
|
-
const issues = [];
|
|
206
|
-
let rollingHash = LEDGER_GENESIS_HASH;
|
|
207
|
-
for (let idx = 0; idx < entries.length; idx++) {
|
|
208
|
-
const entry = entries[idx];
|
|
209
|
-
const entryNumber = idx + 1;
|
|
210
|
-
if (entry.prevHash === void 0) {
|
|
211
|
-
const message = `Entry #${entryNumber} missing prevHash`;
|
|
212
|
-
if (options.strict) issues.push(message);
|
|
213
|
-
else warnings.push(message);
|
|
214
|
-
} else if (entry.prevHash !== rollingHash) {
|
|
215
|
-
issues.push(`Entry #${entryNumber} prevHash mismatch`);
|
|
216
|
-
}
|
|
217
|
-
const normalized = normalizeEntryForHash(entry, rollingHash);
|
|
218
|
-
const expectedHash = computeEntryHash(normalized);
|
|
219
|
-
if (entry.hash === void 0) {
|
|
220
|
-
const message = `Entry #${entryNumber} missing hash`;
|
|
221
|
-
if (options.strict) issues.push(message);
|
|
222
|
-
else warnings.push(message);
|
|
223
|
-
rollingHash = expectedHash;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (entry.hash !== expectedHash) {
|
|
227
|
-
issues.push(`Entry #${entryNumber} hash mismatch`);
|
|
228
|
-
}
|
|
229
|
-
rollingHash = entry.hash;
|
|
230
|
-
}
|
|
231
|
-
const chainState = loadChainState(workspacePath);
|
|
232
|
-
if (chainState) {
|
|
233
|
-
if (chainState.count !== entries.length) {
|
|
234
|
-
issues.push(`Chain state count mismatch: state=${chainState.count} actual=${entries.length}`);
|
|
235
|
-
}
|
|
236
|
-
if (chainState.lastHash !== rollingHash) {
|
|
237
|
-
issues.push("Chain state lastHash mismatch");
|
|
238
|
-
}
|
|
239
|
-
} else if (entries.length > 0) {
|
|
240
|
-
warnings.push("Ledger chain state file missing");
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
ok: issues.length === 0 && (!options.strict || warnings.length === 0),
|
|
244
|
-
entries: entries.length,
|
|
245
|
-
lastHash: rollingHash,
|
|
246
|
-
issues,
|
|
247
|
-
warnings,
|
|
248
|
-
chainState
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
function currentOwner(workspacePath, target) {
|
|
252
|
-
return allClaims(workspacePath).get(target) ?? null;
|
|
253
|
-
}
|
|
254
|
-
function isClaimed(workspacePath, target) {
|
|
255
|
-
return currentOwner(workspacePath, target) !== null;
|
|
256
|
-
}
|
|
257
|
-
function historyOf(workspacePath, target) {
|
|
258
|
-
return readAll(workspacePath).filter((e) => e.target === target);
|
|
259
|
-
}
|
|
260
|
-
function activityOf(workspacePath, actor) {
|
|
261
|
-
return readAll(workspacePath).filter((e) => e.actor === actor);
|
|
262
|
-
}
|
|
263
|
-
function allClaims(workspacePath) {
|
|
264
|
-
return claimsFromIndex(workspacePath);
|
|
265
|
-
}
|
|
266
|
-
function recent(workspacePath, count = 20) {
|
|
267
|
-
const all = readAll(workspacePath);
|
|
268
|
-
return all.slice(-count);
|
|
269
|
-
}
|
|
270
|
-
function updateIndexWithEntry(workspacePath, entry) {
|
|
271
|
-
const index = loadIndex(workspacePath) ?? seedIndex();
|
|
272
|
-
applyClaimMutation(index, entry);
|
|
273
|
-
index.lastEntryTs = entry.ts;
|
|
274
|
-
saveIndex(workspacePath, index);
|
|
275
|
-
}
|
|
276
|
-
function updateChainStateWithEntry(workspacePath, entry) {
|
|
277
|
-
const state = ensureChainState(workspacePath);
|
|
278
|
-
const chainState = {
|
|
279
|
-
version: LEDGER_CHAIN_VERSION,
|
|
280
|
-
algorithm: "sha256",
|
|
281
|
-
lastHash: entry.hash ?? state.lastHash,
|
|
282
|
-
count: state.count + 1,
|
|
283
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
284
|
-
};
|
|
285
|
-
saveChainState(workspacePath, chainState);
|
|
286
|
-
}
|
|
287
|
-
function saveIndex(workspacePath, index) {
|
|
288
|
-
const idxPath = ledgerIndexPath(workspacePath);
|
|
289
|
-
const dir = path.dirname(idxPath);
|
|
290
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
291
|
-
fs.writeFileSync(idxPath, JSON.stringify(index, null, 2) + "\n", "utf-8");
|
|
292
|
-
}
|
|
293
|
-
function saveChainState(workspacePath, state) {
|
|
294
|
-
const chainPath = ledgerChainStatePath(workspacePath);
|
|
295
|
-
const dir = path.dirname(chainPath);
|
|
296
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
297
|
-
fs.writeFileSync(chainPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
298
|
-
}
|
|
299
|
-
function seedIndex() {
|
|
300
|
-
return {
|
|
301
|
-
version: LEDGER_INDEX_VERSION,
|
|
302
|
-
lastEntryTs: "",
|
|
303
|
-
claims: {}
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
function applyClaimMutation(index, entry) {
|
|
307
|
-
if (entry.op === "claim") {
|
|
308
|
-
index.claims[entry.target] = entry.actor;
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
if (entry.op === "release" || entry.op === "done" || entry.op === "cancel") {
|
|
312
|
-
delete index.claims[entry.target];
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function claimsFromLedger(workspacePath) {
|
|
316
|
-
const claims = /* @__PURE__ */ new Map();
|
|
317
|
-
const entries = readAll(workspacePath);
|
|
318
|
-
for (const entry of entries) {
|
|
319
|
-
if (entry.op === "claim") claims.set(entry.target, entry.actor);
|
|
320
|
-
if (entry.op === "release" || entry.op === "done" || entry.op === "cancel") {
|
|
321
|
-
claims.delete(entry.target);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return claims;
|
|
325
|
-
}
|
|
326
|
-
function ensureChainState(workspacePath) {
|
|
327
|
-
const existing = loadChainState(workspacePath);
|
|
328
|
-
if (existing?.version === LEDGER_CHAIN_VERSION) return existing;
|
|
329
|
-
return rebuildHashChainState(workspacePath);
|
|
330
|
-
}
|
|
331
|
-
function normalizeEntryForHash(entry, fallbackPrevHash) {
|
|
332
|
-
return {
|
|
333
|
-
ts: entry.ts,
|
|
334
|
-
actor: entry.actor,
|
|
335
|
-
op: entry.op,
|
|
336
|
-
target: entry.target,
|
|
337
|
-
...entry.type ? { type: entry.type } : {},
|
|
338
|
-
...entry.data ? { data: entry.data } : {},
|
|
339
|
-
prevHash: entry.prevHash ?? fallbackPrevHash
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
function computeEntryHash(entry) {
|
|
343
|
-
const payload = stableStringify({
|
|
344
|
-
ts: entry.ts,
|
|
345
|
-
actor: entry.actor,
|
|
346
|
-
op: entry.op,
|
|
347
|
-
target: entry.target,
|
|
348
|
-
...entry.type ? { type: entry.type } : {},
|
|
349
|
-
...entry.data ? { data: entry.data } : {},
|
|
350
|
-
prevHash: entry.prevHash ?? LEDGER_GENESIS_HASH
|
|
351
|
-
});
|
|
352
|
-
return crypto.createHash("sha256").update(payload).digest("hex");
|
|
353
|
-
}
|
|
354
|
-
function stableStringify(value) {
|
|
355
|
-
if (value === null || typeof value !== "object") {
|
|
356
|
-
return JSON.stringify(value);
|
|
357
|
-
}
|
|
358
|
-
if (Array.isArray(value)) {
|
|
359
|
-
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
360
|
-
}
|
|
361
|
-
const obj = value;
|
|
362
|
-
const keys = Object.keys(obj).sort();
|
|
363
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// src/store.ts
|
|
367
|
-
var store_exports = {};
|
|
368
|
-
__export(store_exports, {
|
|
369
|
-
activeThreads: () => activeThreads,
|
|
370
|
-
blockedThreads: () => blockedThreads,
|
|
371
|
-
create: () => create,
|
|
372
|
-
findByField: () => findByField,
|
|
373
|
-
list: () => list,
|
|
374
|
-
openThreads: () => openThreads,
|
|
375
|
-
read: () => read,
|
|
376
|
-
remove: () => remove,
|
|
377
|
-
threadsInSpace: () => threadsInSpace,
|
|
378
|
-
update: () => update
|
|
379
|
-
});
|
|
380
|
-
import fs5 from "fs";
|
|
381
|
-
import path5 from "path";
|
|
382
|
-
import matter from "gray-matter";
|
|
383
|
-
|
|
384
|
-
// src/registry.ts
|
|
385
|
-
var registry_exports = {};
|
|
386
|
-
__export(registry_exports, {
|
|
387
|
-
defineType: () => defineType,
|
|
388
|
-
extendType: () => extendType,
|
|
389
|
-
getType: () => getType,
|
|
390
|
-
listTypes: () => listTypes,
|
|
391
|
-
loadRegistry: () => loadRegistry,
|
|
392
|
-
registryPath: () => registryPath,
|
|
393
|
-
saveRegistry: () => saveRegistry
|
|
394
|
-
});
|
|
395
|
-
import fs2 from "fs";
|
|
396
|
-
import path2 from "path";
|
|
397
|
-
var REGISTRY_FILE = ".workgraph/registry.json";
|
|
398
|
-
var CURRENT_VERSION = 1;
|
|
399
|
-
var BUILT_IN_TYPES = [
|
|
400
|
-
{
|
|
401
|
-
name: "thread",
|
|
402
|
-
description: "A unit of coordinated work. The core workgraph node.",
|
|
403
|
-
directory: "threads",
|
|
404
|
-
builtIn: true,
|
|
405
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
406
|
-
createdBy: "system",
|
|
407
|
-
fields: {
|
|
408
|
-
title: { type: "string", required: true, description: "What this thread is about" },
|
|
409
|
-
goal: { type: "string", required: true, description: "What success looks like" },
|
|
410
|
-
status: {
|
|
411
|
-
type: "string",
|
|
412
|
-
required: true,
|
|
413
|
-
default: "open",
|
|
414
|
-
enum: ["open", "active", "blocked", "done", "cancelled"],
|
|
415
|
-
description: "open | active | blocked | done | cancelled"
|
|
416
|
-
},
|
|
417
|
-
owner: { type: "string", description: "Agent that claimed this thread" },
|
|
418
|
-
priority: {
|
|
419
|
-
type: "string",
|
|
420
|
-
default: "medium",
|
|
421
|
-
enum: ["urgent", "high", "medium", "low"],
|
|
422
|
-
description: "urgent | high | medium | low"
|
|
423
|
-
},
|
|
424
|
-
deps: { type: "list", default: [], description: "Thread refs this depends on" },
|
|
425
|
-
parent: { type: "ref", refTypes: ["thread"], description: "Parent thread if decomposed from larger thread" },
|
|
426
|
-
space: { type: "ref", refTypes: ["space"], description: "Space ref this thread belongs to" },
|
|
427
|
-
context_refs: { type: "list", default: [], description: "Docs that inform this work" },
|
|
428
|
-
tags: { type: "list", default: [], description: "Freeform tags" },
|
|
429
|
-
created: { type: "date", required: true },
|
|
430
|
-
updated: { type: "date", required: true }
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
name: "space",
|
|
435
|
-
description: "A workspace boundary that groups related threads and sets context.",
|
|
436
|
-
directory: "spaces",
|
|
437
|
-
builtIn: true,
|
|
438
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
439
|
-
createdBy: "system",
|
|
440
|
-
fields: {
|
|
441
|
-
title: { type: "string", required: true, description: "Space name" },
|
|
442
|
-
description: { type: "string", description: "What this space is for" },
|
|
443
|
-
members: { type: "list", default: [], description: "Agent names that participate" },
|
|
444
|
-
thread_refs: { type: "list", default: [], description: "Thread refs in this space" },
|
|
445
|
-
tags: { type: "list", default: [], description: "Freeform tags" },
|
|
446
|
-
created: { type: "date", required: true },
|
|
447
|
-
updated: { type: "date", required: true }
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: "decision",
|
|
452
|
-
description: "A recorded decision with reasoning and context.",
|
|
453
|
-
directory: "decisions",
|
|
454
|
-
builtIn: true,
|
|
455
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
456
|
-
createdBy: "system",
|
|
457
|
-
fields: {
|
|
458
|
-
title: { type: "string", required: true },
|
|
459
|
-
date: { type: "date", required: true },
|
|
460
|
-
status: { type: "string", default: "draft", enum: ["draft", "proposed", "approved", "active", "superseded", "reverted"], description: "draft | proposed | approved | active | superseded | reverted" },
|
|
461
|
-
context_refs: { type: "list", default: [], description: "What informed this decision" },
|
|
462
|
-
tags: { type: "list", default: [] }
|
|
463
|
-
}
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
name: "lesson",
|
|
467
|
-
description: "A captured insight or pattern learned from experience.",
|
|
468
|
-
directory: "lessons",
|
|
469
|
-
builtIn: true,
|
|
470
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
471
|
-
createdBy: "system",
|
|
472
|
-
fields: {
|
|
473
|
-
title: { type: "string", required: true },
|
|
474
|
-
date: { type: "date", required: true },
|
|
475
|
-
confidence: { type: "string", default: "medium", description: "high | medium | low" },
|
|
476
|
-
context_refs: { type: "list", default: [] },
|
|
477
|
-
tags: { type: "list", default: [] }
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
name: "fact",
|
|
482
|
-
description: "A structured piece of knowledge with optional temporal validity.",
|
|
483
|
-
directory: "facts",
|
|
484
|
-
builtIn: true,
|
|
485
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
486
|
-
createdBy: "system",
|
|
487
|
-
fields: {
|
|
488
|
-
subject: { type: "string", required: true },
|
|
489
|
-
predicate: { type: "string", required: true },
|
|
490
|
-
object: { type: "string", required: true },
|
|
491
|
-
confidence: { type: "number", default: 1 },
|
|
492
|
-
valid_from: { type: "date" },
|
|
493
|
-
valid_until: { type: "date" },
|
|
494
|
-
source: { type: "ref", description: "Where this fact came from" }
|
|
495
|
-
}
|
|
496
|
-
},
|
|
497
|
-
{
|
|
498
|
-
name: "agent",
|
|
499
|
-
description: "A registered participant in the workgraph.",
|
|
500
|
-
directory: "agents",
|
|
501
|
-
builtIn: true,
|
|
502
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
503
|
-
createdBy: "system",
|
|
504
|
-
fields: {
|
|
505
|
-
name: { type: "string", required: true },
|
|
506
|
-
role: { type: "string", description: "What this agent specializes in" },
|
|
507
|
-
capabilities: { type: "list", default: [], description: "What this agent can do" },
|
|
508
|
-
active_threads: { type: "list", default: [], description: "Threads currently claimed" },
|
|
509
|
-
last_seen: { type: "date" }
|
|
510
|
-
}
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
name: "skill",
|
|
514
|
-
description: "A reusable agent skill shared through the workgraph workspace.",
|
|
515
|
-
directory: "skills",
|
|
516
|
-
builtIn: true,
|
|
517
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
518
|
-
createdBy: "system",
|
|
519
|
-
fields: {
|
|
520
|
-
title: { type: "string", required: true, description: "Skill title" },
|
|
521
|
-
status: { type: "string", required: true, default: "draft", enum: ["draft", "proposed", "active", "deprecated", "archived"], description: "draft | proposed | active | deprecated | archived" },
|
|
522
|
-
version: { type: "string", default: "0.1.0", template: "semver", description: "Semantic version of this skill" },
|
|
523
|
-
owner: { type: "string", description: "Primary skill owner/maintainer" },
|
|
524
|
-
reviewers: { type: "list", default: [], description: "Reviewers involved in proposal" },
|
|
525
|
-
proposal_thread: { type: "ref", description: "Thread coordinating review/promotion" },
|
|
526
|
-
proposed_at: { type: "date" },
|
|
527
|
-
promoted_at: { type: "date" },
|
|
528
|
-
depends_on: { type: "list", default: [], description: "Skill dependencies by slug or path" },
|
|
529
|
-
distribution: { type: "string", default: "tailscale-shared-vault", description: "Distribution channel for skill usage" },
|
|
530
|
-
tailscale_path: { type: "string", description: "Shared vault path over Tailscale" },
|
|
531
|
-
tags: { type: "list", default: [] },
|
|
532
|
-
created: { type: "date", required: true },
|
|
533
|
-
updated: { type: "date", required: true }
|
|
534
|
-
}
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
name: "onboarding",
|
|
538
|
-
description: "Agent or team onboarding lifecycle primitive.",
|
|
539
|
-
directory: "onboarding",
|
|
540
|
-
builtIn: true,
|
|
541
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
542
|
-
createdBy: "system",
|
|
543
|
-
fields: {
|
|
544
|
-
title: { type: "string", required: true },
|
|
545
|
-
actor: { type: "string", required: true },
|
|
546
|
-
status: { type: "string", default: "active", enum: ["active", "completed", "paused"] },
|
|
547
|
-
spaces: { type: "list", default: [] },
|
|
548
|
-
thread_refs: { type: "list", default: [] },
|
|
549
|
-
board: { type: "ref" },
|
|
550
|
-
command_center: { type: "ref" },
|
|
551
|
-
created: { type: "date", required: true },
|
|
552
|
-
updated: { type: "date", required: true },
|
|
553
|
-
tags: { type: "list", default: [] }
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
{
|
|
557
|
-
name: "policy",
|
|
558
|
-
description: "Governance policy primitive for approvals and guardrails.",
|
|
559
|
-
directory: "policies",
|
|
560
|
-
builtIn: true,
|
|
561
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
562
|
-
createdBy: "system",
|
|
563
|
-
fields: {
|
|
564
|
-
title: { type: "string", required: true },
|
|
565
|
-
status: { type: "string", default: "draft", enum: ["draft", "proposed", "approved", "active", "retired"] },
|
|
566
|
-
scope: { type: "string", default: "workspace" },
|
|
567
|
-
approvers: { type: "list", default: [] },
|
|
568
|
-
created: { type: "date", required: true },
|
|
569
|
-
updated: { type: "date", required: true },
|
|
570
|
-
tags: { type: "list", default: [] }
|
|
571
|
-
}
|
|
572
|
-
},
|
|
573
|
-
{
|
|
574
|
-
name: "incident",
|
|
575
|
-
description: "Incident coordination primitive with gated lifecycle.",
|
|
576
|
-
directory: "incidents",
|
|
577
|
-
builtIn: true,
|
|
578
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
579
|
-
createdBy: "system",
|
|
580
|
-
fields: {
|
|
581
|
-
title: { type: "string", required: true },
|
|
582
|
-
severity: { type: "string", default: "sev3", enum: ["sev0", "sev1", "sev2", "sev3", "sev4"] },
|
|
583
|
-
status: { type: "string", default: "draft", enum: ["draft", "proposed", "approved", "active", "resolved", "closed"] },
|
|
584
|
-
owner: { type: "string" },
|
|
585
|
-
created: { type: "date", required: true },
|
|
586
|
-
updated: { type: "date", required: true },
|
|
587
|
-
tags: { type: "list", default: [] }
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
name: "trigger",
|
|
592
|
-
description: "Event trigger contract with policy-aware activation.",
|
|
593
|
-
directory: "triggers",
|
|
594
|
-
builtIn: true,
|
|
595
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
596
|
-
createdBy: "system",
|
|
597
|
-
fields: {
|
|
598
|
-
title: { type: "string", required: true },
|
|
599
|
-
event: { type: "string", required: true },
|
|
600
|
-
status: { type: "string", default: "draft", enum: ["draft", "proposed", "approved", "active", "paused", "retired"] },
|
|
601
|
-
action: { type: "string", required: true },
|
|
602
|
-
idempotency_scope: { type: "string", default: "event+target" },
|
|
603
|
-
created: { type: "date", required: true },
|
|
604
|
-
updated: { type: "date", required: true },
|
|
605
|
-
tags: { type: "list", default: [] }
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
name: "checkpoint",
|
|
610
|
-
description: "Agent checkpoint/hand-off primitive for orientation continuity.",
|
|
611
|
-
directory: "checkpoints",
|
|
612
|
-
builtIn: true,
|
|
613
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
614
|
-
createdBy: "system",
|
|
615
|
-
fields: {
|
|
616
|
-
title: { type: "string", required: true },
|
|
617
|
-
actor: { type: "string", required: true },
|
|
618
|
-
summary: { type: "string", required: true },
|
|
619
|
-
next: { type: "list", default: [] },
|
|
620
|
-
blocked: { type: "list", default: [] },
|
|
621
|
-
created: { type: "date", required: true },
|
|
622
|
-
updated: { type: "date", required: true },
|
|
623
|
-
tags: { type: "list", default: [] }
|
|
624
|
-
}
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
name: "run",
|
|
628
|
-
description: "Background agent run primitive with lifecycle state.",
|
|
629
|
-
directory: "runs",
|
|
630
|
-
builtIn: true,
|
|
631
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
632
|
-
createdBy: "system",
|
|
633
|
-
fields: {
|
|
634
|
-
title: { type: "string", required: true },
|
|
635
|
-
objective: { type: "string", required: true },
|
|
636
|
-
runtime: { type: "string", required: true },
|
|
637
|
-
status: { type: "string", required: true, default: "queued", enum: ["queued", "running", "succeeded", "failed", "cancelled"] },
|
|
638
|
-
run_id: { type: "string", required: true },
|
|
639
|
-
owner: { type: "string" },
|
|
640
|
-
created: { type: "date", required: true },
|
|
641
|
-
updated: { type: "date", required: true },
|
|
642
|
-
tags: { type: "list", default: [] }
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
];
|
|
646
|
-
function registryPath(workspacePath) {
|
|
647
|
-
return path2.join(workspacePath, REGISTRY_FILE);
|
|
648
|
-
}
|
|
649
|
-
function loadRegistry(workspacePath) {
|
|
650
|
-
const rPath = registryPath(workspacePath);
|
|
651
|
-
if (fs2.existsSync(rPath)) {
|
|
652
|
-
const raw = fs2.readFileSync(rPath, "utf-8");
|
|
653
|
-
const registry = JSON.parse(raw);
|
|
654
|
-
return ensureBuiltIns(registry);
|
|
655
|
-
}
|
|
656
|
-
return seedRegistry();
|
|
657
|
-
}
|
|
658
|
-
function saveRegistry(workspacePath, registry) {
|
|
659
|
-
const rPath = registryPath(workspacePath);
|
|
660
|
-
const dir = path2.dirname(rPath);
|
|
661
|
-
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
662
|
-
fs2.writeFileSync(rPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
663
|
-
}
|
|
664
|
-
function defineType(workspacePath, name, description, fields, actor, directory) {
|
|
665
|
-
const registry = loadRegistry(workspacePath);
|
|
666
|
-
const safeName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
667
|
-
if (registry.types[safeName]?.builtIn) {
|
|
668
|
-
throw new Error(`Cannot redefine built-in type "${safeName}". You can extend it with new fields instead.`);
|
|
669
|
-
}
|
|
670
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
671
|
-
const typeDef = {
|
|
672
|
-
name: safeName,
|
|
673
|
-
description,
|
|
674
|
-
fields: {
|
|
675
|
-
title: { type: "string", required: true },
|
|
676
|
-
created: { type: "date", required: true },
|
|
677
|
-
updated: { type: "date", required: true },
|
|
678
|
-
tags: { type: "list", default: [] },
|
|
679
|
-
...fields
|
|
680
|
-
},
|
|
681
|
-
directory: directory ?? `${safeName}s`,
|
|
682
|
-
builtIn: false,
|
|
683
|
-
createdAt: now,
|
|
684
|
-
createdBy: actor
|
|
685
|
-
};
|
|
686
|
-
registry.types[safeName] = typeDef;
|
|
687
|
-
saveRegistry(workspacePath, registry);
|
|
688
|
-
append(workspacePath, actor, "define", ".workgraph/registry.json", safeName, {
|
|
689
|
-
name: safeName,
|
|
690
|
-
directory: typeDef.directory,
|
|
691
|
-
fields: Object.keys(typeDef.fields)
|
|
692
|
-
});
|
|
693
|
-
return typeDef;
|
|
694
|
-
}
|
|
695
|
-
function getType(workspacePath, name) {
|
|
696
|
-
const registry = loadRegistry(workspacePath);
|
|
697
|
-
return registry.types[name];
|
|
698
|
-
}
|
|
699
|
-
function listTypes(workspacePath) {
|
|
700
|
-
const registry = loadRegistry(workspacePath);
|
|
701
|
-
return Object.values(registry.types);
|
|
702
|
-
}
|
|
703
|
-
function extendType(workspacePath, name, newFields, _actor) {
|
|
704
|
-
const registry = loadRegistry(workspacePath);
|
|
705
|
-
const existing = registry.types[name];
|
|
706
|
-
if (!existing) throw new Error(`Type "${name}" not found in registry.`);
|
|
707
|
-
existing.fields = { ...existing.fields, ...newFields };
|
|
708
|
-
saveRegistry(workspacePath, registry);
|
|
709
|
-
return existing;
|
|
710
|
-
}
|
|
711
|
-
function seedRegistry() {
|
|
712
|
-
const types = {};
|
|
713
|
-
for (const t of BUILT_IN_TYPES) {
|
|
714
|
-
types[t.name] = t;
|
|
715
|
-
}
|
|
716
|
-
return { version: CURRENT_VERSION, types };
|
|
717
|
-
}
|
|
718
|
-
function ensureBuiltIns(registry) {
|
|
719
|
-
for (const t of BUILT_IN_TYPES) {
|
|
720
|
-
if (!registry.types[t.name]) {
|
|
721
|
-
registry.types[t.name] = t;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return registry;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// src/graph.ts
|
|
728
|
-
var graph_exports = {};
|
|
729
|
-
__export(graph_exports, {
|
|
730
|
-
buildWikiLinkGraph: () => buildWikiLinkGraph,
|
|
731
|
-
graphHygieneReport: () => graphHygieneReport,
|
|
732
|
-
graphIndexPath: () => graphIndexPath,
|
|
733
|
-
graphNeighborhood: () => graphNeighborhood,
|
|
734
|
-
readWikiLinkGraphIndex: () => readWikiLinkGraphIndex,
|
|
735
|
-
refreshWikiLinkGraphIndex: () => refreshWikiLinkGraphIndex
|
|
736
|
-
});
|
|
737
|
-
import fs3 from "fs";
|
|
738
|
-
import path3 from "path";
|
|
739
|
-
var GRAPH_INDEX_FILE = ".workgraph/graph-index.json";
|
|
740
|
-
function graphIndexPath(workspacePath) {
|
|
741
|
-
return path3.join(workspacePath, GRAPH_INDEX_FILE);
|
|
742
|
-
}
|
|
743
|
-
function buildWikiLinkGraph(workspacePath) {
|
|
744
|
-
const nodes = listMarkdownFiles(workspacePath);
|
|
745
|
-
const nodeSet = new Set(nodes);
|
|
746
|
-
const edges = [];
|
|
747
|
-
const backlinks = {};
|
|
748
|
-
const outDegree = /* @__PURE__ */ new Map();
|
|
749
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
750
|
-
const brokenLinks = [];
|
|
751
|
-
for (const node of nodes) {
|
|
752
|
-
const content = fs3.readFileSync(path3.join(workspacePath, node), "utf-8");
|
|
753
|
-
const links = extractWikiLinks(content);
|
|
754
|
-
outDegree.set(node, links.length);
|
|
755
|
-
for (const rawLink of links) {
|
|
756
|
-
const target = normalizeWikiRef(rawLink);
|
|
757
|
-
edges.push({ from: node, to: target });
|
|
758
|
-
if (!backlinks[target]) backlinks[target] = [];
|
|
759
|
-
backlinks[target].push(node);
|
|
760
|
-
inDegree.set(target, (inDegree.get(target) ?? 0) + 1);
|
|
761
|
-
if (!nodeSet.has(target) && !target.startsWith("http")) {
|
|
762
|
-
brokenLinks.push({ from: node, to: target });
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
const orphans = nodes.filter((node) => (outDegree.get(node) ?? 0) === 0 && (inDegree.get(node) ?? 0) === 0);
|
|
767
|
-
const hubs = nodes.map((node) => ({
|
|
768
|
-
node,
|
|
769
|
-
degree: (outDegree.get(node) ?? 0) + (inDegree.get(node) ?? 0)
|
|
770
|
-
})).filter((entry) => entry.degree > 0).sort((a, b) => b.degree - a.degree || a.node.localeCompare(b.node)).slice(0, 20);
|
|
771
|
-
return {
|
|
772
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
773
|
-
nodes: nodes.sort(),
|
|
774
|
-
edges,
|
|
775
|
-
backlinks,
|
|
776
|
-
orphans: orphans.sort(),
|
|
777
|
-
brokenLinks,
|
|
778
|
-
hubs
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
function refreshWikiLinkGraphIndex(workspacePath) {
|
|
782
|
-
const graph = buildWikiLinkGraph(workspacePath);
|
|
783
|
-
const indexPath = graphIndexPath(workspacePath);
|
|
784
|
-
const dir = path3.dirname(indexPath);
|
|
785
|
-
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
786
|
-
fs3.writeFileSync(indexPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
787
|
-
return graph;
|
|
788
|
-
}
|
|
789
|
-
function readWikiLinkGraphIndex(workspacePath) {
|
|
790
|
-
const indexPath = graphIndexPath(workspacePath);
|
|
791
|
-
if (!fs3.existsSync(indexPath)) return null;
|
|
792
|
-
try {
|
|
793
|
-
const raw = fs3.readFileSync(indexPath, "utf-8");
|
|
794
|
-
return JSON.parse(raw);
|
|
795
|
-
} catch {
|
|
796
|
-
return null;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
function graphHygieneReport(workspacePath) {
|
|
800
|
-
const graph = buildWikiLinkGraph(workspacePath);
|
|
801
|
-
return {
|
|
802
|
-
generatedAt: graph.generatedAt,
|
|
803
|
-
nodeCount: graph.nodes.length,
|
|
804
|
-
edgeCount: graph.edges.length,
|
|
805
|
-
orphanCount: graph.orphans.length,
|
|
806
|
-
brokenLinkCount: graph.brokenLinks.length,
|
|
807
|
-
hubs: graph.hubs,
|
|
808
|
-
orphans: graph.orphans,
|
|
809
|
-
brokenLinks: graph.brokenLinks
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
function graphNeighborhood(workspacePath, nodeRef, options = {}) {
|
|
813
|
-
const graph = options.refresh ? refreshWikiLinkGraphIndex(workspacePath) : readWikiLinkGraphIndex(workspacePath) ?? buildWikiLinkGraph(workspacePath);
|
|
814
|
-
const node = normalizeWikiRef(nodeRef);
|
|
815
|
-
const outgoing = graph.edges.filter((edge) => edge.from === node).map((edge) => edge.to).sort();
|
|
816
|
-
const incoming = (graph.backlinks[node] ?? []).slice().sort();
|
|
817
|
-
return {
|
|
818
|
-
node,
|
|
819
|
-
exists: graph.nodes.includes(node),
|
|
820
|
-
outgoing,
|
|
821
|
-
incoming
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
function listMarkdownFiles(workspacePath) {
|
|
825
|
-
const output = [];
|
|
826
|
-
const stack = [workspacePath];
|
|
827
|
-
const ignoredDirs = /* @__PURE__ */ new Set([".git", "node_modules", "dist"]);
|
|
828
|
-
while (stack.length > 0) {
|
|
829
|
-
const current = stack.pop();
|
|
830
|
-
const entries = fs3.readdirSync(current, { withFileTypes: true });
|
|
831
|
-
for (const entry of entries) {
|
|
832
|
-
const absPath = path3.join(current, entry.name);
|
|
833
|
-
const relPath = path3.relative(workspacePath, absPath).replace(/\\/g, "/");
|
|
834
|
-
if (entry.isDirectory()) {
|
|
835
|
-
if (ignoredDirs.has(entry.name)) continue;
|
|
836
|
-
stack.push(absPath);
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
840
|
-
output.push(relPath);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
return output;
|
|
844
|
-
}
|
|
845
|
-
function extractWikiLinks(content) {
|
|
846
|
-
const matches = content.matchAll(/\[\[([^[\]]+)\]\]/g);
|
|
847
|
-
const refs = [];
|
|
848
|
-
for (const match of matches) {
|
|
849
|
-
if (match[1]) refs.push(match[1].trim());
|
|
850
|
-
}
|
|
851
|
-
return refs;
|
|
852
|
-
}
|
|
853
|
-
function normalizeWikiRef(rawRef) {
|
|
854
|
-
const primary = rawRef.split("|")[0].trim();
|
|
855
|
-
if (!primary) return primary;
|
|
856
|
-
if (/^https?:\/\//i.test(primary)) return primary;
|
|
857
|
-
return primary.endsWith(".md") ? primary : `${primary}.md`;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// src/policy.ts
|
|
861
|
-
var policy_exports = {};
|
|
862
|
-
__export(policy_exports, {
|
|
863
|
-
canTransitionStatus: () => canTransitionStatus,
|
|
864
|
-
getParty: () => getParty,
|
|
865
|
-
loadPolicyRegistry: () => loadPolicyRegistry,
|
|
866
|
-
policyPath: () => policyPath,
|
|
867
|
-
savePolicyRegistry: () => savePolicyRegistry,
|
|
868
|
-
upsertParty: () => upsertParty
|
|
869
|
-
});
|
|
870
|
-
import fs4 from "fs";
|
|
871
|
-
import path4 from "path";
|
|
872
|
-
var POLICY_FILE = ".workgraph/policy.json";
|
|
873
|
-
var POLICY_VERSION = 1;
|
|
874
|
-
var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["decision", "policy", "incident", "trigger"]);
|
|
875
|
-
function policyPath(workspacePath) {
|
|
876
|
-
return path4.join(workspacePath, POLICY_FILE);
|
|
877
|
-
}
|
|
878
|
-
function loadPolicyRegistry(workspacePath) {
|
|
879
|
-
const pPath = policyPath(workspacePath);
|
|
880
|
-
if (!fs4.existsSync(pPath)) {
|
|
881
|
-
const seeded = seedPolicyRegistry();
|
|
882
|
-
savePolicyRegistry(workspacePath, seeded);
|
|
883
|
-
return seeded;
|
|
884
|
-
}
|
|
885
|
-
try {
|
|
886
|
-
const raw = fs4.readFileSync(pPath, "utf-8");
|
|
887
|
-
const parsed = JSON.parse(raw);
|
|
888
|
-
if (!parsed.version || !parsed.parties) {
|
|
889
|
-
return seedPolicyRegistry();
|
|
890
|
-
}
|
|
891
|
-
return parsed;
|
|
892
|
-
} catch {
|
|
893
|
-
return seedPolicyRegistry();
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
function savePolicyRegistry(workspacePath, registry) {
|
|
897
|
-
const pPath = policyPath(workspacePath);
|
|
898
|
-
const dir = path4.dirname(pPath);
|
|
899
|
-
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
900
|
-
fs4.writeFileSync(pPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
901
|
-
}
|
|
902
|
-
function upsertParty(workspacePath, partyId, updates) {
|
|
903
|
-
const registry = loadPolicyRegistry(workspacePath);
|
|
904
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
905
|
-
const existing = registry.parties[partyId];
|
|
906
|
-
const next = {
|
|
907
|
-
id: partyId,
|
|
908
|
-
roles: updates.roles ?? existing?.roles ?? [],
|
|
909
|
-
capabilities: updates.capabilities ?? existing?.capabilities ?? [],
|
|
910
|
-
createdAt: existing?.createdAt ?? now,
|
|
911
|
-
updatedAt: now
|
|
912
|
-
};
|
|
913
|
-
registry.parties[partyId] = next;
|
|
914
|
-
savePolicyRegistry(workspacePath, registry);
|
|
915
|
-
return next;
|
|
916
|
-
}
|
|
917
|
-
function getParty(workspacePath, partyId) {
|
|
918
|
-
const registry = loadPolicyRegistry(workspacePath);
|
|
919
|
-
return registry.parties[partyId] ?? null;
|
|
920
|
-
}
|
|
921
|
-
function canTransitionStatus(workspacePath, actor, primitiveType, fromStatus, toStatus) {
|
|
922
|
-
if (!fromStatus || !toStatus || fromStatus === toStatus) {
|
|
923
|
-
return { allowed: true };
|
|
924
|
-
}
|
|
925
|
-
if (!SENSITIVE_TYPES.has(primitiveType)) {
|
|
926
|
-
return { allowed: true };
|
|
927
|
-
}
|
|
928
|
-
if (actor === "system") {
|
|
929
|
-
return { allowed: true };
|
|
930
|
-
}
|
|
931
|
-
const needsPromotionCapability = ["approved", "active"].includes(toStatus);
|
|
932
|
-
if (!needsPromotionCapability) {
|
|
933
|
-
return { allowed: true };
|
|
934
|
-
}
|
|
935
|
-
const party = getParty(workspacePath, actor);
|
|
936
|
-
if (!party) {
|
|
937
|
-
return {
|
|
938
|
-
allowed: false,
|
|
939
|
-
reason: `Policy gate blocked transition ${primitiveType}:${fromStatus}->${toStatus}; actor "${actor}" is not a registered party.`
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
const requiredCapabilities = [
|
|
943
|
-
`promote:${primitiveType}`,
|
|
944
|
-
"promote:sensitive"
|
|
945
|
-
];
|
|
946
|
-
const hasCapability = requiredCapabilities.some((cap) => party.capabilities.includes(cap));
|
|
947
|
-
if (!hasCapability) {
|
|
948
|
-
return {
|
|
949
|
-
allowed: false,
|
|
950
|
-
reason: `Policy gate blocked transition ${primitiveType}:${fromStatus}->${toStatus}; actor "${actor}" lacks required capabilities (${requiredCapabilities.join(" or ")}).`
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
return { allowed: true };
|
|
954
|
-
}
|
|
955
|
-
function seedPolicyRegistry() {
|
|
956
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
957
|
-
return {
|
|
958
|
-
version: POLICY_VERSION,
|
|
959
|
-
parties: {
|
|
960
|
-
system: {
|
|
961
|
-
id: "system",
|
|
962
|
-
roles: ["admin"],
|
|
963
|
-
capabilities: ["promote:sensitive", "dispatch:run", "policy:manage"],
|
|
964
|
-
createdAt: now,
|
|
965
|
-
updatedAt: now
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// src/store.ts
|
|
972
|
-
function create(workspacePath, typeName, fields, body, actor, options = {}) {
|
|
973
|
-
const typeDef = getType(workspacePath, typeName);
|
|
974
|
-
if (!typeDef) {
|
|
975
|
-
throw new Error(`Unknown primitive type "${typeName}". Run \`workgraph primitive list\` to see available types, or \`workgraph primitive define\` to create one.`);
|
|
976
|
-
}
|
|
977
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
978
|
-
const merged = applyDefaults(typeDef, {
|
|
979
|
-
...fields,
|
|
980
|
-
created: fields.created ?? now,
|
|
981
|
-
updated: now
|
|
982
|
-
});
|
|
983
|
-
const initialStatus = typeof merged.status === "string" ? String(merged.status) : void 0;
|
|
984
|
-
const createPolicyDecision = canTransitionStatus(
|
|
985
|
-
workspacePath,
|
|
986
|
-
actor,
|
|
987
|
-
typeName,
|
|
988
|
-
"draft",
|
|
989
|
-
initialStatus ?? "draft"
|
|
990
|
-
);
|
|
991
|
-
if (!createPolicyDecision.allowed) {
|
|
992
|
-
throw new Error(createPolicyDecision.reason ?? "Policy gate blocked create transition.");
|
|
993
|
-
}
|
|
994
|
-
validateFields(workspacePath, typeDef, merged, "create");
|
|
995
|
-
const relDir = typeDef.directory;
|
|
996
|
-
const slug = slugify(String(merged.title ?? merged.name ?? typeName));
|
|
997
|
-
const relPath = resolveCreatePath(relDir, slug, options.pathOverride);
|
|
998
|
-
const absDir = path5.dirname(path5.join(workspacePath, relPath));
|
|
999
|
-
const absPath = path5.join(workspacePath, relPath);
|
|
1000
|
-
if (!fs5.existsSync(absDir)) fs5.mkdirSync(absDir, { recursive: true });
|
|
1001
|
-
if (fs5.existsSync(absPath)) {
|
|
1002
|
-
throw new Error(`File already exists: ${relPath}. Use update instead.`);
|
|
1003
|
-
}
|
|
1004
|
-
const content = matter.stringify(body, stripUndefined(merged));
|
|
1005
|
-
fs5.writeFileSync(absPath, content, "utf-8");
|
|
1006
|
-
append(workspacePath, actor, "create", relPath, typeName, {
|
|
1007
|
-
title: merged.title ?? slug,
|
|
1008
|
-
...typeof merged.status === "string" ? { status: merged.status } : {}
|
|
1009
|
-
});
|
|
1010
|
-
refreshWikiLinkGraphIndex(workspacePath);
|
|
1011
|
-
return { path: relPath, type: typeName, fields: merged, body };
|
|
1012
|
-
}
|
|
1013
|
-
function read(workspacePath, relPath) {
|
|
1014
|
-
const absPath = path5.join(workspacePath, relPath);
|
|
1015
|
-
if (!fs5.existsSync(absPath)) return null;
|
|
1016
|
-
const raw = fs5.readFileSync(absPath, "utf-8");
|
|
1017
|
-
const { data, content } = matter(raw);
|
|
1018
|
-
const typeName = inferType(workspacePath, relPath);
|
|
1019
|
-
return { path: relPath, type: typeName, fields: data, body: content.trim() };
|
|
1020
|
-
}
|
|
1021
|
-
function list(workspacePath, typeName) {
|
|
1022
|
-
const typeDef = getType(workspacePath, typeName);
|
|
1023
|
-
if (!typeDef) return [];
|
|
1024
|
-
const dir = path5.join(workspacePath, typeDef.directory);
|
|
1025
|
-
if (!fs5.existsSync(dir)) return [];
|
|
1026
|
-
const files = listMarkdownFilesRecursive(dir);
|
|
1027
|
-
const instances = [];
|
|
1028
|
-
for (const file of files) {
|
|
1029
|
-
const relPath = path5.relative(workspacePath, file).replace(/\\/g, "/");
|
|
1030
|
-
const inst = read(workspacePath, relPath);
|
|
1031
|
-
if (inst) instances.push(inst);
|
|
1032
|
-
}
|
|
1033
|
-
return instances;
|
|
1034
|
-
}
|
|
1035
|
-
function update(workspacePath, relPath, fieldUpdates, bodyUpdate, actor) {
|
|
1036
|
-
const existing = read(workspacePath, relPath);
|
|
1037
|
-
if (!existing) throw new Error(`Not found: ${relPath}`);
|
|
1038
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1039
|
-
const newFields = { ...existing.fields, ...fieldUpdates, updated: now };
|
|
1040
|
-
const typeDef = getType(workspacePath, existing.type);
|
|
1041
|
-
if (!typeDef) throw new Error(`Unknown primitive type "${existing.type}" for ${relPath}`);
|
|
1042
|
-
const previousStatus = typeof existing.fields["status"] === "string" ? String(existing.fields["status"]) : void 0;
|
|
1043
|
-
const nextStatus = typeof newFields["status"] === "string" ? String(newFields["status"]) : void 0;
|
|
1044
|
-
const transitionDecision = canTransitionStatus(
|
|
1045
|
-
workspacePath,
|
|
1046
|
-
actor,
|
|
1047
|
-
existing.type,
|
|
1048
|
-
previousStatus,
|
|
1049
|
-
nextStatus
|
|
1050
|
-
);
|
|
1051
|
-
if (!transitionDecision.allowed) {
|
|
1052
|
-
throw new Error(transitionDecision.reason ?? "Policy gate blocked status transition.");
|
|
1053
|
-
}
|
|
1054
|
-
validateFields(workspacePath, typeDef, newFields, "update");
|
|
1055
|
-
const newBody = bodyUpdate ?? existing.body;
|
|
1056
|
-
const absPath = path5.join(workspacePath, relPath);
|
|
1057
|
-
const content = matter.stringify(newBody, stripUndefined(newFields));
|
|
1058
|
-
fs5.writeFileSync(absPath, content, "utf-8");
|
|
1059
|
-
append(workspacePath, actor, "update", relPath, existing.type, {
|
|
1060
|
-
changed: Object.keys(fieldUpdates),
|
|
1061
|
-
...previousStatus !== nextStatus && nextStatus ? {
|
|
1062
|
-
from_status: previousStatus ?? null,
|
|
1063
|
-
to_status: nextStatus
|
|
1064
|
-
} : {}
|
|
1065
|
-
});
|
|
1066
|
-
refreshWikiLinkGraphIndex(workspacePath);
|
|
1067
|
-
return { path: relPath, type: existing.type, fields: newFields, body: newBody };
|
|
1068
|
-
}
|
|
1069
|
-
function remove(workspacePath, relPath, actor) {
|
|
1070
|
-
const absPath = path5.join(workspacePath, relPath);
|
|
1071
|
-
if (!fs5.existsSync(absPath)) throw new Error(`Not found: ${relPath}`);
|
|
1072
|
-
const archiveDir = path5.join(workspacePath, ".workgraph", "archive");
|
|
1073
|
-
if (!fs5.existsSync(archiveDir)) fs5.mkdirSync(archiveDir, { recursive: true });
|
|
1074
|
-
const archivePath = path5.join(archiveDir, path5.basename(relPath));
|
|
1075
|
-
fs5.renameSync(absPath, archivePath);
|
|
1076
|
-
const typeName = inferType(workspacePath, relPath);
|
|
1077
|
-
append(workspacePath, actor, "delete", relPath, typeName);
|
|
1078
|
-
refreshWikiLinkGraphIndex(workspacePath);
|
|
1079
|
-
}
|
|
1080
|
-
function findByField(workspacePath, typeName, field, value) {
|
|
1081
|
-
return list(workspacePath, typeName).filter((inst) => inst.fields[field] === value);
|
|
1082
|
-
}
|
|
1083
|
-
function openThreads(workspacePath) {
|
|
1084
|
-
return findByField(workspacePath, "thread", "status", "open");
|
|
1085
|
-
}
|
|
1086
|
-
function activeThreads(workspacePath) {
|
|
1087
|
-
return findByField(workspacePath, "thread", "status", "active");
|
|
1088
|
-
}
|
|
1089
|
-
function blockedThreads(workspacePath) {
|
|
1090
|
-
return findByField(workspacePath, "thread", "status", "blocked");
|
|
1091
|
-
}
|
|
1092
|
-
function threadsInSpace(workspacePath, spaceRef) {
|
|
1093
|
-
const normalizedTarget = normalizeRefPath(spaceRef);
|
|
1094
|
-
return list(workspacePath, "thread").filter((thread) => {
|
|
1095
|
-
const rawSpace = thread.fields.space;
|
|
1096
|
-
if (!rawSpace) return false;
|
|
1097
|
-
return normalizeRefPath(rawSpace) === normalizedTarget;
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
function slugify(text) {
|
|
1101
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
1102
|
-
}
|
|
1103
|
-
function resolveCreatePath(directory, slug, pathOverride) {
|
|
1104
|
-
if (!pathOverride) {
|
|
1105
|
-
return `${directory}/${slug}.md`;
|
|
1106
|
-
}
|
|
1107
|
-
const normalized = pathOverride.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1108
|
-
const withExtension = normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
1109
|
-
if (!withExtension.startsWith(`${directory}/`)) {
|
|
1110
|
-
throw new Error(`Invalid create path override "${pathOverride}". Must stay under "${directory}/".`);
|
|
1111
|
-
}
|
|
1112
|
-
return withExtension;
|
|
1113
|
-
}
|
|
1114
|
-
function listMarkdownFilesRecursive(rootDir) {
|
|
1115
|
-
const output = [];
|
|
1116
|
-
const stack = [rootDir];
|
|
1117
|
-
while (stack.length > 0) {
|
|
1118
|
-
const current = stack.pop();
|
|
1119
|
-
const entries = fs5.readdirSync(current, { withFileTypes: true });
|
|
1120
|
-
for (const entry of entries) {
|
|
1121
|
-
const absPath = path5.join(current, entry.name);
|
|
1122
|
-
if (entry.isDirectory()) {
|
|
1123
|
-
stack.push(absPath);
|
|
1124
|
-
continue;
|
|
1125
|
-
}
|
|
1126
|
-
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1127
|
-
output.push(absPath);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
return output;
|
|
1132
|
-
}
|
|
1133
|
-
function applyDefaults(typeDef, fields) {
|
|
1134
|
-
const result = { ...fields };
|
|
1135
|
-
for (const [key, def] of Object.entries(typeDef.fields)) {
|
|
1136
|
-
if (result[key] === void 0 && def.default !== void 0) {
|
|
1137
|
-
result[key] = def.default;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
return result;
|
|
1141
|
-
}
|
|
1142
|
-
function stripUndefined(obj) {
|
|
1143
|
-
const result = {};
|
|
1144
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
1145
|
-
if (v !== void 0) result[k] = v;
|
|
1146
|
-
}
|
|
1147
|
-
return result;
|
|
1148
|
-
}
|
|
1149
|
-
function inferType(workspacePath, relPath) {
|
|
1150
|
-
const registry = loadRegistry(workspacePath);
|
|
1151
|
-
const dir = relPath.split("/")[0];
|
|
1152
|
-
for (const typeDef of Object.values(registry.types)) {
|
|
1153
|
-
if (typeDef.directory === dir) return typeDef.name;
|
|
1154
|
-
}
|
|
1155
|
-
return "unknown";
|
|
1156
|
-
}
|
|
1157
|
-
function normalizeRefPath(value) {
|
|
1158
|
-
const raw = String(value ?? "").trim();
|
|
1159
|
-
if (!raw) return "";
|
|
1160
|
-
const unwrapped = raw.startsWith("[[") && raw.endsWith("]]") ? raw.slice(2, -2) : raw;
|
|
1161
|
-
return unwrapped.endsWith(".md") ? unwrapped : `${unwrapped}.md`;
|
|
1162
|
-
}
|
|
1163
|
-
function validateFields(workspacePath, typeDef, fields, mode) {
|
|
1164
|
-
const issues = [];
|
|
1165
|
-
for (const [fieldName, definition] of Object.entries(typeDef.fields)) {
|
|
1166
|
-
const value = fields[fieldName];
|
|
1167
|
-
if (definition.required && isMissingRequiredValue(value)) {
|
|
1168
|
-
issues.push(`Missing required field "${fieldName}"`);
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
if (value === void 0 || value === null) continue;
|
|
1172
|
-
if (!isFieldTypeCompatible(definition.type, value)) {
|
|
1173
|
-
issues.push(`Field "${fieldName}" expected ${definition.type}, got ${describeValue(value)}`);
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
if (definition.enum && definition.enum.length > 0 && !definition.enum.includes(value)) {
|
|
1177
|
-
issues.push(`Field "${fieldName}" must be one of [${definition.enum.join(", ")}]`);
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
if (definition.template && typeof value === "string" && !matchesTemplate(definition.template, value)) {
|
|
1181
|
-
issues.push(`Field "${fieldName}" does not satisfy template "${definition.template}"`);
|
|
1182
|
-
continue;
|
|
1183
|
-
}
|
|
1184
|
-
if (definition.pattern && typeof value === "string") {
|
|
1185
|
-
let expression;
|
|
1186
|
-
try {
|
|
1187
|
-
expression = new RegExp(definition.pattern);
|
|
1188
|
-
} catch {
|
|
1189
|
-
issues.push(`Field "${fieldName}" has invalid pattern "${definition.pattern}"`);
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1192
|
-
if (!expression.test(value)) {
|
|
1193
|
-
issues.push(`Field "${fieldName}" does not match pattern "${definition.pattern}"`);
|
|
1194
|
-
continue;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
if (definition.type === "ref" && typeof value === "string") {
|
|
1198
|
-
const refValidation = validateRefValue(workspacePath, value, definition.refTypes);
|
|
1199
|
-
if (!refValidation.ok) {
|
|
1200
|
-
issues.push(refValidation.reason);
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
if (issues.length > 0) {
|
|
1206
|
-
throw new Error(`Invalid ${typeDef.name} ${mode} payload: ${issues.join("; ")}`);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
function isMissingRequiredValue(value) {
|
|
1210
|
-
if (value === void 0 || value === null) return true;
|
|
1211
|
-
if (typeof value === "string" && value.trim().length === 0) return true;
|
|
1212
|
-
return false;
|
|
1213
|
-
}
|
|
1214
|
-
function isFieldTypeCompatible(type, value) {
|
|
1215
|
-
switch (type) {
|
|
1216
|
-
case "string":
|
|
1217
|
-
case "ref":
|
|
1218
|
-
return typeof value === "string";
|
|
1219
|
-
case "date":
|
|
1220
|
-
return typeof value === "string" && isDateString(value);
|
|
1221
|
-
case "number":
|
|
1222
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
1223
|
-
case "boolean":
|
|
1224
|
-
return typeof value === "boolean";
|
|
1225
|
-
case "list":
|
|
1226
|
-
return Array.isArray(value);
|
|
1227
|
-
case "any":
|
|
1228
|
-
return true;
|
|
1229
|
-
default:
|
|
1230
|
-
return true;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
function describeValue(value) {
|
|
1234
|
-
if (Array.isArray(value)) return "array";
|
|
1235
|
-
if (value === null) return "null";
|
|
1236
|
-
return typeof value;
|
|
1237
|
-
}
|
|
1238
|
-
function matchesTemplate(template, value) {
|
|
1239
|
-
switch (template) {
|
|
1240
|
-
case "slug":
|
|
1241
|
-
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
1242
|
-
case "semver":
|
|
1243
|
-
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value);
|
|
1244
|
-
case "email":
|
|
1245
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
1246
|
-
case "url":
|
|
1247
|
-
try {
|
|
1248
|
-
const url = new URL(value);
|
|
1249
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
1250
|
-
} catch {
|
|
1251
|
-
return false;
|
|
1252
|
-
}
|
|
1253
|
-
case "iso-date":
|
|
1254
|
-
return isDateString(value);
|
|
1255
|
-
default:
|
|
1256
|
-
return true;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
function isDateString(value) {
|
|
1260
|
-
const timestamp = Date.parse(value);
|
|
1261
|
-
return Number.isFinite(timestamp);
|
|
1262
|
-
}
|
|
1263
|
-
function validateRefValue(workspacePath, rawRef, allowedTypes) {
|
|
1264
|
-
const normalized = normalizeRefPath(rawRef);
|
|
1265
|
-
if (normalized.startsWith("external/")) {
|
|
1266
|
-
return { ok: true };
|
|
1267
|
-
}
|
|
1268
|
-
const target = read(workspacePath, normalized);
|
|
1269
|
-
if (!target && allowedTypes && allowedTypes.length > 0) {
|
|
1270
|
-
const refDir = normalized.split("/")[0];
|
|
1271
|
-
const registry = loadRegistry(workspacePath);
|
|
1272
|
-
const allowedDirs = allowedTypes.map((typeName) => registry.types[typeName]?.directory).filter((dirName) => !!dirName);
|
|
1273
|
-
if (allowedDirs.includes(refDir)) {
|
|
1274
|
-
return { ok: true };
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
if (!target) {
|
|
1278
|
-
return { ok: false, reason: `Reference target not found: ${normalized}` };
|
|
1279
|
-
}
|
|
1280
|
-
if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(target.type)) {
|
|
1281
|
-
return {
|
|
1282
|
-
ok: false,
|
|
1283
|
-
reason: `Reference ${normalized} has type "${target.type}" but allowed types are [${allowedTypes.join(", ")}]`
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
return { ok: true };
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// src/orientation.ts
|
|
1290
|
-
var orientation_exports = {};
|
|
1291
|
-
__export(orientation_exports, {
|
|
1292
|
-
brief: () => brief,
|
|
1293
|
-
checkpoint: () => checkpoint,
|
|
1294
|
-
intake: () => intake,
|
|
1295
|
-
statusSnapshot: () => statusSnapshot
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
// src/query.ts
|
|
1299
|
-
var query_exports = {};
|
|
1300
|
-
__export(query_exports, {
|
|
1301
|
-
keywordSearch: () => keywordSearch,
|
|
1302
|
-
queryPrimitives: () => queryPrimitives
|
|
1303
|
-
});
|
|
1304
|
-
function queryPrimitives(workspacePath, filters = {}) {
|
|
1305
|
-
const typeNames = filters.type ? [filters.type] : listTypes(workspacePath).map((type) => type.name);
|
|
1306
|
-
const all = typeNames.flatMap((typeName) => list(workspacePath, typeName));
|
|
1307
|
-
const matched = all.filter((instance) => matchesFilters(instance, filters));
|
|
1308
|
-
const offset = Math.max(0, filters.offset ?? 0);
|
|
1309
|
-
const limited = filters.limit && filters.limit >= 0 ? matched.slice(offset, offset + filters.limit) : matched.slice(offset);
|
|
1310
|
-
return limited;
|
|
1311
|
-
}
|
|
1312
|
-
function keywordSearch(workspacePath, text, filters = {}) {
|
|
1313
|
-
return queryPrimitives(workspacePath, {
|
|
1314
|
-
...filters,
|
|
1315
|
-
text
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
function matchesFilters(instance, filters) {
|
|
1319
|
-
if (filters.status && String(instance.fields.status ?? "") !== filters.status) return false;
|
|
1320
|
-
if (filters.owner && String(instance.fields.owner ?? "") !== filters.owner) return false;
|
|
1321
|
-
if (filters.tag && !hasTag(instance, filters.tag)) return false;
|
|
1322
|
-
if (filters.pathIncludes && !instance.path.includes(filters.pathIncludes)) return false;
|
|
1323
|
-
if (filters.updatedAfter && !isDateOnOrAfter(instance.fields.updated, filters.updatedAfter)) return false;
|
|
1324
|
-
if (filters.updatedBefore && !isDateOnOrBefore(instance.fields.updated, filters.updatedBefore)) return false;
|
|
1325
|
-
if (filters.createdAfter && !isDateOnOrAfter(instance.fields.created, filters.createdAfter)) return false;
|
|
1326
|
-
if (filters.createdBefore && !isDateOnOrBefore(instance.fields.created, filters.createdBefore)) return false;
|
|
1327
|
-
if (filters.text && !containsText(instance, filters.text)) return false;
|
|
1328
|
-
return true;
|
|
1329
|
-
}
|
|
1330
|
-
function hasTag(instance, tag) {
|
|
1331
|
-
const tags = instance.fields.tags;
|
|
1332
|
-
if (!Array.isArray(tags)) return false;
|
|
1333
|
-
return tags.map((value) => String(value)).includes(tag);
|
|
1334
|
-
}
|
|
1335
|
-
function containsText(instance, text) {
|
|
1336
|
-
const haystack = [
|
|
1337
|
-
instance.path,
|
|
1338
|
-
instance.type,
|
|
1339
|
-
stringifyFields(instance.fields),
|
|
1340
|
-
instance.body
|
|
1341
|
-
].join("\n").toLowerCase();
|
|
1342
|
-
return haystack.includes(text.toLowerCase());
|
|
1343
|
-
}
|
|
1344
|
-
function stringifyFields(fields) {
|
|
1345
|
-
try {
|
|
1346
|
-
return JSON.stringify(fields);
|
|
1347
|
-
} catch {
|
|
1348
|
-
return "";
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
function isDateOnOrAfter(value, thresholdIso) {
|
|
1352
|
-
const ts = Date.parse(String(value ?? ""));
|
|
1353
|
-
const threshold = Date.parse(thresholdIso);
|
|
1354
|
-
if (!Number.isFinite(ts) || !Number.isFinite(threshold)) return false;
|
|
1355
|
-
return ts >= threshold;
|
|
1356
|
-
}
|
|
1357
|
-
function isDateOnOrBefore(value, thresholdIso) {
|
|
1358
|
-
const ts = Date.parse(String(value ?? ""));
|
|
1359
|
-
const threshold = Date.parse(thresholdIso);
|
|
1360
|
-
if (!Number.isFinite(ts) || !Number.isFinite(threshold)) return false;
|
|
1361
|
-
return ts <= threshold;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// src/thread.ts
|
|
1365
|
-
var thread_exports = {};
|
|
1366
|
-
__export(thread_exports, {
|
|
1367
|
-
block: () => block,
|
|
1368
|
-
cancel: () => cancel,
|
|
1369
|
-
claim: () => claim,
|
|
1370
|
-
claimNextReady: () => claimNextReady,
|
|
1371
|
-
claimNextReadyInSpace: () => claimNextReadyInSpace,
|
|
1372
|
-
createThread: () => createThread,
|
|
1373
|
-
decompose: () => decompose,
|
|
1374
|
-
done: () => done,
|
|
1375
|
-
isReadyForClaim: () => isReadyForClaim,
|
|
1376
|
-
listReadyThreads: () => listReadyThreads,
|
|
1377
|
-
listReadyThreadsInSpace: () => listReadyThreadsInSpace,
|
|
1378
|
-
pickNextReadyThread: () => pickNextReadyThread,
|
|
1379
|
-
pickNextReadyThreadInSpace: () => pickNextReadyThreadInSpace,
|
|
1380
|
-
release: () => release,
|
|
1381
|
-
unblock: () => unblock
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
// src/types.ts
|
|
1385
|
-
var THREAD_STATUS_TRANSITIONS = {
|
|
1386
|
-
open: ["active", "cancelled"],
|
|
1387
|
-
active: ["blocked", "done", "cancelled", "open"],
|
|
1388
|
-
blocked: ["active", "cancelled"],
|
|
1389
|
-
done: [],
|
|
1390
|
-
cancelled: ["open"]
|
|
1391
|
-
};
|
|
1392
|
-
|
|
1393
|
-
// src/thread.ts
|
|
1394
|
-
function createThread(workspacePath, title, goal, actor, opts = {}) {
|
|
1395
|
-
const normalizedSpace = opts.space ? normalizeWorkspaceRef(opts.space) : void 0;
|
|
1396
|
-
const contextRefs = opts.context_refs ?? [];
|
|
1397
|
-
const mergedContextRefs = normalizedSpace && !contextRefs.includes(normalizedSpace) ? [...contextRefs, normalizedSpace] : contextRefs;
|
|
1398
|
-
return create(workspacePath, "thread", {
|
|
1399
|
-
title,
|
|
1400
|
-
goal,
|
|
1401
|
-
status: "open",
|
|
1402
|
-
priority: opts.priority ?? "medium",
|
|
1403
|
-
deps: opts.deps ?? [],
|
|
1404
|
-
parent: opts.parent,
|
|
1405
|
-
space: normalizedSpace,
|
|
1406
|
-
context_refs: mergedContextRefs,
|
|
1407
|
-
tags: opts.tags ?? []
|
|
1408
|
-
}, `## Goal
|
|
1409
|
-
|
|
1410
|
-
${goal}
|
|
1411
|
-
`, actor);
|
|
1412
|
-
}
|
|
1413
|
-
function isReadyForClaim(workspacePath, threadPathOrInstance) {
|
|
1414
|
-
const instance = typeof threadPathOrInstance === "string" ? read(workspacePath, threadPathOrInstance) : threadPathOrInstance;
|
|
1415
|
-
if (!instance) return false;
|
|
1416
|
-
if (instance.type !== "thread") return false;
|
|
1417
|
-
if (instance.fields.status !== "open") return false;
|
|
1418
|
-
const hasUnfinishedChildren = list(workspacePath, "thread").some(
|
|
1419
|
-
(candidate) => candidate.fields.parent === instance.path && !["done", "cancelled"].includes(String(candidate.fields.status))
|
|
1420
|
-
);
|
|
1421
|
-
if (hasUnfinishedChildren) return false;
|
|
1422
|
-
const deps = Array.isArray(instance.fields.deps) ? instance.fields.deps : [];
|
|
1423
|
-
if (deps.length === 0) return true;
|
|
1424
|
-
for (const dep of deps) {
|
|
1425
|
-
const depRef = normalizeThreadRef(dep);
|
|
1426
|
-
if (depRef.startsWith("external/")) return false;
|
|
1427
|
-
const depThread = read(workspacePath, depRef);
|
|
1428
|
-
if (!depThread || depThread.fields.status !== "done") {
|
|
1429
|
-
return false;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
return true;
|
|
1433
|
-
}
|
|
1434
|
-
function listReadyThreads(workspacePath) {
|
|
1435
|
-
const open = openThreads(workspacePath);
|
|
1436
|
-
return open.filter((t) => isReadyForClaim(workspacePath, t)).sort(compareThreadPriority);
|
|
1437
|
-
}
|
|
1438
|
-
function listReadyThreadsInSpace(workspacePath, spaceRef) {
|
|
1439
|
-
const normalizedSpace = normalizeWorkspaceRef(spaceRef);
|
|
1440
|
-
return listReadyThreads(workspacePath).filter(
|
|
1441
|
-
(thread) => normalizeWorkspaceRef(thread.fields.space) === normalizedSpace
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
function pickNextReadyThread(workspacePath) {
|
|
1445
|
-
const ready = listReadyThreads(workspacePath);
|
|
1446
|
-
return ready[0] ?? null;
|
|
1447
|
-
}
|
|
1448
|
-
function pickNextReadyThreadInSpace(workspacePath, spaceRef) {
|
|
1449
|
-
const ready = listReadyThreadsInSpace(workspacePath, spaceRef);
|
|
1450
|
-
return ready[0] ?? null;
|
|
1451
|
-
}
|
|
1452
|
-
function claimNextReady(workspacePath, actor) {
|
|
1453
|
-
const next = pickNextReadyThread(workspacePath);
|
|
1454
|
-
if (!next) return null;
|
|
1455
|
-
return claim(workspacePath, next.path, actor);
|
|
1456
|
-
}
|
|
1457
|
-
function claimNextReadyInSpace(workspacePath, actor, spaceRef) {
|
|
1458
|
-
const next = pickNextReadyThreadInSpace(workspacePath, spaceRef);
|
|
1459
|
-
if (!next) return null;
|
|
1460
|
-
return claim(workspacePath, next.path, actor);
|
|
1461
|
-
}
|
|
1462
|
-
function claim(workspacePath, threadPath, actor) {
|
|
1463
|
-
const thread = read(workspacePath, threadPath);
|
|
1464
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
1465
|
-
const status2 = thread.fields.status;
|
|
1466
|
-
if (status2 !== "open") {
|
|
1467
|
-
throw new Error(`Cannot claim thread in "${status2}" state. Only "open" threads can be claimed.`);
|
|
1468
|
-
}
|
|
1469
|
-
const owner = currentOwner(workspacePath, threadPath);
|
|
1470
|
-
if (owner) {
|
|
1471
|
-
throw new Error(`Thread already claimed by "${owner}". Wait for release or use a different thread.`);
|
|
1472
|
-
}
|
|
1473
|
-
append(workspacePath, actor, "claim", threadPath, "thread");
|
|
1474
|
-
return update(workspacePath, threadPath, {
|
|
1475
|
-
status: "active",
|
|
1476
|
-
owner: actor
|
|
1477
|
-
}, void 0, actor);
|
|
1478
|
-
}
|
|
1479
|
-
function release(workspacePath, threadPath, actor, reason) {
|
|
1480
|
-
const thread = read(workspacePath, threadPath);
|
|
1481
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
1482
|
-
assertOwner(workspacePath, threadPath, actor);
|
|
1483
|
-
append(
|
|
1484
|
-
workspacePath,
|
|
1485
|
-
actor,
|
|
1486
|
-
"release",
|
|
1487
|
-
threadPath,
|
|
1488
|
-
"thread",
|
|
1489
|
-
reason ? { reason } : void 0
|
|
1490
|
-
);
|
|
1491
|
-
return update(workspacePath, threadPath, {
|
|
1492
|
-
status: "open",
|
|
1493
|
-
owner: null
|
|
1494
|
-
}, void 0, actor);
|
|
1495
|
-
}
|
|
1496
|
-
function block(workspacePath, threadPath, actor, blockedBy, reason) {
|
|
1497
|
-
const thread = read(workspacePath, threadPath);
|
|
1498
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
1499
|
-
assertTransition(thread.fields.status, "blocked");
|
|
1500
|
-
append(workspacePath, actor, "block", threadPath, "thread", {
|
|
1501
|
-
blocked_by: blockedBy,
|
|
1502
|
-
...reason ? { reason } : {}
|
|
1503
|
-
});
|
|
1504
|
-
const currentDeps = thread.fields.deps ?? [];
|
|
1505
|
-
const updatedDeps = currentDeps.includes(blockedBy) ? currentDeps : [...currentDeps, blockedBy];
|
|
1506
|
-
return update(workspacePath, threadPath, {
|
|
1507
|
-
status: "blocked",
|
|
1508
|
-
deps: updatedDeps
|
|
1509
|
-
}, void 0, actor);
|
|
1510
|
-
}
|
|
1511
|
-
function unblock(workspacePath, threadPath, actor) {
|
|
1512
|
-
const thread = read(workspacePath, threadPath);
|
|
1513
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
1514
|
-
assertTransition(thread.fields.status, "active");
|
|
1515
|
-
append(workspacePath, actor, "unblock", threadPath, "thread");
|
|
1516
|
-
return update(workspacePath, threadPath, {
|
|
1517
|
-
status: "active"
|
|
1518
|
-
}, void 0, actor);
|
|
1519
|
-
}
|
|
1520
|
-
function done(workspacePath, threadPath, actor, output) {
|
|
1521
|
-
const thread = read(workspacePath, threadPath);
|
|
1522
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
1523
|
-
assertTransition(thread.fields.status, "done");
|
|
1524
|
-
assertOwner(workspacePath, threadPath, actor);
|
|
1525
|
-
append(
|
|
1526
|
-
workspacePath,
|
|
1527
|
-
actor,
|
|
1528
|
-
"done",
|
|
1529
|
-
threadPath,
|
|
1530
|
-
"thread",
|
|
1531
|
-
output ? { output } : void 0
|
|
1532
|
-
);
|
|
1533
|
-
const newBody = output ? `${thread.body}
|
|
1534
|
-
|
|
1535
|
-
## Output
|
|
1536
|
-
|
|
1537
|
-
${output}
|
|
1538
|
-
` : thread.body;
|
|
1539
|
-
return update(workspacePath, threadPath, {
|
|
1540
|
-
status: "done"
|
|
1541
|
-
}, newBody, actor);
|
|
1542
|
-
}
|
|
1543
|
-
function cancel(workspacePath, threadPath, actor, reason) {
|
|
1544
|
-
const thread = read(workspacePath, threadPath);
|
|
1545
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
1546
|
-
assertTransition(thread.fields.status, "cancelled");
|
|
1547
|
-
append(
|
|
1548
|
-
workspacePath,
|
|
1549
|
-
actor,
|
|
1550
|
-
"cancel",
|
|
1551
|
-
threadPath,
|
|
1552
|
-
"thread",
|
|
1553
|
-
reason ? { reason } : void 0
|
|
1554
|
-
);
|
|
1555
|
-
return update(workspacePath, threadPath, {
|
|
1556
|
-
status: "cancelled",
|
|
1557
|
-
owner: null
|
|
1558
|
-
}, void 0, actor);
|
|
1559
|
-
}
|
|
1560
|
-
function decompose(workspacePath, parentPath, subthreads, actor) {
|
|
1561
|
-
const parent = read(workspacePath, parentPath);
|
|
1562
|
-
if (!parent) throw new Error(`Thread not found: ${parentPath}`);
|
|
1563
|
-
const created = [];
|
|
1564
|
-
for (const sub of subthreads) {
|
|
1565
|
-
const inst = createThread(workspacePath, sub.title, sub.goal, actor, {
|
|
1566
|
-
parent: parentPath,
|
|
1567
|
-
deps: sub.deps,
|
|
1568
|
-
space: typeof parent.fields.space === "string" ? parent.fields.space : void 0
|
|
1569
|
-
});
|
|
1570
|
-
created.push(inst);
|
|
1571
|
-
}
|
|
1572
|
-
const childRefs = created.map((c) => `[[${c.path}]]`);
|
|
1573
|
-
const decomposeNote = `
|
|
1574
|
-
|
|
1575
|
-
## Sub-threads
|
|
1576
|
-
|
|
1577
|
-
${childRefs.map((r) => `- ${r}`).join("\n")}
|
|
1578
|
-
`;
|
|
1579
|
-
update(workspacePath, parentPath, {}, parent.body + decomposeNote, actor);
|
|
1580
|
-
append(workspacePath, actor, "decompose", parentPath, "thread", {
|
|
1581
|
-
children: created.map((c) => c.path)
|
|
1582
|
-
});
|
|
1583
|
-
return created;
|
|
1584
|
-
}
|
|
1585
|
-
function assertTransition(from, to) {
|
|
1586
|
-
const allowed = THREAD_STATUS_TRANSITIONS[from];
|
|
1587
|
-
if (!allowed?.includes(to)) {
|
|
1588
|
-
throw new Error(`Invalid transition: "${from}" \u2192 "${to}". Allowed: ${allowed?.join(", ") ?? "none"}`);
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
function assertOwner(workspacePath, threadPath, actor) {
|
|
1592
|
-
const owner = currentOwner(workspacePath, threadPath);
|
|
1593
|
-
if (owner && owner !== actor) {
|
|
1594
|
-
throw new Error(`Thread is owned by "${owner}", not "${actor}". Only the owner can perform this action.`);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
function compareThreadPriority(a, b) {
|
|
1598
|
-
const rank = (value) => {
|
|
1599
|
-
const normalized = String(value ?? "medium").toLowerCase();
|
|
1600
|
-
switch (normalized) {
|
|
1601
|
-
case "urgent":
|
|
1602
|
-
return 0;
|
|
1603
|
-
case "high":
|
|
1604
|
-
return 1;
|
|
1605
|
-
case "medium":
|
|
1606
|
-
return 2;
|
|
1607
|
-
case "low":
|
|
1608
|
-
return 3;
|
|
1609
|
-
default:
|
|
1610
|
-
return 4;
|
|
1611
|
-
}
|
|
1612
|
-
};
|
|
1613
|
-
const byPriority = rank(a.fields.priority) - rank(b.fields.priority);
|
|
1614
|
-
if (byPriority !== 0) return byPriority;
|
|
1615
|
-
const createdA = Date.parse(String(a.fields.created ?? ""));
|
|
1616
|
-
const createdB = Date.parse(String(b.fields.created ?? ""));
|
|
1617
|
-
const safeA = Number.isNaN(createdA) ? Number.MAX_SAFE_INTEGER : createdA;
|
|
1618
|
-
const safeB = Number.isNaN(createdB) ? Number.MAX_SAFE_INTEGER : createdB;
|
|
1619
|
-
return safeA - safeB;
|
|
1620
|
-
}
|
|
1621
|
-
function normalizeThreadRef(value) {
|
|
1622
|
-
const raw = String(value ?? "").trim();
|
|
1623
|
-
if (!raw) return raw;
|
|
1624
|
-
const unwrapped = raw.startsWith("[[") && raw.endsWith("]]") ? raw.slice(2, -2) : raw;
|
|
1625
|
-
if (unwrapped.startsWith("external/")) return unwrapped;
|
|
1626
|
-
if (unwrapped.endsWith(".md")) return unwrapped;
|
|
1627
|
-
return `${unwrapped}.md`;
|
|
1628
|
-
}
|
|
1629
|
-
function normalizeWorkspaceRef(value) {
|
|
1630
|
-
const raw = String(value ?? "").trim();
|
|
1631
|
-
if (!raw) return "";
|
|
1632
|
-
const unwrapped = raw.startsWith("[[") && raw.endsWith("]]") ? raw.slice(2, -2) : raw;
|
|
1633
|
-
return unwrapped.endsWith(".md") ? unwrapped : `${unwrapped}.md`;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// src/orientation.ts
|
|
1637
|
-
function statusSnapshot(workspacePath) {
|
|
1638
|
-
const threads = list(workspacePath, "thread");
|
|
1639
|
-
const allPrimitives = queryPrimitives(workspacePath);
|
|
1640
|
-
const byType = allPrimitives.reduce((acc, instance) => {
|
|
1641
|
-
acc[instance.type] = (acc[instance.type] ?? 0) + 1;
|
|
1642
|
-
return acc;
|
|
1643
|
-
}, {});
|
|
1644
|
-
const claims = allClaims(workspacePath);
|
|
1645
|
-
const ready = listReadyThreads(workspacePath);
|
|
1646
|
-
return {
|
|
1647
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1648
|
-
threads: {
|
|
1649
|
-
total: threads.length,
|
|
1650
|
-
open: threads.filter((item) => item.fields.status === "open").length,
|
|
1651
|
-
active: threads.filter((item) => item.fields.status === "active").length,
|
|
1652
|
-
blocked: threads.filter((item) => item.fields.status === "blocked").length,
|
|
1653
|
-
done: threads.filter((item) => item.fields.status === "done").length,
|
|
1654
|
-
cancelled: threads.filter((item) => item.fields.status === "cancelled").length,
|
|
1655
|
-
ready: ready.length
|
|
1656
|
-
},
|
|
1657
|
-
claims: {
|
|
1658
|
-
active: claims.size
|
|
1659
|
-
},
|
|
1660
|
-
primitives: {
|
|
1661
|
-
total: allPrimitives.length,
|
|
1662
|
-
byType
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
function brief(workspacePath, actor, options = {}) {
|
|
1667
|
-
const myClaims = [...allClaims(workspacePath).entries()].filter(([, owner]) => owner === actor).map(([target]) => read(workspacePath, target)).filter((instance) => instance !== null);
|
|
1668
|
-
const myOpenThreads = queryPrimitives(workspacePath, {
|
|
1669
|
-
type: "thread",
|
|
1670
|
-
owner: actor
|
|
1671
|
-
}).filter((instance) => ["open", "active"].includes(String(instance.fields.status)));
|
|
1672
|
-
return {
|
|
1673
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1674
|
-
actor,
|
|
1675
|
-
myClaims,
|
|
1676
|
-
myOpenThreads,
|
|
1677
|
-
blockedThreads: blockedThreads(workspacePath),
|
|
1678
|
-
nextReadyThreads: listReadyThreads(workspacePath).slice(0, options.nextCount ?? 5),
|
|
1679
|
-
recentActivity: recent(workspacePath, options.recentCount ?? 12)
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
function checkpoint(workspacePath, actor, summary, options = {}) {
|
|
1683
|
-
const title = `Checkpoint ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
1684
|
-
const bodyLines = [
|
|
1685
|
-
"## Summary",
|
|
1686
|
-
"",
|
|
1687
|
-
summary,
|
|
1688
|
-
"",
|
|
1689
|
-
"## Next",
|
|
1690
|
-
"",
|
|
1691
|
-
...options.next && options.next.length > 0 ? options.next.map((item) => `- ${item}`) : ["- None"],
|
|
1692
|
-
"",
|
|
1693
|
-
"## Blocked",
|
|
1694
|
-
"",
|
|
1695
|
-
...options.blocked && options.blocked.length > 0 ? options.blocked.map((item) => `- ${item}`) : ["- None"],
|
|
1696
|
-
""
|
|
1697
|
-
];
|
|
1698
|
-
return create(
|
|
1699
|
-
workspacePath,
|
|
1700
|
-
"checkpoint",
|
|
1701
|
-
{
|
|
1702
|
-
title,
|
|
1703
|
-
actor,
|
|
1704
|
-
summary,
|
|
1705
|
-
next: options.next ?? [],
|
|
1706
|
-
blocked: options.blocked ?? [],
|
|
1707
|
-
tags: options.tags ?? []
|
|
1708
|
-
},
|
|
1709
|
-
bodyLines.join("\n"),
|
|
1710
|
-
actor
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
function intake(workspacePath, actor, observation, options = {}) {
|
|
1714
|
-
return checkpoint(workspacePath, actor, observation, {
|
|
1715
|
-
tags: ["intake", ...options.tags ?? []]
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
// src/adapter-cursor-cloud.ts
|
|
1720
|
-
var DEFAULT_MAX_STEPS = 200;
|
|
1721
|
-
var DEFAULT_STEP_DELAY_MS = 25;
|
|
1722
|
-
var DEFAULT_AGENT_COUNT = 3;
|
|
1723
|
-
var CursorCloudAdapter = class {
|
|
1724
|
-
name = "cursor-cloud";
|
|
1725
|
-
async create(_input) {
|
|
1726
|
-
return {
|
|
1727
|
-
runId: "adapter-managed",
|
|
1728
|
-
status: "queued"
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
async status(runId) {
|
|
1732
|
-
return { runId, status: "running" };
|
|
1733
|
-
}
|
|
1734
|
-
async followup(runId, _actor, _input) {
|
|
1735
|
-
return { runId, status: "running" };
|
|
1736
|
-
}
|
|
1737
|
-
async stop(runId, _actor) {
|
|
1738
|
-
return { runId, status: "cancelled" };
|
|
1739
|
-
}
|
|
1740
|
-
async logs(_runId) {
|
|
1741
|
-
return [];
|
|
1742
|
-
}
|
|
1743
|
-
async execute(input) {
|
|
1744
|
-
const start = Date.now();
|
|
1745
|
-
const logs2 = [];
|
|
1746
|
-
const agentPool = normalizeAgents(input.agents, input.actor);
|
|
1747
|
-
const maxSteps = normalizeInt(input.maxSteps, DEFAULT_MAX_STEPS, 1, 5e3);
|
|
1748
|
-
const stepDelayMs = normalizeInt(input.stepDelayMs, DEFAULT_STEP_DELAY_MS, 0, 5e3);
|
|
1749
|
-
const claimedByAgent = {};
|
|
1750
|
-
const completedByAgent = {};
|
|
1751
|
-
let stepsExecuted = 0;
|
|
1752
|
-
let completionCount = 0;
|
|
1753
|
-
let failureCount = 0;
|
|
1754
|
-
let cancelled = false;
|
|
1755
|
-
for (const agent of agentPool) {
|
|
1756
|
-
claimedByAgent[agent] = 0;
|
|
1757
|
-
completedByAgent[agent] = 0;
|
|
1758
|
-
}
|
|
1759
|
-
pushLog(logs2, "info", `Run ${input.runId} started with agents: ${agentPool.join(", ")}`);
|
|
1760
|
-
pushLog(logs2, "info", `Objective: ${input.objective}`);
|
|
1761
|
-
while (stepsExecuted < maxSteps) {
|
|
1762
|
-
if (input.isCancelled?.()) {
|
|
1763
|
-
cancelled = true;
|
|
1764
|
-
pushLog(logs2, "warn", `Run ${input.runId} received cancellation signal.`);
|
|
1765
|
-
break;
|
|
1766
|
-
}
|
|
1767
|
-
const claimedThisRound = [];
|
|
1768
|
-
for (const agent of agentPool) {
|
|
1769
|
-
try {
|
|
1770
|
-
const claimed = input.space ? claimNextReadyInSpace(input.workspacePath, agent, input.space) : claimNextReady(input.workspacePath, agent);
|
|
1771
|
-
if (!claimed) {
|
|
1772
|
-
continue;
|
|
1773
|
-
}
|
|
1774
|
-
const path7 = claimed.path;
|
|
1775
|
-
const goal = String(claimed.fields.goal ?? claimed.fields.title ?? path7);
|
|
1776
|
-
claimedThisRound.push({ agent, threadPath: path7, goal });
|
|
1777
|
-
claimedByAgent[agent] += 1;
|
|
1778
|
-
pushLog(logs2, "info", `${agent} claimed ${path7}`);
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
pushLog(logs2, "warn", `${agent} claim skipped: ${errorMessage(error)}`);
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
if (claimedThisRound.length === 0) {
|
|
1784
|
-
const readyRemaining = listReady(input.workspacePath, input.space).length;
|
|
1785
|
-
if (readyRemaining === 0) {
|
|
1786
|
-
pushLog(logs2, "info", "No ready threads remaining; autonomous loop complete.");
|
|
1787
|
-
break;
|
|
1788
|
-
}
|
|
1789
|
-
if (stepDelayMs > 0) {
|
|
1790
|
-
await sleep(stepDelayMs);
|
|
1791
|
-
}
|
|
1792
|
-
continue;
|
|
1793
|
-
}
|
|
1794
|
-
await Promise.all(claimedThisRound.map(async (claimed) => {
|
|
1795
|
-
if (input.isCancelled?.()) {
|
|
1796
|
-
cancelled = true;
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
if (stepDelayMs > 0) {
|
|
1800
|
-
await sleep(stepDelayMs);
|
|
1801
|
-
}
|
|
1802
|
-
try {
|
|
1803
|
-
done(
|
|
1804
|
-
input.workspacePath,
|
|
1805
|
-
claimed.threadPath,
|
|
1806
|
-
claimed.agent,
|
|
1807
|
-
`Completed by ${claimed.agent} during dispatch run ${input.runId}. Goal: ${claimed.goal}`
|
|
1808
|
-
);
|
|
1809
|
-
completionCount += 1;
|
|
1810
|
-
completedByAgent[claimed.agent] += 1;
|
|
1811
|
-
pushLog(logs2, "info", `${claimed.agent} completed ${claimed.threadPath}`);
|
|
1812
|
-
} catch (error) {
|
|
1813
|
-
failureCount += 1;
|
|
1814
|
-
pushLog(logs2, "error", `${claimed.agent} failed to complete ${claimed.threadPath}: ${errorMessage(error)}`);
|
|
1815
|
-
}
|
|
1816
|
-
}));
|
|
1817
|
-
stepsExecuted += claimedThisRound.length;
|
|
1818
|
-
if (cancelled) break;
|
|
1819
|
-
}
|
|
1820
|
-
const readyAfter = listReady(input.workspacePath, input.space);
|
|
1821
|
-
const activeAfter = input.space ? threadsInSpace(input.workspacePath, input.space).filter((candidate) => candidate.fields.status === "active") : activeThreads(input.workspacePath);
|
|
1822
|
-
const openAfter = input.space ? threadsInSpace(input.workspacePath, input.space).filter((candidate) => candidate.fields.status === "open") : openThreads(input.workspacePath);
|
|
1823
|
-
const blockedAfter = input.space ? threadsInSpace(input.workspacePath, input.space).filter((candidate) => candidate.fields.status === "blocked") : blockedThreads(input.workspacePath);
|
|
1824
|
-
const elapsedMs = Date.now() - start;
|
|
1825
|
-
const summary = renderSummary({
|
|
1826
|
-
objective: input.objective,
|
|
1827
|
-
runId: input.runId,
|
|
1828
|
-
completed: completionCount,
|
|
1829
|
-
failed: failureCount,
|
|
1830
|
-
stepsExecuted,
|
|
1831
|
-
readyRemaining: readyAfter.length,
|
|
1832
|
-
openRemaining: openAfter.length,
|
|
1833
|
-
blockedRemaining: blockedAfter.length,
|
|
1834
|
-
activeRemaining: activeAfter.length,
|
|
1835
|
-
elapsedMs,
|
|
1836
|
-
claimedByAgent,
|
|
1837
|
-
completedByAgent,
|
|
1838
|
-
cancelled
|
|
1839
|
-
});
|
|
1840
|
-
if (input.createCheckpoint !== false) {
|
|
1841
|
-
try {
|
|
1842
|
-
checkpoint(
|
|
1843
|
-
input.workspacePath,
|
|
1844
|
-
input.actor,
|
|
1845
|
-
`Dispatch run ${input.runId} completed autonomous execution.`,
|
|
1846
|
-
{
|
|
1847
|
-
next: readyAfter.slice(0, 10).map((entry) => entry.path),
|
|
1848
|
-
blocked: blockedAfter.slice(0, 10).map((entry) => entry.path),
|
|
1849
|
-
tags: ["dispatch", "autonomous-run"]
|
|
1850
|
-
}
|
|
1851
|
-
);
|
|
1852
|
-
pushLog(logs2, "info", `Checkpoint recorded for run ${input.runId}.`);
|
|
1853
|
-
} catch (error) {
|
|
1854
|
-
pushLog(logs2, "warn", `Checkpoint creation skipped: ${errorMessage(error)}`);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
if (cancelled) {
|
|
1858
|
-
return {
|
|
1859
|
-
status: "cancelled",
|
|
1860
|
-
output: summary,
|
|
1861
|
-
logs: logs2,
|
|
1862
|
-
metrics: {
|
|
1863
|
-
completed: completionCount,
|
|
1864
|
-
failed: failureCount,
|
|
1865
|
-
readyRemaining: readyAfter.length,
|
|
1866
|
-
openRemaining: openAfter.length,
|
|
1867
|
-
blockedRemaining: blockedAfter.length,
|
|
1868
|
-
elapsedMs,
|
|
1869
|
-
claimedByAgent,
|
|
1870
|
-
completedByAgent
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
if (failureCount > 0) {
|
|
1875
|
-
return {
|
|
1876
|
-
status: "failed",
|
|
1877
|
-
error: summary,
|
|
1878
|
-
logs: logs2,
|
|
1879
|
-
metrics: {
|
|
1880
|
-
completed: completionCount,
|
|
1881
|
-
failed: failureCount,
|
|
1882
|
-
readyRemaining: readyAfter.length,
|
|
1883
|
-
openRemaining: openAfter.length,
|
|
1884
|
-
blockedRemaining: blockedAfter.length,
|
|
1885
|
-
elapsedMs,
|
|
1886
|
-
claimedByAgent,
|
|
1887
|
-
completedByAgent
|
|
1888
|
-
}
|
|
1889
|
-
};
|
|
1890
|
-
}
|
|
1891
|
-
const status2 = readyAfter.length === 0 && activeAfter.length === 0 ? "succeeded" : "failed";
|
|
1892
|
-
if (status2 === "failed") {
|
|
1893
|
-
pushLog(logs2, "warn", "Execution stopped with actionable work still remaining.");
|
|
1894
|
-
}
|
|
1895
|
-
return {
|
|
1896
|
-
status: status2,
|
|
1897
|
-
output: summary,
|
|
1898
|
-
logs: logs2,
|
|
1899
|
-
metrics: {
|
|
1900
|
-
completed: completionCount,
|
|
1901
|
-
failed: failureCount,
|
|
1902
|
-
readyRemaining: readyAfter.length,
|
|
1903
|
-
openRemaining: openAfter.length,
|
|
1904
|
-
blockedRemaining: blockedAfter.length,
|
|
1905
|
-
elapsedMs,
|
|
1906
|
-
claimedByAgent,
|
|
1907
|
-
completedByAgent
|
|
1908
|
-
}
|
|
1909
|
-
};
|
|
1910
|
-
}
|
|
1911
|
-
};
|
|
1912
|
-
function normalizeAgents(agents, actor) {
|
|
1913
|
-
const fromInput = (agents ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
1914
|
-
if (fromInput.length > 0) return [...new Set(fromInput)];
|
|
1915
|
-
return Array.from({ length: DEFAULT_AGENT_COUNT }, (_, idx) => `${actor}-worker-${idx + 1}`);
|
|
1916
|
-
}
|
|
1917
|
-
function normalizeInt(rawValue, fallback, min, max) {
|
|
1918
|
-
const value = Number.isFinite(rawValue) ? Number(rawValue) : fallback;
|
|
1919
|
-
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
1920
|
-
}
|
|
1921
|
-
function pushLog(target, level, message) {
|
|
1922
|
-
target.push({
|
|
1923
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1924
|
-
level,
|
|
1925
|
-
message
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
function listReady(workspacePath, space) {
|
|
1929
|
-
return space ? listReadyThreadsInSpace(workspacePath, space) : listReadyThreads(workspacePath);
|
|
1930
|
-
}
|
|
1931
|
-
function errorMessage(error) {
|
|
1932
|
-
return error instanceof Error ? error.message : String(error);
|
|
1933
|
-
}
|
|
1934
|
-
function sleep(ms) {
|
|
1935
|
-
return new Promise((resolve) => {
|
|
1936
|
-
setTimeout(resolve, ms);
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
function renderSummary(data) {
|
|
1940
|
-
const lines = [
|
|
1941
|
-
`Autonomous dispatch summary for ${data.runId}`,
|
|
1942
|
-
`Objective: ${data.objective}`,
|
|
1943
|
-
`Completed threads: ${data.completed}`,
|
|
1944
|
-
`Failed completions: ${data.failed}`,
|
|
1945
|
-
`Scheduler steps executed: ${data.stepsExecuted}`,
|
|
1946
|
-
`Ready remaining: ${data.readyRemaining}`,
|
|
1947
|
-
`Open remaining: ${data.openRemaining}`,
|
|
1948
|
-
`Blocked remaining: ${data.blockedRemaining}`,
|
|
1949
|
-
`Active remaining: ${data.activeRemaining}`,
|
|
1950
|
-
`Elapsed ms: ${data.elapsedMs}`,
|
|
1951
|
-
`Cancelled: ${data.cancelled ? "yes" : "no"}`,
|
|
1952
|
-
"",
|
|
1953
|
-
"Claims by agent:",
|
|
1954
|
-
...Object.entries(data.claimedByAgent).map(([agent, count]) => `- ${agent}: ${count}`),
|
|
1955
|
-
"",
|
|
1956
|
-
"Completions by agent:",
|
|
1957
|
-
...Object.entries(data.completedByAgent).map(([agent, count]) => `- ${agent}: ${count}`)
|
|
1958
|
-
];
|
|
1959
|
-
return lines.join("\n");
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// src/runtime-adapter-registry.ts
|
|
1963
|
-
var adapterFactories = /* @__PURE__ */ new Map([
|
|
1964
|
-
["cursor-cloud", () => new CursorCloudAdapter()]
|
|
1965
|
-
]);
|
|
1966
|
-
function resolveDispatchAdapter(name) {
|
|
1967
|
-
const safeName = normalizeName(name);
|
|
1968
|
-
const factory = adapterFactories.get(safeName);
|
|
1969
|
-
if (!factory) {
|
|
1970
|
-
throw new Error(`Unknown dispatch adapter "${name}". Registered adapters: ${listDispatchAdapters().join(", ") || "none"}.`);
|
|
1971
|
-
}
|
|
1972
|
-
return factory();
|
|
1973
|
-
}
|
|
1974
|
-
function listDispatchAdapters() {
|
|
1975
|
-
return [...adapterFactories.keys()].sort((a, b) => a.localeCompare(b));
|
|
1976
|
-
}
|
|
1977
|
-
function normalizeName(name) {
|
|
1978
|
-
return String(name || "").trim().toLowerCase();
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
// src/dispatch.ts
|
|
1982
|
-
var RUNS_FILE = ".workgraph/dispatch-runs.json";
|
|
1983
|
-
function createRun(workspacePath, input) {
|
|
1984
|
-
const state = loadRuns(workspacePath);
|
|
1985
|
-
if (input.idempotencyKey) {
|
|
1986
|
-
const existing = state.runs.find((run2) => run2.idempotencyKey === input.idempotencyKey);
|
|
1987
|
-
if (existing) return existing;
|
|
1988
|
-
}
|
|
1989
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1990
|
-
const run = {
|
|
1991
|
-
id: `run_${randomUUID()}`,
|
|
1992
|
-
createdAt: now,
|
|
1993
|
-
updatedAt: now,
|
|
1994
|
-
actor: input.actor,
|
|
1995
|
-
adapter: input.adapter ?? "cursor-cloud",
|
|
1996
|
-
objective: input.objective,
|
|
1997
|
-
status: "queued",
|
|
1998
|
-
idempotencyKey: input.idempotencyKey,
|
|
1999
|
-
context: input.context,
|
|
2000
|
-
followups: [],
|
|
2001
|
-
logs: [
|
|
2002
|
-
{ ts: now, level: "info", message: `Run created for objective: ${input.objective}` }
|
|
2003
|
-
]
|
|
2004
|
-
};
|
|
2005
|
-
state.runs.push(run);
|
|
2006
|
-
saveRuns(workspacePath, state);
|
|
2007
|
-
append(workspacePath, input.actor, "create", `.workgraph/runs/${run.id}`, "run", {
|
|
2008
|
-
adapter: run.adapter,
|
|
2009
|
-
objective: run.objective,
|
|
2010
|
-
status: run.status
|
|
2011
|
-
});
|
|
2012
|
-
ensureRunPrimitive(workspacePath, run, input.actor);
|
|
2013
|
-
return run;
|
|
2014
|
-
}
|
|
2015
|
-
function status(workspacePath, runId) {
|
|
2016
|
-
const run = getRun(workspacePath, runId);
|
|
2017
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
2018
|
-
return run;
|
|
2019
|
-
}
|
|
2020
|
-
function followup(workspacePath, runId, actor, input) {
|
|
2021
|
-
const state = loadRuns(workspacePath);
|
|
2022
|
-
const run = state.runs.find((entry) => entry.id === runId);
|
|
2023
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
2024
|
-
if (!["queued", "running"].includes(run.status)) {
|
|
2025
|
-
throw new Error(`Cannot send follow-up to run ${runId} in terminal status "${run.status}".`);
|
|
2026
|
-
}
|
|
2027
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2028
|
-
run.followups.push({ ts: now, actor, input });
|
|
2029
|
-
run.updatedAt = now;
|
|
2030
|
-
run.logs.push({ ts: now, level: "info", message: `Follow-up from ${actor}: ${input}` });
|
|
2031
|
-
if (run.status === "queued") run.status = "running";
|
|
2032
|
-
saveRuns(workspacePath, state);
|
|
2033
|
-
append(workspacePath, actor, "update", `.workgraph/runs/${run.id}`, "run", {
|
|
2034
|
-
followup: true,
|
|
2035
|
-
status: run.status
|
|
2036
|
-
});
|
|
2037
|
-
syncRunPrimitive(workspacePath, run, actor);
|
|
2038
|
-
return run;
|
|
2039
|
-
}
|
|
2040
|
-
function stop(workspacePath, runId, actor) {
|
|
2041
|
-
return setStatus(workspacePath, runId, actor, "cancelled", "Run cancelled by operator.");
|
|
2042
|
-
}
|
|
2043
|
-
function markRun(workspacePath, runId, actor, nextStatus, options = {}) {
|
|
2044
|
-
const run = setStatus(workspacePath, runId, actor, nextStatus, `Run moved to ${nextStatus}.`);
|
|
2045
|
-
if (options.output) run.output = options.output;
|
|
2046
|
-
if (options.error) run.error = options.error;
|
|
2047
|
-
if (options.contextPatch && Object.keys(options.contextPatch).length > 0) {
|
|
2048
|
-
run.context = {
|
|
2049
|
-
...run.context ?? {},
|
|
2050
|
-
...options.contextPatch
|
|
2051
|
-
};
|
|
2052
|
-
}
|
|
2053
|
-
const state = loadRuns(workspacePath);
|
|
2054
|
-
const target = state.runs.find((entry) => entry.id === runId);
|
|
2055
|
-
if (target) {
|
|
2056
|
-
target.output = run.output;
|
|
2057
|
-
target.error = run.error;
|
|
2058
|
-
target.context = run.context;
|
|
2059
|
-
target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2060
|
-
saveRuns(workspacePath, state);
|
|
2061
|
-
syncRunPrimitive(workspacePath, target, actor);
|
|
2062
|
-
}
|
|
2063
|
-
return target ?? run;
|
|
2064
|
-
}
|
|
2065
|
-
function logs(workspacePath, runId) {
|
|
2066
|
-
return status(workspacePath, runId).logs;
|
|
2067
|
-
}
|
|
2068
|
-
function listRuns(workspacePath, options = {}) {
|
|
2069
|
-
const runs = loadRuns(workspacePath).runs.filter((run) => options.status ? run.status === options.status : true).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2070
|
-
if (options.limit && options.limit > 0) {
|
|
2071
|
-
return runs.slice(0, options.limit);
|
|
2072
|
-
}
|
|
2073
|
-
return runs;
|
|
2074
|
-
}
|
|
2075
|
-
async function executeRun(workspacePath, runId, input) {
|
|
2076
|
-
const existing = status(workspacePath, runId);
|
|
2077
|
-
if (!["queued", "running"].includes(existing.status)) {
|
|
2078
|
-
throw new Error(`Run ${runId} is in terminal status "${existing.status}" and cannot be executed.`);
|
|
2079
|
-
}
|
|
2080
|
-
const adapter = resolveDispatchAdapter(existing.adapter);
|
|
2081
|
-
if (!adapter.execute) {
|
|
2082
|
-
throw new Error(`Dispatch adapter "${existing.adapter}" does not implement execute().`);
|
|
2083
|
-
}
|
|
2084
|
-
if (existing.status === "queued") {
|
|
2085
|
-
setStatus(workspacePath, runId, input.actor, "running", `Run started on adapter "${existing.adapter}".`);
|
|
2086
|
-
}
|
|
2087
|
-
const execution = await adapter.execute({
|
|
2088
|
-
workspacePath,
|
|
2089
|
-
runId,
|
|
2090
|
-
actor: input.actor,
|
|
2091
|
-
objective: existing.objective,
|
|
2092
|
-
context: existing.context,
|
|
2093
|
-
agents: input.agents,
|
|
2094
|
-
maxSteps: input.maxSteps,
|
|
2095
|
-
stepDelayMs: input.stepDelayMs,
|
|
2096
|
-
space: input.space,
|
|
2097
|
-
createCheckpoint: input.createCheckpoint,
|
|
2098
|
-
isCancelled: () => status(workspacePath, runId).status === "cancelled"
|
|
2099
|
-
});
|
|
2100
|
-
appendRunLogs(workspacePath, runId, input.actor, execution.logs);
|
|
2101
|
-
const finalStatus = execution.status;
|
|
2102
|
-
if (finalStatus === "queued" || finalStatus === "running") {
|
|
2103
|
-
throw new Error(`Adapter returned invalid terminal status "${finalStatus}" for execute().`);
|
|
2104
|
-
}
|
|
2105
|
-
return markRun(workspacePath, runId, input.actor, finalStatus, {
|
|
2106
|
-
output: execution.output,
|
|
2107
|
-
error: execution.error,
|
|
2108
|
-
contextPatch: execution.metrics ? { adapter_metrics: execution.metrics } : void 0
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
async function createAndExecuteRun(workspacePath, createInput, executeInput = {}) {
|
|
2112
|
-
const run = createRun(workspacePath, createInput);
|
|
2113
|
-
return executeRun(workspacePath, run.id, {
|
|
2114
|
-
actor: createInput.actor,
|
|
2115
|
-
...executeInput
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
function appendRunLogs(workspacePath, runId, actor, logEntries) {
|
|
2119
|
-
if (logEntries.length === 0) return;
|
|
2120
|
-
const state = loadRuns(workspacePath);
|
|
2121
|
-
const run = state.runs.find((entry) => entry.id === runId);
|
|
2122
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
2123
|
-
run.logs.push(...logEntries);
|
|
2124
|
-
run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2125
|
-
saveRuns(workspacePath, state);
|
|
2126
|
-
append(workspacePath, actor, "update", `.workgraph/runs/${run.id}`, "run", {
|
|
2127
|
-
log_append_count: logEntries.length
|
|
2128
|
-
});
|
|
2129
|
-
syncRunPrimitive(workspacePath, run, actor);
|
|
2130
|
-
}
|
|
2131
|
-
function setStatus(workspacePath, runId, actor, statusValue, logMessage) {
|
|
2132
|
-
const state = loadRuns(workspacePath);
|
|
2133
|
-
const run = state.runs.find((entry) => entry.id === runId);
|
|
2134
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
2135
|
-
assertRunStatusTransition(run.status, statusValue, runId);
|
|
2136
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2137
|
-
run.status = statusValue;
|
|
2138
|
-
run.updatedAt = now;
|
|
2139
|
-
run.logs.push({ ts: now, level: "info", message: logMessage });
|
|
2140
|
-
saveRuns(workspacePath, state);
|
|
2141
|
-
append(workspacePath, actor, "update", `.workgraph/runs/${run.id}`, "run", {
|
|
2142
|
-
status: run.status
|
|
2143
|
-
});
|
|
2144
|
-
syncRunPrimitive(workspacePath, run, actor);
|
|
2145
|
-
return run;
|
|
2146
|
-
}
|
|
2147
|
-
function runsPath(workspacePath) {
|
|
2148
|
-
return path6.join(workspacePath, RUNS_FILE);
|
|
2149
|
-
}
|
|
2150
|
-
function loadRuns(workspacePath) {
|
|
2151
|
-
const rPath = runsPath(workspacePath);
|
|
2152
|
-
if (!fs6.existsSync(rPath)) {
|
|
2153
|
-
const seeded = { version: 1, runs: [] };
|
|
2154
|
-
saveRuns(workspacePath, seeded);
|
|
2155
|
-
return seeded;
|
|
2156
|
-
}
|
|
2157
|
-
const raw = fs6.readFileSync(rPath, "utf-8");
|
|
2158
|
-
const parsed = JSON.parse(raw);
|
|
2159
|
-
return {
|
|
2160
|
-
version: parsed.version ?? 1,
|
|
2161
|
-
runs: parsed.runs ?? []
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
|
-
function saveRuns(workspacePath, value) {
|
|
2165
|
-
const rPath = runsPath(workspacePath);
|
|
2166
|
-
const dir = path6.dirname(rPath);
|
|
2167
|
-
if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
|
|
2168
|
-
fs6.writeFileSync(rPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
2169
|
-
}
|
|
2170
|
-
function getRun(workspacePath, runId) {
|
|
2171
|
-
const state = loadRuns(workspacePath);
|
|
2172
|
-
return state.runs.find((run) => run.id === runId) ?? null;
|
|
2173
|
-
}
|
|
2174
|
-
function ensureRunPrimitive(workspacePath, run, actor) {
|
|
2175
|
-
const safeTitle = `${run.objective} (${run.id.slice(0, 8)})`;
|
|
2176
|
-
const runPrimitivePath = `runs/${run.id}.md`;
|
|
2177
|
-
const existing = read(workspacePath, runPrimitivePath);
|
|
2178
|
-
if (existing) return;
|
|
2179
|
-
create(
|
|
2180
|
-
workspacePath,
|
|
2181
|
-
"run",
|
|
2182
|
-
{
|
|
2183
|
-
title: safeTitle,
|
|
2184
|
-
objective: run.objective,
|
|
2185
|
-
runtime: run.adapter,
|
|
2186
|
-
status: run.status,
|
|
2187
|
-
run_id: run.id,
|
|
2188
|
-
owner: run.actor,
|
|
2189
|
-
tags: ["dispatch"]
|
|
2190
|
-
},
|
|
2191
|
-
`## Objective
|
|
2192
|
-
|
|
2193
|
-
${run.objective}
|
|
2194
|
-
`,
|
|
2195
|
-
actor,
|
|
2196
|
-
{ pathOverride: runPrimitivePath }
|
|
2197
|
-
);
|
|
2198
|
-
}
|
|
2199
|
-
function syncRunPrimitive(workspacePath, run, actor) {
|
|
2200
|
-
const runs = list(workspacePath, "run");
|
|
2201
|
-
const existing = runs.find((entry) => String(entry.fields.run_id) === run.id);
|
|
2202
|
-
if (!existing) return;
|
|
2203
|
-
update(
|
|
2204
|
-
workspacePath,
|
|
2205
|
-
existing.path,
|
|
2206
|
-
{
|
|
2207
|
-
status: run.status,
|
|
2208
|
-
runtime: run.adapter,
|
|
2209
|
-
objective: run.objective,
|
|
2210
|
-
owner: run.actor
|
|
2211
|
-
},
|
|
2212
|
-
renderRunBody(run),
|
|
2213
|
-
actor
|
|
2214
|
-
);
|
|
2215
|
-
}
|
|
2216
|
-
function renderRunBody(run) {
|
|
2217
|
-
const lines = [
|
|
2218
|
-
"## Objective",
|
|
2219
|
-
"",
|
|
2220
|
-
run.objective,
|
|
2221
|
-
"",
|
|
2222
|
-
"## Status",
|
|
2223
|
-
"",
|
|
2224
|
-
run.status,
|
|
2225
|
-
"",
|
|
2226
|
-
"## Logs",
|
|
2227
|
-
"",
|
|
2228
|
-
...run.logs.slice(-20).map((entry) => `- ${entry.ts} [${entry.level}] ${entry.message}`),
|
|
2229
|
-
""
|
|
2230
|
-
];
|
|
2231
|
-
if (run.output) {
|
|
2232
|
-
lines.push("## Output");
|
|
2233
|
-
lines.push("");
|
|
2234
|
-
lines.push(run.output);
|
|
2235
|
-
lines.push("");
|
|
2236
|
-
}
|
|
2237
|
-
if (run.error) {
|
|
2238
|
-
lines.push("## Error");
|
|
2239
|
-
lines.push("");
|
|
2240
|
-
lines.push(run.error);
|
|
2241
|
-
lines.push("");
|
|
2242
|
-
}
|
|
2243
|
-
if (run.context && Object.keys(run.context).length > 0) {
|
|
2244
|
-
lines.push("## Context");
|
|
2245
|
-
lines.push("");
|
|
2246
|
-
lines.push("```json");
|
|
2247
|
-
lines.push(JSON.stringify(run.context, null, 2));
|
|
2248
|
-
lines.push("```");
|
|
2249
|
-
lines.push("");
|
|
2250
|
-
}
|
|
2251
|
-
return lines.join("\n");
|
|
2252
|
-
}
|
|
2253
|
-
var RUN_STATUS_TRANSITIONS = {
|
|
2254
|
-
queued: ["running", "cancelled"],
|
|
2255
|
-
running: ["succeeded", "failed", "cancelled"],
|
|
2256
|
-
succeeded: [],
|
|
2257
|
-
failed: [],
|
|
2258
|
-
cancelled: []
|
|
2259
|
-
};
|
|
2260
|
-
function assertRunStatusTransition(from, to, runId) {
|
|
2261
|
-
if (from === to) return;
|
|
2262
|
-
const allowed = RUN_STATUS_TRANSITIONS[from] ?? [];
|
|
2263
|
-
if (!allowed.includes(to)) {
|
|
2264
|
-
throw new Error(`Invalid run transition for ${runId}: ${from} -> ${to}. Allowed: ${allowed.join(", ") || "none"}.`);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
// src/mcp-server.ts
|
|
2269
|
-
var DEFAULT_SERVER_NAME = "workgraph-mcp-server";
|
|
2270
|
-
var DEFAULT_SERVER_VERSION = "0.1.0";
|
|
2271
|
-
function createWorkgraphMcpServer(options) {
|
|
2272
|
-
const server = new McpServer({
|
|
2273
|
-
name: options.name ?? DEFAULT_SERVER_NAME,
|
|
2274
|
-
version: options.version ?? DEFAULT_SERVER_VERSION
|
|
2275
|
-
});
|
|
2276
|
-
registerResources(server, options);
|
|
2277
|
-
registerTools(server, options);
|
|
2278
|
-
return server;
|
|
2279
|
-
}
|
|
2280
|
-
async function startWorkgraphMcpServer(options) {
|
|
2281
|
-
const server = createWorkgraphMcpServer(options);
|
|
2282
|
-
const transport = new StdioServerTransport();
|
|
2283
|
-
await server.connect(transport);
|
|
2284
|
-
return server;
|
|
2285
|
-
}
|
|
2286
|
-
function registerResources(server, options) {
|
|
2287
|
-
server.registerResource(
|
|
2288
|
-
"workspace-status",
|
|
2289
|
-
"workgraph://status",
|
|
2290
|
-
{
|
|
2291
|
-
title: "Workgraph Status Snapshot",
|
|
2292
|
-
description: "Current thread/claim/primitive counts for the workspace.",
|
|
2293
|
-
mimeType: "application/json"
|
|
2294
|
-
},
|
|
2295
|
-
async () => {
|
|
2296
|
-
const snapshot = statusSnapshot(options.workspacePath);
|
|
2297
|
-
return {
|
|
2298
|
-
contents: [
|
|
2299
|
-
{
|
|
2300
|
-
uri: "workgraph://status",
|
|
2301
|
-
mimeType: "application/json",
|
|
2302
|
-
text: toPrettyJson(snapshot)
|
|
2303
|
-
}
|
|
2304
|
-
]
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
);
|
|
2308
|
-
server.registerResource(
|
|
2309
|
-
"actor-brief",
|
|
2310
|
-
new ResourceTemplate("workgraph://brief/{actor}", { list: void 0 }),
|
|
2311
|
-
{
|
|
2312
|
-
title: "Actor Brief",
|
|
2313
|
-
description: "Actor-specific operational brief derived from workspace state.",
|
|
2314
|
-
mimeType: "application/json"
|
|
2315
|
-
},
|
|
2316
|
-
async (uri, variables) => {
|
|
2317
|
-
const actor = String(variables.actor ?? options.defaultActor ?? "anonymous");
|
|
2318
|
-
const brief2 = brief(options.workspacePath, actor);
|
|
2319
|
-
return {
|
|
2320
|
-
contents: [
|
|
2321
|
-
{
|
|
2322
|
-
uri: uri.toString(),
|
|
2323
|
-
mimeType: "application/json",
|
|
2324
|
-
text: toPrettyJson(brief2)
|
|
2325
|
-
}
|
|
2326
|
-
]
|
|
2327
|
-
};
|
|
2328
|
-
}
|
|
2329
|
-
);
|
|
2330
|
-
}
|
|
2331
|
-
function registerTools(server, options) {
|
|
2332
|
-
server.registerTool(
|
|
2333
|
-
"workgraph_status",
|
|
2334
|
-
{
|
|
2335
|
-
title: "Workgraph Status",
|
|
2336
|
-
description: "Return a compact status snapshot for the configured workspace.",
|
|
2337
|
-
annotations: {
|
|
2338
|
-
readOnlyHint: true,
|
|
2339
|
-
idempotentHint: true
|
|
2340
|
-
}
|
|
2341
|
-
},
|
|
2342
|
-
async () => {
|
|
2343
|
-
try {
|
|
2344
|
-
const snapshot = statusSnapshot(options.workspacePath);
|
|
2345
|
-
return okResult(snapshot, renderStatusSummary(snapshot));
|
|
2346
|
-
} catch (error) {
|
|
2347
|
-
return errorResult(error);
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
);
|
|
2351
|
-
server.registerTool(
|
|
2352
|
-
"workgraph_brief",
|
|
2353
|
-
{
|
|
2354
|
-
title: "Workgraph Brief",
|
|
2355
|
-
description: "Return actor-centric operational brief (claims, blockers, and next work).",
|
|
2356
|
-
inputSchema: {
|
|
2357
|
-
actor: z.string().optional(),
|
|
2358
|
-
recentCount: z.number().int().min(1).max(100).optional(),
|
|
2359
|
-
nextCount: z.number().int().min(1).max(100).optional()
|
|
2360
|
-
},
|
|
2361
|
-
annotations: {
|
|
2362
|
-
readOnlyHint: true,
|
|
2363
|
-
idempotentHint: true
|
|
2364
|
-
}
|
|
2365
|
-
},
|
|
2366
|
-
async (args) => {
|
|
2367
|
-
try {
|
|
2368
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2369
|
-
const brief2 = brief(options.workspacePath, actor, {
|
|
2370
|
-
recentCount: args.recentCount,
|
|
2371
|
-
nextCount: args.nextCount
|
|
2372
|
-
});
|
|
2373
|
-
return okResult(brief2, `Brief for ${actor}: claims=${brief2.myClaims.length}, blocked=${brief2.blockedThreads.length}`);
|
|
2374
|
-
} catch (error) {
|
|
2375
|
-
return errorResult(error);
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
);
|
|
2379
|
-
server.registerTool(
|
|
2380
|
-
"workgraph_query",
|
|
2381
|
-
{
|
|
2382
|
-
title: "Workgraph Query",
|
|
2383
|
-
description: "Query primitives using multi-field filters.",
|
|
2384
|
-
inputSchema: {
|
|
2385
|
-
type: z.string().optional(),
|
|
2386
|
-
status: z.string().optional(),
|
|
2387
|
-
owner: z.string().optional(),
|
|
2388
|
-
tag: z.string().optional(),
|
|
2389
|
-
text: z.string().optional(),
|
|
2390
|
-
pathIncludes: z.string().optional(),
|
|
2391
|
-
updatedAfter: z.string().optional(),
|
|
2392
|
-
updatedBefore: z.string().optional(),
|
|
2393
|
-
createdAfter: z.string().optional(),
|
|
2394
|
-
createdBefore: z.string().optional(),
|
|
2395
|
-
limit: z.number().int().min(0).max(1e3).optional(),
|
|
2396
|
-
offset: z.number().int().min(0).max(1e4).optional()
|
|
2397
|
-
},
|
|
2398
|
-
annotations: {
|
|
2399
|
-
readOnlyHint: true,
|
|
2400
|
-
idempotentHint: true
|
|
2401
|
-
}
|
|
2402
|
-
},
|
|
2403
|
-
async (args) => {
|
|
2404
|
-
try {
|
|
2405
|
-
const results = queryPrimitives(options.workspacePath, {
|
|
2406
|
-
type: args.type,
|
|
2407
|
-
status: args.status,
|
|
2408
|
-
owner: args.owner,
|
|
2409
|
-
tag: args.tag,
|
|
2410
|
-
text: args.text,
|
|
2411
|
-
pathIncludes: args.pathIncludes,
|
|
2412
|
-
updatedAfter: args.updatedAfter,
|
|
2413
|
-
updatedBefore: args.updatedBefore,
|
|
2414
|
-
createdAfter: args.createdAfter,
|
|
2415
|
-
createdBefore: args.createdBefore,
|
|
2416
|
-
limit: args.limit,
|
|
2417
|
-
offset: args.offset
|
|
2418
|
-
});
|
|
2419
|
-
return okResult({ results, count: results.length }, `Query returned ${results.length} primitive(s).`);
|
|
2420
|
-
} catch (error) {
|
|
2421
|
-
return errorResult(error);
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
);
|
|
2425
|
-
server.registerTool(
|
|
2426
|
-
"workgraph_thread_list",
|
|
2427
|
-
{
|
|
2428
|
-
title: "Thread List",
|
|
2429
|
-
description: "List workspace threads, optionally filtered by status/space/readiness.",
|
|
2430
|
-
inputSchema: {
|
|
2431
|
-
status: z.string().optional(),
|
|
2432
|
-
readyOnly: z.boolean().optional(),
|
|
2433
|
-
space: z.string().optional()
|
|
2434
|
-
},
|
|
2435
|
-
annotations: {
|
|
2436
|
-
readOnlyHint: true,
|
|
2437
|
-
idempotentHint: true
|
|
2438
|
-
}
|
|
2439
|
-
},
|
|
2440
|
-
async (args) => {
|
|
2441
|
-
try {
|
|
2442
|
-
let threads = args.space ? threadsInSpace(options.workspacePath, args.space) : list(options.workspacePath, "thread");
|
|
2443
|
-
const readySet = new Set(
|
|
2444
|
-
(args.space ? listReadyThreadsInSpace(options.workspacePath, args.space) : listReadyThreads(options.workspacePath)).map((entry) => entry.path)
|
|
2445
|
-
);
|
|
2446
|
-
if (args.status) {
|
|
2447
|
-
threads = threads.filter((entry) => String(entry.fields.status) === args.status);
|
|
2448
|
-
}
|
|
2449
|
-
if (args.readyOnly) {
|
|
2450
|
-
threads = threads.filter((entry) => readySet.has(entry.path));
|
|
2451
|
-
}
|
|
2452
|
-
const enriched = threads.map((entry) => ({
|
|
2453
|
-
...entry,
|
|
2454
|
-
ready: readySet.has(entry.path)
|
|
2455
|
-
}));
|
|
2456
|
-
return okResult({ threads: enriched, count: enriched.length }, `Thread list returned ${enriched.length} item(s).`);
|
|
2457
|
-
} catch (error) {
|
|
2458
|
-
return errorResult(error);
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
);
|
|
2462
|
-
server.registerTool(
|
|
2463
|
-
"workgraph_thread_show",
|
|
2464
|
-
{
|
|
2465
|
-
title: "Thread Show",
|
|
2466
|
-
description: "Read one thread and its ledger history.",
|
|
2467
|
-
inputSchema: {
|
|
2468
|
-
threadPath: z.string().min(1)
|
|
2469
|
-
},
|
|
2470
|
-
annotations: {
|
|
2471
|
-
readOnlyHint: true,
|
|
2472
|
-
idempotentHint: true
|
|
2473
|
-
}
|
|
2474
|
-
},
|
|
2475
|
-
async (args) => {
|
|
2476
|
-
try {
|
|
2477
|
-
const threadEntry = read(options.workspacePath, args.threadPath);
|
|
2478
|
-
if (!threadEntry) {
|
|
2479
|
-
return errorResult(`Thread not found: ${args.threadPath}`);
|
|
2480
|
-
}
|
|
2481
|
-
const history = historyOf(options.workspacePath, args.threadPath);
|
|
2482
|
-
return okResult({ thread: threadEntry, history }, `Thread ${args.threadPath} has ${history.length} ledger event(s).`);
|
|
2483
|
-
} catch (error) {
|
|
2484
|
-
return errorResult(error);
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
);
|
|
2488
|
-
server.registerTool(
|
|
2489
|
-
"workgraph_ledger_recent",
|
|
2490
|
-
{
|
|
2491
|
-
title: "Ledger Recent",
|
|
2492
|
-
description: "Read recent ledger events.",
|
|
2493
|
-
inputSchema: {
|
|
2494
|
-
count: z.number().int().min(1).max(500).optional(),
|
|
2495
|
-
actor: z.string().optional()
|
|
2496
|
-
},
|
|
2497
|
-
annotations: {
|
|
2498
|
-
readOnlyHint: true,
|
|
2499
|
-
idempotentHint: true
|
|
2500
|
-
}
|
|
2501
|
-
},
|
|
2502
|
-
async (args) => {
|
|
2503
|
-
try {
|
|
2504
|
-
let entries = recent(options.workspacePath, args.count ?? 20);
|
|
2505
|
-
if (args.actor) {
|
|
2506
|
-
entries = entries.filter((entry) => entry.actor === args.actor);
|
|
2507
|
-
}
|
|
2508
|
-
return okResult({ entries, count: entries.length }, `Ledger returned ${entries.length} event(s).`);
|
|
2509
|
-
} catch (error) {
|
|
2510
|
-
return errorResult(error);
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
);
|
|
2514
|
-
server.registerTool(
|
|
2515
|
-
"workgraph_graph_hygiene",
|
|
2516
|
-
{
|
|
2517
|
-
title: "Graph Hygiene",
|
|
2518
|
-
description: "Generate wiki-link graph hygiene report.",
|
|
2519
|
-
annotations: {
|
|
2520
|
-
readOnlyHint: true,
|
|
2521
|
-
idempotentHint: true
|
|
2522
|
-
}
|
|
2523
|
-
},
|
|
2524
|
-
async () => {
|
|
2525
|
-
try {
|
|
2526
|
-
const report = graphHygieneReport(options.workspacePath);
|
|
2527
|
-
return okResult(
|
|
2528
|
-
report,
|
|
2529
|
-
`Graph hygiene: nodes=${report.nodeCount}, edges=${report.edgeCount}, orphans=${report.orphanCount}, broken=${report.brokenLinkCount}`
|
|
2530
|
-
);
|
|
2531
|
-
} catch (error) {
|
|
2532
|
-
return errorResult(error);
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
);
|
|
2536
|
-
server.registerTool(
|
|
2537
|
-
"workgraph_thread_claim",
|
|
2538
|
-
{
|
|
2539
|
-
title: "Thread Claim",
|
|
2540
|
-
description: "Claim a thread for an actor (policy-scoped write).",
|
|
2541
|
-
inputSchema: {
|
|
2542
|
-
threadPath: z.string().min(1),
|
|
2543
|
-
actor: z.string().optional()
|
|
2544
|
-
},
|
|
2545
|
-
annotations: {
|
|
2546
|
-
destructiveHint: true,
|
|
2547
|
-
idempotentHint: false
|
|
2548
|
-
}
|
|
2549
|
-
},
|
|
2550
|
-
async (args) => {
|
|
2551
|
-
try {
|
|
2552
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2553
|
-
const gate = checkWriteGate(options, actor, ["thread:claim", "mcp:write"]);
|
|
2554
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2555
|
-
const updated = claim(options.workspacePath, args.threadPath, actor);
|
|
2556
|
-
return okResult({ thread: updated }, `Claimed ${updated.path} as ${actor}.`);
|
|
2557
|
-
} catch (error) {
|
|
2558
|
-
return errorResult(error);
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
);
|
|
2562
|
-
server.registerTool(
|
|
2563
|
-
"workgraph_thread_done",
|
|
2564
|
-
{
|
|
2565
|
-
title: "Thread Done",
|
|
2566
|
-
description: "Mark a thread as done with output summary (policy-scoped write).",
|
|
2567
|
-
inputSchema: {
|
|
2568
|
-
threadPath: z.string().min(1),
|
|
2569
|
-
actor: z.string().optional(),
|
|
2570
|
-
output: z.string().optional()
|
|
2571
|
-
},
|
|
2572
|
-
annotations: {
|
|
2573
|
-
destructiveHint: true,
|
|
2574
|
-
idempotentHint: false
|
|
2575
|
-
}
|
|
2576
|
-
},
|
|
2577
|
-
async (args) => {
|
|
2578
|
-
try {
|
|
2579
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2580
|
-
const gate = checkWriteGate(options, actor, ["thread:done", "mcp:write"]);
|
|
2581
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2582
|
-
const updated = done(options.workspacePath, args.threadPath, actor, args.output);
|
|
2583
|
-
return okResult({ thread: updated }, `Marked ${updated.path} done as ${actor}.`);
|
|
2584
|
-
} catch (error) {
|
|
2585
|
-
return errorResult(error);
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
);
|
|
2589
|
-
server.registerTool(
|
|
2590
|
-
"workgraph_checkpoint_create",
|
|
2591
|
-
{
|
|
2592
|
-
title: "Checkpoint Create",
|
|
2593
|
-
description: "Create a checkpoint primitive for hand-off continuity (policy-scoped write).",
|
|
2594
|
-
inputSchema: {
|
|
2595
|
-
actor: z.string().optional(),
|
|
2596
|
-
summary: z.string().min(1),
|
|
2597
|
-
next: z.array(z.string()).optional(),
|
|
2598
|
-
blocked: z.array(z.string()).optional(),
|
|
2599
|
-
tags: z.array(z.string()).optional()
|
|
2600
|
-
},
|
|
2601
|
-
annotations: {
|
|
2602
|
-
destructiveHint: true,
|
|
2603
|
-
idempotentHint: false
|
|
2604
|
-
}
|
|
2605
|
-
},
|
|
2606
|
-
async (args) => {
|
|
2607
|
-
try {
|
|
2608
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2609
|
-
const gate = checkWriteGate(options, actor, ["checkpoint:create", "mcp:write"]);
|
|
2610
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2611
|
-
const checkpoint2 = checkpoint(options.workspacePath, actor, args.summary, {
|
|
2612
|
-
next: args.next,
|
|
2613
|
-
blocked: args.blocked,
|
|
2614
|
-
tags: args.tags
|
|
2615
|
-
});
|
|
2616
|
-
return okResult({ checkpoint: checkpoint2 }, `Created checkpoint ${checkpoint2.path}.`);
|
|
2617
|
-
} catch (error) {
|
|
2618
|
-
return errorResult(error);
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
);
|
|
2622
|
-
server.registerTool(
|
|
2623
|
-
"workgraph_dispatch_create",
|
|
2624
|
-
{
|
|
2625
|
-
title: "Dispatch Create",
|
|
2626
|
-
description: "Create a dispatch run request (policy-scoped write).",
|
|
2627
|
-
inputSchema: {
|
|
2628
|
-
actor: z.string().optional(),
|
|
2629
|
-
objective: z.string().min(1),
|
|
2630
|
-
adapter: z.string().optional(),
|
|
2631
|
-
idempotencyKey: z.string().optional()
|
|
2632
|
-
},
|
|
2633
|
-
annotations: {
|
|
2634
|
-
destructiveHint: true,
|
|
2635
|
-
idempotentHint: false
|
|
2636
|
-
}
|
|
2637
|
-
},
|
|
2638
|
-
async (args) => {
|
|
2639
|
-
try {
|
|
2640
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2641
|
-
const gate = checkWriteGate(options, actor, ["dispatch:run", "mcp:write"]);
|
|
2642
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2643
|
-
const run = createRun(options.workspacePath, {
|
|
2644
|
-
actor,
|
|
2645
|
-
objective: args.objective,
|
|
2646
|
-
adapter: args.adapter,
|
|
2647
|
-
idempotencyKey: args.idempotencyKey
|
|
2648
|
-
});
|
|
2649
|
-
return okResult({ run }, `Created run ${run.id} (${run.status}).`);
|
|
2650
|
-
} catch (error) {
|
|
2651
|
-
return errorResult(error);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
);
|
|
2655
|
-
server.registerTool(
|
|
2656
|
-
"workgraph_dispatch_execute",
|
|
2657
|
-
{
|
|
2658
|
-
title: "Dispatch Execute",
|
|
2659
|
-
description: "Execute one queued/running run through its adapter (policy-scoped write).",
|
|
2660
|
-
inputSchema: {
|
|
2661
|
-
actor: z.string().optional(),
|
|
2662
|
-
runId: z.string().min(1),
|
|
2663
|
-
agents: z.array(z.string()).optional(),
|
|
2664
|
-
maxSteps: z.number().int().min(1).max(5e3).optional(),
|
|
2665
|
-
stepDelayMs: z.number().int().min(0).max(5e3).optional(),
|
|
2666
|
-
space: z.string().optional(),
|
|
2667
|
-
createCheckpoint: z.boolean().optional()
|
|
2668
|
-
},
|
|
2669
|
-
annotations: {
|
|
2670
|
-
destructiveHint: true,
|
|
2671
|
-
idempotentHint: false
|
|
2672
|
-
}
|
|
2673
|
-
},
|
|
2674
|
-
async (args) => {
|
|
2675
|
-
try {
|
|
2676
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2677
|
-
const gate = checkWriteGate(options, actor, ["dispatch:run", "mcp:write"]);
|
|
2678
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2679
|
-
const run = await executeRun(options.workspacePath, args.runId, {
|
|
2680
|
-
actor,
|
|
2681
|
-
agents: args.agents,
|
|
2682
|
-
maxSteps: args.maxSteps,
|
|
2683
|
-
stepDelayMs: args.stepDelayMs,
|
|
2684
|
-
space: args.space,
|
|
2685
|
-
createCheckpoint: args.createCheckpoint
|
|
2686
|
-
});
|
|
2687
|
-
return okResult({ run }, `Executed run ${run.id} -> ${run.status}.`);
|
|
2688
|
-
} catch (error) {
|
|
2689
|
-
return errorResult(error);
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
);
|
|
2693
|
-
server.registerTool(
|
|
2694
|
-
"workgraph_dispatch_followup",
|
|
2695
|
-
{
|
|
2696
|
-
title: "Dispatch Follow-up",
|
|
2697
|
-
description: "Send follow-up input to a run (policy-scoped write).",
|
|
2698
|
-
inputSchema: {
|
|
2699
|
-
actor: z.string().optional(),
|
|
2700
|
-
runId: z.string().min(1),
|
|
2701
|
-
input: z.string().min(1)
|
|
2702
|
-
},
|
|
2703
|
-
annotations: {
|
|
2704
|
-
destructiveHint: true,
|
|
2705
|
-
idempotentHint: false
|
|
2706
|
-
}
|
|
2707
|
-
},
|
|
2708
|
-
async (args) => {
|
|
2709
|
-
try {
|
|
2710
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2711
|
-
const gate = checkWriteGate(options, actor, ["dispatch:run", "mcp:write"]);
|
|
2712
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2713
|
-
const run = followup(options.workspacePath, args.runId, actor, args.input);
|
|
2714
|
-
return okResult({ run }, `Follow-up recorded for ${run.id}.`);
|
|
2715
|
-
} catch (error) {
|
|
2716
|
-
return errorResult(error);
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
);
|
|
2720
|
-
server.registerTool(
|
|
2721
|
-
"workgraph_dispatch_stop",
|
|
2722
|
-
{
|
|
2723
|
-
title: "Dispatch Stop",
|
|
2724
|
-
description: "Stop/cancel a run (policy-scoped write).",
|
|
2725
|
-
inputSchema: {
|
|
2726
|
-
actor: z.string().optional(),
|
|
2727
|
-
runId: z.string().min(1)
|
|
2728
|
-
},
|
|
2729
|
-
annotations: {
|
|
2730
|
-
destructiveHint: true,
|
|
2731
|
-
idempotentHint: false
|
|
2732
|
-
}
|
|
2733
|
-
},
|
|
2734
|
-
async (args) => {
|
|
2735
|
-
try {
|
|
2736
|
-
const actor = resolveActor(args.actor, options.defaultActor);
|
|
2737
|
-
const gate = checkWriteGate(options, actor, ["dispatch:run", "mcp:write"]);
|
|
2738
|
-
if (!gate.allowed) return errorResult(gate.reason);
|
|
2739
|
-
const run = stop(options.workspacePath, args.runId, actor);
|
|
2740
|
-
return okResult({ run }, `Stopped run ${run.id}.`);
|
|
2741
|
-
} catch (error) {
|
|
2742
|
-
return errorResult(error);
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
);
|
|
2746
|
-
}
|
|
2747
|
-
function resolveActor(actor, defaultActor) {
|
|
2748
|
-
const resolved = actor ?? defaultActor ?? "anonymous";
|
|
2749
|
-
return String(resolved);
|
|
2750
|
-
}
|
|
2751
|
-
function checkWriteGate(options, actor, requiredCapabilities) {
|
|
2752
|
-
if (options.readOnly) {
|
|
2753
|
-
return {
|
|
2754
|
-
allowed: false,
|
|
2755
|
-
reason: "MCP server is configured read-only; write tool is disabled."
|
|
2756
|
-
};
|
|
2757
|
-
}
|
|
2758
|
-
if (actor === "system") {
|
|
2759
|
-
return { allowed: true };
|
|
2760
|
-
}
|
|
2761
|
-
const party = getParty(options.workspacePath, actor);
|
|
2762
|
-
if (!party) {
|
|
2763
|
-
return {
|
|
2764
|
-
allowed: false,
|
|
2765
|
-
reason: `Policy gate blocked MCP write: actor "${actor}" is not a registered party.`
|
|
2766
|
-
};
|
|
2767
|
-
}
|
|
2768
|
-
const hasCapability = requiredCapabilities.some((capability) => party.capabilities.includes(capability));
|
|
2769
|
-
if (!hasCapability) {
|
|
2770
|
-
return {
|
|
2771
|
-
allowed: false,
|
|
2772
|
-
reason: `Policy gate blocked MCP write: actor "${actor}" lacks capabilities [${requiredCapabilities.join(", ")}].`
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2775
|
-
return { allowed: true };
|
|
2776
|
-
}
|
|
2777
|
-
function okResult(data, summary) {
|
|
2778
|
-
return {
|
|
2779
|
-
content: [
|
|
2780
|
-
{
|
|
2781
|
-
type: "text",
|
|
2782
|
-
text: `${summary}
|
|
2783
|
-
|
|
2784
|
-
${toPrettyJson(data)}`
|
|
2785
|
-
}
|
|
2786
|
-
],
|
|
2787
|
-
structuredContent: data
|
|
2788
|
-
};
|
|
2789
|
-
}
|
|
2790
|
-
function errorResult(error) {
|
|
2791
|
-
const text = error instanceof Error ? error.message : String(error);
|
|
2792
|
-
return {
|
|
2793
|
-
isError: true,
|
|
2794
|
-
content: [
|
|
2795
|
-
{
|
|
2796
|
-
type: "text",
|
|
2797
|
-
text
|
|
2798
|
-
}
|
|
2799
|
-
]
|
|
2800
|
-
};
|
|
2801
|
-
}
|
|
2802
|
-
function toPrettyJson(value) {
|
|
2803
|
-
return JSON.stringify(value, null, 2);
|
|
2804
|
-
}
|
|
2805
|
-
function renderStatusSummary(snapshot) {
|
|
2806
|
-
return [
|
|
2807
|
-
`threads(total=${snapshot.threads.total}, open=${snapshot.threads.open}, active=${snapshot.threads.active}, blocked=${snapshot.threads.blocked}, done=${snapshot.threads.done})`,
|
|
2808
|
-
`claims(active=${snapshot.claims.active})`,
|
|
2809
|
-
`primitives(total=${snapshot.primitives.total})`
|
|
2810
|
-
].join(" ");
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
export {
|
|
2814
|
-
__export,
|
|
2815
|
-
THREAD_STATUS_TRANSITIONS,
|
|
2816
|
-
append,
|
|
2817
|
-
historyOf,
|
|
2818
|
-
allClaims,
|
|
2819
|
-
recent,
|
|
2820
|
-
ledger_exports,
|
|
2821
|
-
loadRegistry,
|
|
2822
|
-
saveRegistry,
|
|
2823
|
-
listTypes,
|
|
2824
|
-
registry_exports,
|
|
2825
|
-
refreshWikiLinkGraphIndex,
|
|
2826
|
-
graph_exports,
|
|
2827
|
-
loadPolicyRegistry,
|
|
2828
|
-
policy_exports,
|
|
2829
|
-
create,
|
|
2830
|
-
read,
|
|
2831
|
-
list,
|
|
2832
|
-
update,
|
|
2833
|
-
store_exports,
|
|
2834
|
-
createThread,
|
|
2835
|
-
thread_exports,
|
|
2836
|
-
keywordSearch,
|
|
2837
|
-
query_exports,
|
|
2838
|
-
checkpoint,
|
|
2839
|
-
orientation_exports,
|
|
2840
|
-
CursorCloudAdapter,
|
|
2841
|
-
createRun,
|
|
2842
|
-
dispatch_exports,
|
|
2843
|
-
createWorkgraphMcpServer,
|
|
2844
|
-
startWorkgraphMcpServer,
|
|
2845
|
-
mcp_server_exports
|
|
2846
|
-
};
|