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