@versatly/workgraph 0.1.0 → 0.2.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 +66 -11
- package/SKILL.md +10 -10
- package/bin/workgraph.js +0 -0
- package/dist/chunk-XUMA4O2Z.js +2817 -0
- package/dist/cli.js +574 -23
- package/dist/index.d.ts +402 -7
- package/dist/index.js +19 -1
- package/package.json +15 -6
- package/dist/chunk-CRQXDCPR.js +0 -1443
package/dist/chunk-CRQXDCPR.js
DELETED
|
@@ -1,1443 +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 = ".clawvault/ledger.jsonl";
|
|
58
|
-
var LEDGER_INDEX_FILE = ".clawvault/ledger-index.json";
|
|
59
|
-
var LEDGER_CHAIN_FILE = ".clawvault/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 = ".clawvault/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: { type: "string", required: true, default: "open", description: "open | active | blocked | done | cancelled" },
|
|
377
|
-
owner: { type: "string", description: "Agent that claimed this thread" },
|
|
378
|
-
priority: { type: "string", default: "medium", description: "urgent | high | medium | low" },
|
|
379
|
-
deps: { type: "list", default: [], description: "Thread refs this depends on" },
|
|
380
|
-
parent: { type: "ref", description: "Parent thread if decomposed from larger thread" },
|
|
381
|
-
space: { type: "ref", description: "Space ref this thread belongs to" },
|
|
382
|
-
context_refs: { type: "list", default: [], description: "Docs that inform this work" },
|
|
383
|
-
tags: { type: "list", default: [], description: "Freeform tags" },
|
|
384
|
-
created: { type: "date", required: true },
|
|
385
|
-
updated: { type: "date", required: true }
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
name: "space",
|
|
390
|
-
description: "A workspace boundary that groups related threads and sets context.",
|
|
391
|
-
directory: "spaces",
|
|
392
|
-
builtIn: true,
|
|
393
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
394
|
-
createdBy: "system",
|
|
395
|
-
fields: {
|
|
396
|
-
title: { type: "string", required: true, description: "Space name" },
|
|
397
|
-
description: { type: "string", description: "What this space is for" },
|
|
398
|
-
members: { type: "list", default: [], description: "Agent names that participate" },
|
|
399
|
-
thread_refs: { type: "list", default: [], description: "Thread refs in this space" },
|
|
400
|
-
tags: { type: "list", default: [], description: "Freeform tags" },
|
|
401
|
-
created: { type: "date", required: true },
|
|
402
|
-
updated: { type: "date", required: true }
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: "decision",
|
|
407
|
-
description: "A recorded decision with reasoning and context.",
|
|
408
|
-
directory: "decisions",
|
|
409
|
-
builtIn: true,
|
|
410
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
411
|
-
createdBy: "system",
|
|
412
|
-
fields: {
|
|
413
|
-
title: { type: "string", required: true },
|
|
414
|
-
date: { type: "date", required: true },
|
|
415
|
-
status: { type: "string", default: "active", description: "active | superseded | reverted" },
|
|
416
|
-
context_refs: { type: "list", default: [], description: "What informed this decision" },
|
|
417
|
-
tags: { type: "list", default: [] }
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
name: "lesson",
|
|
422
|
-
description: "A captured insight or pattern learned from experience.",
|
|
423
|
-
directory: "lessons",
|
|
424
|
-
builtIn: true,
|
|
425
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
426
|
-
createdBy: "system",
|
|
427
|
-
fields: {
|
|
428
|
-
title: { type: "string", required: true },
|
|
429
|
-
date: { type: "date", required: true },
|
|
430
|
-
confidence: { type: "string", default: "medium", description: "high | medium | low" },
|
|
431
|
-
context_refs: { type: "list", default: [] },
|
|
432
|
-
tags: { type: "list", default: [] }
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
name: "fact",
|
|
437
|
-
description: "A structured piece of knowledge with optional temporal validity.",
|
|
438
|
-
directory: "facts",
|
|
439
|
-
builtIn: true,
|
|
440
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
441
|
-
createdBy: "system",
|
|
442
|
-
fields: {
|
|
443
|
-
subject: { type: "string", required: true },
|
|
444
|
-
predicate: { type: "string", required: true },
|
|
445
|
-
object: { type: "string", required: true },
|
|
446
|
-
confidence: { type: "number", default: 1 },
|
|
447
|
-
valid_from: { type: "date" },
|
|
448
|
-
valid_until: { type: "date" },
|
|
449
|
-
source: { type: "ref", description: "Where this fact came from" }
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
name: "agent",
|
|
454
|
-
description: "A registered participant in the workgraph.",
|
|
455
|
-
directory: "agents",
|
|
456
|
-
builtIn: true,
|
|
457
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
458
|
-
createdBy: "system",
|
|
459
|
-
fields: {
|
|
460
|
-
name: { type: "string", required: true },
|
|
461
|
-
role: { type: "string", description: "What this agent specializes in" },
|
|
462
|
-
capabilities: { type: "list", default: [], description: "What this agent can do" },
|
|
463
|
-
active_threads: { type: "list", default: [], description: "Threads currently claimed" },
|
|
464
|
-
last_seen: { type: "date" }
|
|
465
|
-
}
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
name: "skill",
|
|
469
|
-
description: "A reusable agent skill shared through the workgraph workspace.",
|
|
470
|
-
directory: "skills",
|
|
471
|
-
builtIn: true,
|
|
472
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
473
|
-
createdBy: "system",
|
|
474
|
-
fields: {
|
|
475
|
-
title: { type: "string", required: true, description: "Skill title" },
|
|
476
|
-
status: { type: "string", required: true, default: "draft", description: "draft | proposed | active | deprecated | archived" },
|
|
477
|
-
version: { type: "string", default: "0.1.0", description: "Semantic version of this skill" },
|
|
478
|
-
owner: { type: "string", description: "Primary skill owner/maintainer" },
|
|
479
|
-
reviewers: { type: "list", default: [], description: "Reviewers involved in proposal" },
|
|
480
|
-
proposal_thread: { type: "ref", description: "Thread coordinating review/promotion" },
|
|
481
|
-
proposed_at: { type: "date" },
|
|
482
|
-
promoted_at: { type: "date" },
|
|
483
|
-
distribution: { type: "string", default: "tailscale-shared-vault", description: "Distribution channel for skill usage" },
|
|
484
|
-
tailscale_path: { type: "string", description: "Shared vault path over Tailscale" },
|
|
485
|
-
tags: { type: "list", default: [] },
|
|
486
|
-
created: { type: "date", required: true },
|
|
487
|
-
updated: { type: "date", required: true }
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
];
|
|
491
|
-
function registryPath(workspacePath) {
|
|
492
|
-
return path2.join(workspacePath, REGISTRY_FILE);
|
|
493
|
-
}
|
|
494
|
-
function loadRegistry(workspacePath) {
|
|
495
|
-
const rPath = registryPath(workspacePath);
|
|
496
|
-
if (fs2.existsSync(rPath)) {
|
|
497
|
-
const raw = fs2.readFileSync(rPath, "utf-8");
|
|
498
|
-
const registry = JSON.parse(raw);
|
|
499
|
-
return ensureBuiltIns(registry);
|
|
500
|
-
}
|
|
501
|
-
return seedRegistry();
|
|
502
|
-
}
|
|
503
|
-
function saveRegistry(workspacePath, registry) {
|
|
504
|
-
const rPath = registryPath(workspacePath);
|
|
505
|
-
const dir = path2.dirname(rPath);
|
|
506
|
-
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
507
|
-
fs2.writeFileSync(rPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
508
|
-
}
|
|
509
|
-
function defineType(workspacePath, name, description, fields, actor, directory) {
|
|
510
|
-
const registry = loadRegistry(workspacePath);
|
|
511
|
-
const safeName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
512
|
-
if (registry.types[safeName]?.builtIn) {
|
|
513
|
-
throw new Error(`Cannot redefine built-in type "${safeName}". You can extend it with new fields instead.`);
|
|
514
|
-
}
|
|
515
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
516
|
-
const typeDef = {
|
|
517
|
-
name: safeName,
|
|
518
|
-
description,
|
|
519
|
-
fields: {
|
|
520
|
-
title: { type: "string", required: true },
|
|
521
|
-
created: { type: "date", required: true },
|
|
522
|
-
updated: { type: "date", required: true },
|
|
523
|
-
tags: { type: "list", default: [] },
|
|
524
|
-
...fields
|
|
525
|
-
},
|
|
526
|
-
directory: directory ?? `${safeName}s`,
|
|
527
|
-
builtIn: false,
|
|
528
|
-
createdAt: now,
|
|
529
|
-
createdBy: actor
|
|
530
|
-
};
|
|
531
|
-
registry.types[safeName] = typeDef;
|
|
532
|
-
saveRegistry(workspacePath, registry);
|
|
533
|
-
append(workspacePath, actor, "define", ".clawvault/registry.json", safeName, {
|
|
534
|
-
name: safeName,
|
|
535
|
-
directory: typeDef.directory,
|
|
536
|
-
fields: Object.keys(typeDef.fields)
|
|
537
|
-
});
|
|
538
|
-
return typeDef;
|
|
539
|
-
}
|
|
540
|
-
function getType(workspacePath, name) {
|
|
541
|
-
const registry = loadRegistry(workspacePath);
|
|
542
|
-
return registry.types[name];
|
|
543
|
-
}
|
|
544
|
-
function listTypes(workspacePath) {
|
|
545
|
-
const registry = loadRegistry(workspacePath);
|
|
546
|
-
return Object.values(registry.types);
|
|
547
|
-
}
|
|
548
|
-
function extendType(workspacePath, name, newFields, _actor) {
|
|
549
|
-
const registry = loadRegistry(workspacePath);
|
|
550
|
-
const existing = registry.types[name];
|
|
551
|
-
if (!existing) throw new Error(`Type "${name}" not found in registry.`);
|
|
552
|
-
existing.fields = { ...existing.fields, ...newFields };
|
|
553
|
-
saveRegistry(workspacePath, registry);
|
|
554
|
-
return existing;
|
|
555
|
-
}
|
|
556
|
-
function seedRegistry() {
|
|
557
|
-
const types = {};
|
|
558
|
-
for (const t of BUILT_IN_TYPES) {
|
|
559
|
-
types[t.name] = t;
|
|
560
|
-
}
|
|
561
|
-
return { version: CURRENT_VERSION, types };
|
|
562
|
-
}
|
|
563
|
-
function ensureBuiltIns(registry) {
|
|
564
|
-
for (const t of BUILT_IN_TYPES) {
|
|
565
|
-
if (!registry.types[t.name]) {
|
|
566
|
-
registry.types[t.name] = t;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return registry;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// src/store.ts
|
|
573
|
-
var store_exports = {};
|
|
574
|
-
__export(store_exports, {
|
|
575
|
-
activeThreads: () => activeThreads,
|
|
576
|
-
blockedThreads: () => blockedThreads,
|
|
577
|
-
create: () => create,
|
|
578
|
-
findByField: () => findByField,
|
|
579
|
-
list: () => list,
|
|
580
|
-
openThreads: () => openThreads,
|
|
581
|
-
read: () => read,
|
|
582
|
-
remove: () => remove,
|
|
583
|
-
threadsInSpace: () => threadsInSpace,
|
|
584
|
-
update: () => update
|
|
585
|
-
});
|
|
586
|
-
import fs3 from "fs";
|
|
587
|
-
import path3 from "path";
|
|
588
|
-
import matter from "gray-matter";
|
|
589
|
-
function create(workspacePath, typeName, fields, body, actor) {
|
|
590
|
-
const typeDef = getType(workspacePath, typeName);
|
|
591
|
-
if (!typeDef) {
|
|
592
|
-
throw new Error(`Unknown primitive type "${typeName}". Run \`workgraph primitive list\` to see available types, or \`workgraph primitive define\` to create one.`);
|
|
593
|
-
}
|
|
594
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
595
|
-
const merged = applyDefaults(typeDef, {
|
|
596
|
-
...fields,
|
|
597
|
-
created: fields.created ?? now,
|
|
598
|
-
updated: now
|
|
599
|
-
});
|
|
600
|
-
validateFields(typeDef, merged, "create");
|
|
601
|
-
const slug = slugify(String(merged.title ?? merged.name ?? typeName));
|
|
602
|
-
const relDir = typeDef.directory;
|
|
603
|
-
const relPath = `${relDir}/${slug}.md`;
|
|
604
|
-
const absDir = path3.join(workspacePath, relDir);
|
|
605
|
-
const absPath = path3.join(workspacePath, relPath);
|
|
606
|
-
if (!fs3.existsSync(absDir)) fs3.mkdirSync(absDir, { recursive: true });
|
|
607
|
-
if (fs3.existsSync(absPath)) {
|
|
608
|
-
throw new Error(`File already exists: ${relPath}. Use update instead.`);
|
|
609
|
-
}
|
|
610
|
-
const content = matter.stringify(body, stripUndefined(merged));
|
|
611
|
-
fs3.writeFileSync(absPath, content, "utf-8");
|
|
612
|
-
append(workspacePath, actor, "create", relPath, typeName, {
|
|
613
|
-
title: merged.title ?? slug
|
|
614
|
-
});
|
|
615
|
-
return { path: relPath, type: typeName, fields: merged, body };
|
|
616
|
-
}
|
|
617
|
-
function read(workspacePath, relPath) {
|
|
618
|
-
const absPath = path3.join(workspacePath, relPath);
|
|
619
|
-
if (!fs3.existsSync(absPath)) return null;
|
|
620
|
-
const raw = fs3.readFileSync(absPath, "utf-8");
|
|
621
|
-
const { data, content } = matter(raw);
|
|
622
|
-
const typeName = inferType(workspacePath, relPath);
|
|
623
|
-
return { path: relPath, type: typeName, fields: data, body: content.trim() };
|
|
624
|
-
}
|
|
625
|
-
function list(workspacePath, typeName) {
|
|
626
|
-
const typeDef = getType(workspacePath, typeName);
|
|
627
|
-
if (!typeDef) return [];
|
|
628
|
-
const dir = path3.join(workspacePath, typeDef.directory);
|
|
629
|
-
if (!fs3.existsSync(dir)) return [];
|
|
630
|
-
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
631
|
-
const instances = [];
|
|
632
|
-
for (const file of files) {
|
|
633
|
-
const relPath = `${typeDef.directory}/${file}`;
|
|
634
|
-
const inst = read(workspacePath, relPath);
|
|
635
|
-
if (inst) instances.push(inst);
|
|
636
|
-
}
|
|
637
|
-
return instances;
|
|
638
|
-
}
|
|
639
|
-
function update(workspacePath, relPath, fieldUpdates, bodyUpdate, actor) {
|
|
640
|
-
const existing = read(workspacePath, relPath);
|
|
641
|
-
if (!existing) throw new Error(`Not found: ${relPath}`);
|
|
642
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
643
|
-
const newFields = { ...existing.fields, ...fieldUpdates, updated: now };
|
|
644
|
-
const typeDef = getType(workspacePath, existing.type);
|
|
645
|
-
if (!typeDef) throw new Error(`Unknown primitive type "${existing.type}" for ${relPath}`);
|
|
646
|
-
validateFields(typeDef, newFields, "update");
|
|
647
|
-
const newBody = bodyUpdate ?? existing.body;
|
|
648
|
-
const absPath = path3.join(workspacePath, relPath);
|
|
649
|
-
const content = matter.stringify(newBody, stripUndefined(newFields));
|
|
650
|
-
fs3.writeFileSync(absPath, content, "utf-8");
|
|
651
|
-
append(workspacePath, actor, "update", relPath, existing.type, {
|
|
652
|
-
changed: Object.keys(fieldUpdates)
|
|
653
|
-
});
|
|
654
|
-
return { path: relPath, type: existing.type, fields: newFields, body: newBody };
|
|
655
|
-
}
|
|
656
|
-
function remove(workspacePath, relPath, actor) {
|
|
657
|
-
const absPath = path3.join(workspacePath, relPath);
|
|
658
|
-
if (!fs3.existsSync(absPath)) throw new Error(`Not found: ${relPath}`);
|
|
659
|
-
const archiveDir = path3.join(workspacePath, ".clawvault", "archive");
|
|
660
|
-
if (!fs3.existsSync(archiveDir)) fs3.mkdirSync(archiveDir, { recursive: true });
|
|
661
|
-
const archivePath = path3.join(archiveDir, path3.basename(relPath));
|
|
662
|
-
fs3.renameSync(absPath, archivePath);
|
|
663
|
-
const typeName = inferType(workspacePath, relPath);
|
|
664
|
-
append(workspacePath, actor, "delete", relPath, typeName);
|
|
665
|
-
}
|
|
666
|
-
function findByField(workspacePath, typeName, field, value) {
|
|
667
|
-
return list(workspacePath, typeName).filter((inst) => inst.fields[field] === value);
|
|
668
|
-
}
|
|
669
|
-
function openThreads(workspacePath) {
|
|
670
|
-
return findByField(workspacePath, "thread", "status", "open");
|
|
671
|
-
}
|
|
672
|
-
function activeThreads(workspacePath) {
|
|
673
|
-
return findByField(workspacePath, "thread", "status", "active");
|
|
674
|
-
}
|
|
675
|
-
function blockedThreads(workspacePath) {
|
|
676
|
-
return findByField(workspacePath, "thread", "status", "blocked");
|
|
677
|
-
}
|
|
678
|
-
function threadsInSpace(workspacePath, spaceRef) {
|
|
679
|
-
const normalizedTarget = normalizeRefPath(spaceRef);
|
|
680
|
-
return list(workspacePath, "thread").filter((thread) => {
|
|
681
|
-
const rawSpace = thread.fields.space;
|
|
682
|
-
if (!rawSpace) return false;
|
|
683
|
-
return normalizeRefPath(rawSpace) === normalizedTarget;
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
function slugify(text) {
|
|
687
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
688
|
-
}
|
|
689
|
-
function applyDefaults(typeDef, fields) {
|
|
690
|
-
const result = { ...fields };
|
|
691
|
-
for (const [key, def] of Object.entries(typeDef.fields)) {
|
|
692
|
-
if (result[key] === void 0 && def.default !== void 0) {
|
|
693
|
-
result[key] = def.default;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return result;
|
|
697
|
-
}
|
|
698
|
-
function stripUndefined(obj) {
|
|
699
|
-
const result = {};
|
|
700
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
701
|
-
if (v !== void 0) result[k] = v;
|
|
702
|
-
}
|
|
703
|
-
return result;
|
|
704
|
-
}
|
|
705
|
-
function inferType(workspacePath, relPath) {
|
|
706
|
-
const registry = loadRegistry(workspacePath);
|
|
707
|
-
const dir = relPath.split("/")[0];
|
|
708
|
-
for (const typeDef of Object.values(registry.types)) {
|
|
709
|
-
if (typeDef.directory === dir) return typeDef.name;
|
|
710
|
-
}
|
|
711
|
-
return "unknown";
|
|
712
|
-
}
|
|
713
|
-
function normalizeRefPath(value) {
|
|
714
|
-
const raw = String(value ?? "").trim();
|
|
715
|
-
if (!raw) return "";
|
|
716
|
-
const unwrapped = raw.startsWith("[[") && raw.endsWith("]]") ? raw.slice(2, -2) : raw;
|
|
717
|
-
return unwrapped.endsWith(".md") ? unwrapped : `${unwrapped}.md`;
|
|
718
|
-
}
|
|
719
|
-
function validateFields(typeDef, fields, mode) {
|
|
720
|
-
const issues = [];
|
|
721
|
-
for (const [fieldName, definition] of Object.entries(typeDef.fields)) {
|
|
722
|
-
const value = fields[fieldName];
|
|
723
|
-
if (definition.required && isMissingRequiredValue(value)) {
|
|
724
|
-
issues.push(`Missing required field "${fieldName}"`);
|
|
725
|
-
continue;
|
|
726
|
-
}
|
|
727
|
-
if (value === void 0 || value === null) continue;
|
|
728
|
-
if (!isFieldTypeCompatible(definition.type, value)) {
|
|
729
|
-
issues.push(`Field "${fieldName}" expected ${definition.type}, got ${describeValue(value)}`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
if (issues.length > 0) {
|
|
733
|
-
throw new Error(`Invalid ${typeDef.name} ${mode} payload: ${issues.join("; ")}`);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
function isMissingRequiredValue(value) {
|
|
737
|
-
if (value === void 0 || value === null) return true;
|
|
738
|
-
if (typeof value === "string" && value.trim().length === 0) return true;
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
|
-
function isFieldTypeCompatible(type, value) {
|
|
742
|
-
switch (type) {
|
|
743
|
-
case "string":
|
|
744
|
-
case "ref":
|
|
745
|
-
case "date":
|
|
746
|
-
return typeof value === "string";
|
|
747
|
-
case "number":
|
|
748
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
749
|
-
case "boolean":
|
|
750
|
-
return typeof value === "boolean";
|
|
751
|
-
case "list":
|
|
752
|
-
return Array.isArray(value);
|
|
753
|
-
case "any":
|
|
754
|
-
return true;
|
|
755
|
-
default:
|
|
756
|
-
return true;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function describeValue(value) {
|
|
760
|
-
if (Array.isArray(value)) return "array";
|
|
761
|
-
if (value === null) return "null";
|
|
762
|
-
return typeof value;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// src/thread.ts
|
|
766
|
-
var thread_exports = {};
|
|
767
|
-
__export(thread_exports, {
|
|
768
|
-
block: () => block,
|
|
769
|
-
cancel: () => cancel,
|
|
770
|
-
claim: () => claim,
|
|
771
|
-
claimNextReady: () => claimNextReady,
|
|
772
|
-
claimNextReadyInSpace: () => claimNextReadyInSpace,
|
|
773
|
-
createThread: () => createThread,
|
|
774
|
-
decompose: () => decompose,
|
|
775
|
-
done: () => done,
|
|
776
|
-
isReadyForClaim: () => isReadyForClaim,
|
|
777
|
-
listReadyThreads: () => listReadyThreads,
|
|
778
|
-
listReadyThreadsInSpace: () => listReadyThreadsInSpace,
|
|
779
|
-
pickNextReadyThread: () => pickNextReadyThread,
|
|
780
|
-
pickNextReadyThreadInSpace: () => pickNextReadyThreadInSpace,
|
|
781
|
-
release: () => release,
|
|
782
|
-
unblock: () => unblock
|
|
783
|
-
});
|
|
784
|
-
function createThread(workspacePath, title, goal, actor, opts = {}) {
|
|
785
|
-
const normalizedSpace = opts.space ? normalizeWorkspaceRef(opts.space) : void 0;
|
|
786
|
-
const contextRefs = opts.context_refs ?? [];
|
|
787
|
-
const mergedContextRefs = normalizedSpace && !contextRefs.includes(normalizedSpace) ? [...contextRefs, normalizedSpace] : contextRefs;
|
|
788
|
-
return create(workspacePath, "thread", {
|
|
789
|
-
title,
|
|
790
|
-
goal,
|
|
791
|
-
status: "open",
|
|
792
|
-
priority: opts.priority ?? "medium",
|
|
793
|
-
deps: opts.deps ?? [],
|
|
794
|
-
parent: opts.parent,
|
|
795
|
-
space: normalizedSpace,
|
|
796
|
-
context_refs: mergedContextRefs,
|
|
797
|
-
tags: opts.tags ?? []
|
|
798
|
-
}, `## Goal
|
|
799
|
-
|
|
800
|
-
${goal}
|
|
801
|
-
`, actor);
|
|
802
|
-
}
|
|
803
|
-
function isReadyForClaim(workspacePath, threadPathOrInstance) {
|
|
804
|
-
const instance = typeof threadPathOrInstance === "string" ? read(workspacePath, threadPathOrInstance) : threadPathOrInstance;
|
|
805
|
-
if (!instance) return false;
|
|
806
|
-
if (instance.type !== "thread") return false;
|
|
807
|
-
if (instance.fields.status !== "open") return false;
|
|
808
|
-
const hasUnfinishedChildren = list(workspacePath, "thread").some(
|
|
809
|
-
(candidate) => candidate.fields.parent === instance.path && !["done", "cancelled"].includes(String(candidate.fields.status))
|
|
810
|
-
);
|
|
811
|
-
if (hasUnfinishedChildren) return false;
|
|
812
|
-
const deps = Array.isArray(instance.fields.deps) ? instance.fields.deps : [];
|
|
813
|
-
if (deps.length === 0) return true;
|
|
814
|
-
for (const dep of deps) {
|
|
815
|
-
const depRef = normalizeThreadRef(dep);
|
|
816
|
-
if (depRef.startsWith("external/")) return false;
|
|
817
|
-
const depThread = read(workspacePath, depRef);
|
|
818
|
-
if (!depThread || depThread.fields.status !== "done") {
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
return true;
|
|
823
|
-
}
|
|
824
|
-
function listReadyThreads(workspacePath) {
|
|
825
|
-
const open = openThreads(workspacePath);
|
|
826
|
-
return open.filter((t) => isReadyForClaim(workspacePath, t)).sort(compareThreadPriority);
|
|
827
|
-
}
|
|
828
|
-
function listReadyThreadsInSpace(workspacePath, spaceRef) {
|
|
829
|
-
const normalizedSpace = normalizeWorkspaceRef(spaceRef);
|
|
830
|
-
return listReadyThreads(workspacePath).filter(
|
|
831
|
-
(thread) => normalizeWorkspaceRef(thread.fields.space) === normalizedSpace
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
function pickNextReadyThread(workspacePath) {
|
|
835
|
-
const ready = listReadyThreads(workspacePath);
|
|
836
|
-
return ready[0] ?? null;
|
|
837
|
-
}
|
|
838
|
-
function pickNextReadyThreadInSpace(workspacePath, spaceRef) {
|
|
839
|
-
const ready = listReadyThreadsInSpace(workspacePath, spaceRef);
|
|
840
|
-
return ready[0] ?? null;
|
|
841
|
-
}
|
|
842
|
-
function claimNextReady(workspacePath, actor) {
|
|
843
|
-
const next = pickNextReadyThread(workspacePath);
|
|
844
|
-
if (!next) return null;
|
|
845
|
-
return claim(workspacePath, next.path, actor);
|
|
846
|
-
}
|
|
847
|
-
function claimNextReadyInSpace(workspacePath, actor, spaceRef) {
|
|
848
|
-
const next = pickNextReadyThreadInSpace(workspacePath, spaceRef);
|
|
849
|
-
if (!next) return null;
|
|
850
|
-
return claim(workspacePath, next.path, actor);
|
|
851
|
-
}
|
|
852
|
-
function claim(workspacePath, threadPath, actor) {
|
|
853
|
-
const thread = read(workspacePath, threadPath);
|
|
854
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
855
|
-
const status = thread.fields.status;
|
|
856
|
-
if (status !== "open") {
|
|
857
|
-
throw new Error(`Cannot claim thread in "${status}" state. Only "open" threads can be claimed.`);
|
|
858
|
-
}
|
|
859
|
-
const owner = currentOwner(workspacePath, threadPath);
|
|
860
|
-
if (owner) {
|
|
861
|
-
throw new Error(`Thread already claimed by "${owner}". Wait for release or use a different thread.`);
|
|
862
|
-
}
|
|
863
|
-
append(workspacePath, actor, "claim", threadPath, "thread");
|
|
864
|
-
return update(workspacePath, threadPath, {
|
|
865
|
-
status: "active",
|
|
866
|
-
owner: actor
|
|
867
|
-
}, void 0, actor);
|
|
868
|
-
}
|
|
869
|
-
function release(workspacePath, threadPath, actor, reason) {
|
|
870
|
-
const thread = read(workspacePath, threadPath);
|
|
871
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
872
|
-
assertOwner(workspacePath, threadPath, actor);
|
|
873
|
-
append(
|
|
874
|
-
workspacePath,
|
|
875
|
-
actor,
|
|
876
|
-
"release",
|
|
877
|
-
threadPath,
|
|
878
|
-
"thread",
|
|
879
|
-
reason ? { reason } : void 0
|
|
880
|
-
);
|
|
881
|
-
return update(workspacePath, threadPath, {
|
|
882
|
-
status: "open",
|
|
883
|
-
owner: null
|
|
884
|
-
}, void 0, actor);
|
|
885
|
-
}
|
|
886
|
-
function block(workspacePath, threadPath, actor, blockedBy, reason) {
|
|
887
|
-
const thread = read(workspacePath, threadPath);
|
|
888
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
889
|
-
assertTransition(thread.fields.status, "blocked");
|
|
890
|
-
append(workspacePath, actor, "block", threadPath, "thread", {
|
|
891
|
-
blocked_by: blockedBy,
|
|
892
|
-
...reason ? { reason } : {}
|
|
893
|
-
});
|
|
894
|
-
const currentDeps = thread.fields.deps ?? [];
|
|
895
|
-
const updatedDeps = currentDeps.includes(blockedBy) ? currentDeps : [...currentDeps, blockedBy];
|
|
896
|
-
return update(workspacePath, threadPath, {
|
|
897
|
-
status: "blocked",
|
|
898
|
-
deps: updatedDeps
|
|
899
|
-
}, void 0, actor);
|
|
900
|
-
}
|
|
901
|
-
function unblock(workspacePath, threadPath, actor) {
|
|
902
|
-
const thread = read(workspacePath, threadPath);
|
|
903
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
904
|
-
assertTransition(thread.fields.status, "active");
|
|
905
|
-
append(workspacePath, actor, "unblock", threadPath, "thread");
|
|
906
|
-
return update(workspacePath, threadPath, {
|
|
907
|
-
status: "active"
|
|
908
|
-
}, void 0, actor);
|
|
909
|
-
}
|
|
910
|
-
function done(workspacePath, threadPath, actor, output) {
|
|
911
|
-
const thread = read(workspacePath, threadPath);
|
|
912
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
913
|
-
assertTransition(thread.fields.status, "done");
|
|
914
|
-
assertOwner(workspacePath, threadPath, actor);
|
|
915
|
-
append(
|
|
916
|
-
workspacePath,
|
|
917
|
-
actor,
|
|
918
|
-
"done",
|
|
919
|
-
threadPath,
|
|
920
|
-
"thread",
|
|
921
|
-
output ? { output } : void 0
|
|
922
|
-
);
|
|
923
|
-
const newBody = output ? `${thread.body}
|
|
924
|
-
|
|
925
|
-
## Output
|
|
926
|
-
|
|
927
|
-
${output}
|
|
928
|
-
` : thread.body;
|
|
929
|
-
return update(workspacePath, threadPath, {
|
|
930
|
-
status: "done"
|
|
931
|
-
}, newBody, actor);
|
|
932
|
-
}
|
|
933
|
-
function cancel(workspacePath, threadPath, actor, reason) {
|
|
934
|
-
const thread = read(workspacePath, threadPath);
|
|
935
|
-
if (!thread) throw new Error(`Thread not found: ${threadPath}`);
|
|
936
|
-
assertTransition(thread.fields.status, "cancelled");
|
|
937
|
-
append(
|
|
938
|
-
workspacePath,
|
|
939
|
-
actor,
|
|
940
|
-
"cancel",
|
|
941
|
-
threadPath,
|
|
942
|
-
"thread",
|
|
943
|
-
reason ? { reason } : void 0
|
|
944
|
-
);
|
|
945
|
-
return update(workspacePath, threadPath, {
|
|
946
|
-
status: "cancelled",
|
|
947
|
-
owner: null
|
|
948
|
-
}, void 0, actor);
|
|
949
|
-
}
|
|
950
|
-
function decompose(workspacePath, parentPath, subthreads, actor) {
|
|
951
|
-
const parent = read(workspacePath, parentPath);
|
|
952
|
-
if (!parent) throw new Error(`Thread not found: ${parentPath}`);
|
|
953
|
-
const created = [];
|
|
954
|
-
for (const sub of subthreads) {
|
|
955
|
-
const inst = createThread(workspacePath, sub.title, sub.goal, actor, {
|
|
956
|
-
parent: parentPath,
|
|
957
|
-
deps: sub.deps,
|
|
958
|
-
space: typeof parent.fields.space === "string" ? parent.fields.space : void 0
|
|
959
|
-
});
|
|
960
|
-
created.push(inst);
|
|
961
|
-
}
|
|
962
|
-
const childRefs = created.map((c) => `[[${c.path}]]`);
|
|
963
|
-
const decomposeNote = `
|
|
964
|
-
|
|
965
|
-
## Sub-threads
|
|
966
|
-
|
|
967
|
-
${childRefs.map((r) => `- ${r}`).join("\n")}
|
|
968
|
-
`;
|
|
969
|
-
update(workspacePath, parentPath, {}, parent.body + decomposeNote, actor);
|
|
970
|
-
append(workspacePath, actor, "decompose", parentPath, "thread", {
|
|
971
|
-
children: created.map((c) => c.path)
|
|
972
|
-
});
|
|
973
|
-
return created;
|
|
974
|
-
}
|
|
975
|
-
function assertTransition(from, to) {
|
|
976
|
-
const allowed = THREAD_STATUS_TRANSITIONS[from];
|
|
977
|
-
if (!allowed?.includes(to)) {
|
|
978
|
-
throw new Error(`Invalid transition: "${from}" \u2192 "${to}". Allowed: ${allowed?.join(", ") ?? "none"}`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
function assertOwner(workspacePath, threadPath, actor) {
|
|
982
|
-
const owner = currentOwner(workspacePath, threadPath);
|
|
983
|
-
if (owner && owner !== actor) {
|
|
984
|
-
throw new Error(`Thread is owned by "${owner}", not "${actor}". Only the owner can perform this action.`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
function compareThreadPriority(a, b) {
|
|
988
|
-
const rank = (value) => {
|
|
989
|
-
const normalized = String(value ?? "medium").toLowerCase();
|
|
990
|
-
switch (normalized) {
|
|
991
|
-
case "urgent":
|
|
992
|
-
return 0;
|
|
993
|
-
case "high":
|
|
994
|
-
return 1;
|
|
995
|
-
case "medium":
|
|
996
|
-
return 2;
|
|
997
|
-
case "low":
|
|
998
|
-
return 3;
|
|
999
|
-
default:
|
|
1000
|
-
return 4;
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
1003
|
-
const byPriority = rank(a.fields.priority) - rank(b.fields.priority);
|
|
1004
|
-
if (byPriority !== 0) return byPriority;
|
|
1005
|
-
const createdA = Date.parse(String(a.fields.created ?? ""));
|
|
1006
|
-
const createdB = Date.parse(String(b.fields.created ?? ""));
|
|
1007
|
-
const safeA = Number.isNaN(createdA) ? Number.MAX_SAFE_INTEGER : createdA;
|
|
1008
|
-
const safeB = Number.isNaN(createdB) ? Number.MAX_SAFE_INTEGER : createdB;
|
|
1009
|
-
return safeA - safeB;
|
|
1010
|
-
}
|
|
1011
|
-
function normalizeThreadRef(value) {
|
|
1012
|
-
const raw = String(value ?? "").trim();
|
|
1013
|
-
if (!raw) return raw;
|
|
1014
|
-
const unwrapped = raw.startsWith("[[") && raw.endsWith("]]") ? raw.slice(2, -2) : raw;
|
|
1015
|
-
if (unwrapped.startsWith("external/")) return unwrapped;
|
|
1016
|
-
if (unwrapped.endsWith(".md")) return unwrapped;
|
|
1017
|
-
return `${unwrapped}.md`;
|
|
1018
|
-
}
|
|
1019
|
-
function normalizeWorkspaceRef(value) {
|
|
1020
|
-
const raw = String(value ?? "").trim();
|
|
1021
|
-
if (!raw) return "";
|
|
1022
|
-
const unwrapped = raw.startsWith("[[") && raw.endsWith("]]") ? raw.slice(2, -2) : raw;
|
|
1023
|
-
return unwrapped.endsWith(".md") ? unwrapped : `${unwrapped}.md`;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// src/workspace.ts
|
|
1027
|
-
var workspace_exports = {};
|
|
1028
|
-
__export(workspace_exports, {
|
|
1029
|
-
initWorkspace: () => initWorkspace,
|
|
1030
|
-
isWorkgraphWorkspace: () => isWorkgraphWorkspace,
|
|
1031
|
-
workspaceConfigPath: () => workspaceConfigPath
|
|
1032
|
-
});
|
|
1033
|
-
import fs5 from "fs";
|
|
1034
|
-
import path5 from "path";
|
|
1035
|
-
|
|
1036
|
-
// src/bases.ts
|
|
1037
|
-
var bases_exports = {};
|
|
1038
|
-
__export(bases_exports, {
|
|
1039
|
-
generateBasesFromPrimitiveRegistry: () => generateBasesFromPrimitiveRegistry,
|
|
1040
|
-
primitiveRegistryManifestPath: () => primitiveRegistryManifestPath,
|
|
1041
|
-
readPrimitiveRegistryManifest: () => readPrimitiveRegistryManifest,
|
|
1042
|
-
syncPrimitiveRegistryManifest: () => syncPrimitiveRegistryManifest
|
|
1043
|
-
});
|
|
1044
|
-
import fs4 from "fs";
|
|
1045
|
-
import path4 from "path";
|
|
1046
|
-
import YAML from "yaml";
|
|
1047
|
-
var REGISTRY_MANIFEST_FILE = ".clawvault/primitive-registry.yaml";
|
|
1048
|
-
var DEFAULT_BASES_DIR = ".clawvault/bases";
|
|
1049
|
-
function primitiveRegistryManifestPath(workspacePath) {
|
|
1050
|
-
return path4.join(workspacePath, REGISTRY_MANIFEST_FILE);
|
|
1051
|
-
}
|
|
1052
|
-
function readPrimitiveRegistryManifest(workspacePath) {
|
|
1053
|
-
const manifestPath = primitiveRegistryManifestPath(workspacePath);
|
|
1054
|
-
if (!fs4.existsSync(manifestPath)) {
|
|
1055
|
-
throw new Error(`Primitive registry manifest not found: ${manifestPath}`);
|
|
1056
|
-
}
|
|
1057
|
-
const raw = fs4.readFileSync(manifestPath, "utf-8");
|
|
1058
|
-
return YAML.parse(raw);
|
|
1059
|
-
}
|
|
1060
|
-
function syncPrimitiveRegistryManifest(workspacePath) {
|
|
1061
|
-
const registry = loadRegistry(workspacePath);
|
|
1062
|
-
const manifest = {
|
|
1063
|
-
version: 1,
|
|
1064
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1065
|
-
primitives: Object.values(registry.types).map((primitive) => ({
|
|
1066
|
-
name: primitive.name,
|
|
1067
|
-
directory: primitive.directory,
|
|
1068
|
-
canonical: primitive.builtIn,
|
|
1069
|
-
builtIn: primitive.builtIn,
|
|
1070
|
-
fields: Object.entries(primitive.fields).map(([name, field]) => ({
|
|
1071
|
-
name,
|
|
1072
|
-
type: field.type,
|
|
1073
|
-
...field.required ? { required: true } : {},
|
|
1074
|
-
...field.description ? { description: field.description } : {}
|
|
1075
|
-
}))
|
|
1076
|
-
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
1077
|
-
};
|
|
1078
|
-
const manifestPath = primitiveRegistryManifestPath(workspacePath);
|
|
1079
|
-
ensureDirectory(path4.dirname(manifestPath));
|
|
1080
|
-
fs4.writeFileSync(manifestPath, YAML.stringify(manifest), "utf-8");
|
|
1081
|
-
return manifest;
|
|
1082
|
-
}
|
|
1083
|
-
function generateBasesFromPrimitiveRegistry(workspacePath, options = {}) {
|
|
1084
|
-
const manifest = readPrimitiveRegistryManifest(workspacePath);
|
|
1085
|
-
const includeNonCanonical = options.includeNonCanonical === true;
|
|
1086
|
-
const outputDirectory = path4.join(workspacePath, options.outputDirectory ?? DEFAULT_BASES_DIR);
|
|
1087
|
-
ensureDirectory(outputDirectory);
|
|
1088
|
-
const generated = [];
|
|
1089
|
-
const primitives = manifest.primitives.filter(
|
|
1090
|
-
(primitive) => includeNonCanonical ? true : primitive.canonical
|
|
1091
|
-
);
|
|
1092
|
-
for (const primitive of primitives) {
|
|
1093
|
-
const relBasePath = `${primitive.name}.base`;
|
|
1094
|
-
const absBasePath = path4.join(outputDirectory, relBasePath);
|
|
1095
|
-
const content = renderBaseFile(primitive);
|
|
1096
|
-
fs4.writeFileSync(absBasePath, content, "utf-8");
|
|
1097
|
-
generated.push(path4.relative(workspacePath, absBasePath).replace(/\\/g, "/"));
|
|
1098
|
-
}
|
|
1099
|
-
return {
|
|
1100
|
-
outputDirectory: path4.relative(workspacePath, outputDirectory).replace(/\\/g, "/"),
|
|
1101
|
-
generated: generated.sort()
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
function renderBaseFile(primitive) {
|
|
1105
|
-
const columnFields = primitive.fields.map((field) => field.name).filter((name, idx, arr) => arr.indexOf(name) === idx);
|
|
1106
|
-
const baseDoc = {
|
|
1107
|
-
id: primitive.name,
|
|
1108
|
-
title: `${titleCase(primitive.name)} Base`,
|
|
1109
|
-
source: {
|
|
1110
|
-
type: "folder",
|
|
1111
|
-
path: primitive.directory,
|
|
1112
|
-
extension: "md"
|
|
1113
|
-
},
|
|
1114
|
-
views: [
|
|
1115
|
-
{
|
|
1116
|
-
id: "table",
|
|
1117
|
-
type: "table",
|
|
1118
|
-
name: "All",
|
|
1119
|
-
columns: ["file.name", ...columnFields]
|
|
1120
|
-
}
|
|
1121
|
-
]
|
|
1122
|
-
};
|
|
1123
|
-
return YAML.stringify(baseDoc);
|
|
1124
|
-
}
|
|
1125
|
-
function ensureDirectory(dirPath) {
|
|
1126
|
-
if (!fs4.existsSync(dirPath)) fs4.mkdirSync(dirPath, { recursive: true });
|
|
1127
|
-
}
|
|
1128
|
-
function titleCase(value) {
|
|
1129
|
-
return value.split(/[-_]/g).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// src/workspace.ts
|
|
1133
|
-
var WORKGRAPH_CONFIG_FILE = ".workgraph.json";
|
|
1134
|
-
function workspaceConfigPath(workspacePath) {
|
|
1135
|
-
return path5.join(workspacePath, WORKGRAPH_CONFIG_FILE);
|
|
1136
|
-
}
|
|
1137
|
-
function isWorkgraphWorkspace(workspacePath) {
|
|
1138
|
-
return fs5.existsSync(workspaceConfigPath(workspacePath));
|
|
1139
|
-
}
|
|
1140
|
-
function initWorkspace(targetPath, options = {}) {
|
|
1141
|
-
const resolvedPath = path5.resolve(targetPath);
|
|
1142
|
-
const configPath = workspaceConfigPath(resolvedPath);
|
|
1143
|
-
if (fs5.existsSync(configPath)) {
|
|
1144
|
-
throw new Error(`Workgraph workspace already initialized at ${resolvedPath}`);
|
|
1145
|
-
}
|
|
1146
|
-
const createdDirectories = [];
|
|
1147
|
-
ensureDir(resolvedPath, createdDirectories);
|
|
1148
|
-
ensureDir(path5.join(resolvedPath, ".clawvault"), createdDirectories);
|
|
1149
|
-
const registry = loadRegistry(resolvedPath);
|
|
1150
|
-
saveRegistry(resolvedPath, registry);
|
|
1151
|
-
syncPrimitiveRegistryManifest(resolvedPath);
|
|
1152
|
-
if (options.createTypeDirs !== false) {
|
|
1153
|
-
const types = listTypes(resolvedPath);
|
|
1154
|
-
for (const typeDef of types) {
|
|
1155
|
-
ensureDir(path5.join(resolvedPath, typeDef.directory), createdDirectories);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1159
|
-
const config = {
|
|
1160
|
-
name: options.name ?? path5.basename(resolvedPath),
|
|
1161
|
-
version: "1.0.0",
|
|
1162
|
-
mode: "workgraph",
|
|
1163
|
-
createdAt: now,
|
|
1164
|
-
updatedAt: now
|
|
1165
|
-
};
|
|
1166
|
-
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1167
|
-
if (options.createReadme !== false) {
|
|
1168
|
-
writeReadmeIfMissing(resolvedPath, config.name);
|
|
1169
|
-
}
|
|
1170
|
-
const bases = options.createBases === false ? { generated: [] } : generateBasesFromPrimitiveRegistry(resolvedPath);
|
|
1171
|
-
return {
|
|
1172
|
-
workspacePath: resolvedPath,
|
|
1173
|
-
configPath,
|
|
1174
|
-
config,
|
|
1175
|
-
createdDirectories,
|
|
1176
|
-
seededTypes: listTypes(resolvedPath).map((t) => t.name),
|
|
1177
|
-
generatedBases: bases.generated,
|
|
1178
|
-
primitiveRegistryManifestPath: ".clawvault/primitive-registry.yaml"
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
function ensureDir(dirPath, createdDirectories) {
|
|
1182
|
-
if (fs5.existsSync(dirPath)) return;
|
|
1183
|
-
fs5.mkdirSync(dirPath, { recursive: true });
|
|
1184
|
-
createdDirectories.push(dirPath);
|
|
1185
|
-
}
|
|
1186
|
-
function writeReadmeIfMissing(workspacePath, name) {
|
|
1187
|
-
const readmePath = path5.join(workspacePath, "README.md");
|
|
1188
|
-
if (fs5.existsSync(readmePath)) return;
|
|
1189
|
-
const content = `# ${name}
|
|
1190
|
-
|
|
1191
|
-
Agent-first workgraph workspace for multi-agent coordination.
|
|
1192
|
-
|
|
1193
|
-
## Quickstart
|
|
1194
|
-
|
|
1195
|
-
\`\`\`bash
|
|
1196
|
-
workgraph thread list --json
|
|
1197
|
-
workgraph thread next --claim --actor agent-a --json
|
|
1198
|
-
workgraph ledger show --count 20 --json
|
|
1199
|
-
\`\`\`
|
|
1200
|
-
`;
|
|
1201
|
-
fs5.writeFileSync(readmePath, content, "utf-8");
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// src/command-center.ts
|
|
1205
|
-
var command_center_exports = {};
|
|
1206
|
-
__export(command_center_exports, {
|
|
1207
|
-
generateCommandCenter: () => generateCommandCenter
|
|
1208
|
-
});
|
|
1209
|
-
import fs6 from "fs";
|
|
1210
|
-
import path6 from "path";
|
|
1211
|
-
function generateCommandCenter(workspacePath, options = {}) {
|
|
1212
|
-
const actor = options.actor ?? "system";
|
|
1213
|
-
const recentCount = options.recentCount ?? 15;
|
|
1214
|
-
const relOutputPath = options.outputPath ?? "Command Center.md";
|
|
1215
|
-
const absOutputPath = resolvePathWithinWorkspace(workspacePath, relOutputPath);
|
|
1216
|
-
const normalizedOutputPath = path6.relative(workspacePath, absOutputPath).replace(/\\/g, "/");
|
|
1217
|
-
const allThreads = list(workspacePath, "thread");
|
|
1218
|
-
const openThreads2 = allThreads.filter((thread) => thread.fields.status === "open");
|
|
1219
|
-
const activeThreads2 = allThreads.filter((thread) => thread.fields.status === "active");
|
|
1220
|
-
const blockedThreads2 = allThreads.filter((thread) => thread.fields.status === "blocked");
|
|
1221
|
-
const doneThreads = allThreads.filter((thread) => thread.fields.status === "done");
|
|
1222
|
-
const claims = allClaims(workspacePath);
|
|
1223
|
-
const recentEvents = recent(workspacePath, recentCount);
|
|
1224
|
-
const content = renderCommandCenter({
|
|
1225
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1226
|
-
openThreads: openThreads2,
|
|
1227
|
-
activeThreads: activeThreads2,
|
|
1228
|
-
blockedThreads: blockedThreads2,
|
|
1229
|
-
doneThreads,
|
|
1230
|
-
claims: [...claims.entries()].map(([target, owner]) => ({ target, owner })),
|
|
1231
|
-
recentEvents
|
|
1232
|
-
});
|
|
1233
|
-
const parentDir = path6.dirname(absOutputPath);
|
|
1234
|
-
if (!fs6.existsSync(parentDir)) fs6.mkdirSync(parentDir, { recursive: true });
|
|
1235
|
-
const existed = fs6.existsSync(absOutputPath);
|
|
1236
|
-
fs6.writeFileSync(absOutputPath, content, "utf-8");
|
|
1237
|
-
append(
|
|
1238
|
-
workspacePath,
|
|
1239
|
-
actor,
|
|
1240
|
-
existed ? "update" : "create",
|
|
1241
|
-
normalizedOutputPath,
|
|
1242
|
-
"command-center",
|
|
1243
|
-
{
|
|
1244
|
-
generated: true,
|
|
1245
|
-
open_threads: openThreads2.length,
|
|
1246
|
-
active_claims: claims.size,
|
|
1247
|
-
recent_events: recentEvents.length
|
|
1248
|
-
}
|
|
1249
|
-
);
|
|
1250
|
-
return {
|
|
1251
|
-
outputPath: normalizedOutputPath,
|
|
1252
|
-
stats: {
|
|
1253
|
-
totalThreads: allThreads.length,
|
|
1254
|
-
openThreads: openThreads2.length,
|
|
1255
|
-
activeThreads: activeThreads2.length,
|
|
1256
|
-
blockedThreads: blockedThreads2.length,
|
|
1257
|
-
doneThreads: doneThreads.length,
|
|
1258
|
-
activeClaims: claims.size,
|
|
1259
|
-
recentEvents: recentEvents.length
|
|
1260
|
-
},
|
|
1261
|
-
content
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
function resolvePathWithinWorkspace(workspacePath, outputPath) {
|
|
1265
|
-
const base = path6.resolve(workspacePath);
|
|
1266
|
-
const resolved = path6.resolve(base, outputPath);
|
|
1267
|
-
if (!resolved.startsWith(base + path6.sep) && resolved !== base) {
|
|
1268
|
-
throw new Error(`Invalid command-center output path: ${outputPath}`);
|
|
1269
|
-
}
|
|
1270
|
-
return resolved;
|
|
1271
|
-
}
|
|
1272
|
-
function renderCommandCenter(input) {
|
|
1273
|
-
const header = [
|
|
1274
|
-
"# Workgraph Command Center",
|
|
1275
|
-
"",
|
|
1276
|
-
`Generated: ${input.generatedAt}`,
|
|
1277
|
-
""
|
|
1278
|
-
];
|
|
1279
|
-
const statusBlock = [
|
|
1280
|
-
"## Thread Status",
|
|
1281
|
-
"",
|
|
1282
|
-
`- Open: ${input.openThreads.length}`,
|
|
1283
|
-
`- Active: ${input.activeThreads.length}`,
|
|
1284
|
-
`- Blocked: ${input.blockedThreads.length}`,
|
|
1285
|
-
`- Done: ${input.doneThreads.length}`,
|
|
1286
|
-
""
|
|
1287
|
-
];
|
|
1288
|
-
const openTable = [
|
|
1289
|
-
"## Open Threads",
|
|
1290
|
-
"",
|
|
1291
|
-
"| Priority | Title | Path |",
|
|
1292
|
-
"|---|---|---|",
|
|
1293
|
-
...input.openThreads.length > 0 ? input.openThreads.map((thread) => `| ${String(thread.fields.priority ?? "medium")} | ${String(thread.fields.title ?? "Untitled")} | \`${thread.path}\` |`) : ["| - | None | - |"],
|
|
1294
|
-
""
|
|
1295
|
-
];
|
|
1296
|
-
const claimsSection = [
|
|
1297
|
-
"## Active Claims",
|
|
1298
|
-
"",
|
|
1299
|
-
...input.claims.length > 0 ? input.claims.map((claim2) => `- ${claim2.owner} -> \`${claim2.target}\``) : ["- None"],
|
|
1300
|
-
""
|
|
1301
|
-
];
|
|
1302
|
-
const blockedSection = [
|
|
1303
|
-
"## Blocked Threads",
|
|
1304
|
-
"",
|
|
1305
|
-
...input.blockedThreads.length > 0 ? input.blockedThreads.map((thread) => {
|
|
1306
|
-
const deps = Array.isArray(thread.fields.deps) ? thread.fields.deps.join(", ") : "";
|
|
1307
|
-
return `- ${String(thread.fields.title ?? thread.path)} (\`${thread.path}\`)${deps ? ` blocked by: ${deps}` : ""}`;
|
|
1308
|
-
}) : ["- None"],
|
|
1309
|
-
""
|
|
1310
|
-
];
|
|
1311
|
-
const recentSection = [
|
|
1312
|
-
"## Recent Ledger Activity",
|
|
1313
|
-
"",
|
|
1314
|
-
...input.recentEvents.length > 0 ? input.recentEvents.map((event) => `- ${event.ts} ${event.op} ${event.actor} -> \`${event.target}\``) : ["- No activity"],
|
|
1315
|
-
""
|
|
1316
|
-
];
|
|
1317
|
-
return [
|
|
1318
|
-
...header,
|
|
1319
|
-
...statusBlock,
|
|
1320
|
-
...openTable,
|
|
1321
|
-
...claimsSection,
|
|
1322
|
-
...blockedSection,
|
|
1323
|
-
...recentSection
|
|
1324
|
-
].join("\n");
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// src/skill.ts
|
|
1328
|
-
var skill_exports = {};
|
|
1329
|
-
__export(skill_exports, {
|
|
1330
|
-
listSkills: () => listSkills,
|
|
1331
|
-
loadSkill: () => loadSkill,
|
|
1332
|
-
promoteSkill: () => promoteSkill,
|
|
1333
|
-
proposeSkill: () => proposeSkill,
|
|
1334
|
-
writeSkill: () => writeSkill
|
|
1335
|
-
});
|
|
1336
|
-
function writeSkill(workspacePath, title, body, actor, options = {}) {
|
|
1337
|
-
const relPath = pathForSkillTitle(title);
|
|
1338
|
-
const existing = read(workspacePath, relPath);
|
|
1339
|
-
const status = options.status ?? existing?.fields.status ?? "draft";
|
|
1340
|
-
if (!existing) {
|
|
1341
|
-
return create(workspacePath, "skill", {
|
|
1342
|
-
title,
|
|
1343
|
-
owner: options.owner ?? actor,
|
|
1344
|
-
version: options.version ?? "0.1.0",
|
|
1345
|
-
status,
|
|
1346
|
-
distribution: options.distribution ?? "tailscale-shared-vault",
|
|
1347
|
-
tailscale_path: options.tailscalePath,
|
|
1348
|
-
reviewers: options.reviewers ?? [],
|
|
1349
|
-
tags: options.tags ?? []
|
|
1350
|
-
}, body, actor);
|
|
1351
|
-
}
|
|
1352
|
-
return update(workspacePath, existing.path, {
|
|
1353
|
-
title,
|
|
1354
|
-
owner: options.owner ?? existing.fields.owner ?? actor,
|
|
1355
|
-
version: options.version ?? existing.fields.version ?? "0.1.0",
|
|
1356
|
-
status,
|
|
1357
|
-
distribution: options.distribution ?? existing.fields.distribution ?? "tailscale-shared-vault",
|
|
1358
|
-
tailscale_path: options.tailscalePath ?? existing.fields.tailscale_path,
|
|
1359
|
-
reviewers: options.reviewers ?? existing.fields.reviewers ?? [],
|
|
1360
|
-
tags: options.tags ?? existing.fields.tags ?? []
|
|
1361
|
-
}, body, actor);
|
|
1362
|
-
}
|
|
1363
|
-
function loadSkill(workspacePath, skillRef) {
|
|
1364
|
-
const normalized = normalizeSkillRef(skillRef);
|
|
1365
|
-
const skill = read(workspacePath, normalized);
|
|
1366
|
-
if (!skill) throw new Error(`Skill not found: ${skillRef}`);
|
|
1367
|
-
if (skill.type !== "skill") throw new Error(`Target is not a skill primitive: ${skillRef}`);
|
|
1368
|
-
return skill;
|
|
1369
|
-
}
|
|
1370
|
-
function listSkills(workspacePath, options = {}) {
|
|
1371
|
-
let skills = list(workspacePath, "skill");
|
|
1372
|
-
if (options.status) {
|
|
1373
|
-
skills = skills.filter((skill) => skill.fields.status === options.status);
|
|
1374
|
-
}
|
|
1375
|
-
return skills;
|
|
1376
|
-
}
|
|
1377
|
-
function proposeSkill(workspacePath, skillRef, actor, options = {}) {
|
|
1378
|
-
const skill = loadSkill(workspacePath, skillRef);
|
|
1379
|
-
let proposalThread = options.proposalThread;
|
|
1380
|
-
if (!proposalThread && options.createThreadIfMissing !== false) {
|
|
1381
|
-
const createdThread = createThread(
|
|
1382
|
-
workspacePath,
|
|
1383
|
-
`Review skill: ${String(skill.fields.title)}`,
|
|
1384
|
-
`Review and approve skill ${skill.path} for activation.`,
|
|
1385
|
-
actor,
|
|
1386
|
-
{
|
|
1387
|
-
priority: "medium",
|
|
1388
|
-
space: options.space,
|
|
1389
|
-
context_refs: [skill.path]
|
|
1390
|
-
}
|
|
1391
|
-
);
|
|
1392
|
-
proposalThread = createdThread.path;
|
|
1393
|
-
}
|
|
1394
|
-
return update(workspacePath, skill.path, {
|
|
1395
|
-
status: "proposed",
|
|
1396
|
-
proposal_thread: proposalThread ?? skill.fields.proposal_thread,
|
|
1397
|
-
proposed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1398
|
-
reviewers: options.reviewers ?? skill.fields.reviewers ?? []
|
|
1399
|
-
}, void 0, actor);
|
|
1400
|
-
}
|
|
1401
|
-
function promoteSkill(workspacePath, skillRef, actor, options = {}) {
|
|
1402
|
-
const skill = loadSkill(workspacePath, skillRef);
|
|
1403
|
-
const currentVersion = String(skill.fields.version ?? "0.1.0");
|
|
1404
|
-
const nextVersion = options.version ?? bumpPatchVersion(currentVersion);
|
|
1405
|
-
return update(workspacePath, skill.path, {
|
|
1406
|
-
status: "active",
|
|
1407
|
-
version: nextVersion,
|
|
1408
|
-
promoted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1409
|
-
}, void 0, actor);
|
|
1410
|
-
}
|
|
1411
|
-
function pathForSkillTitle(title) {
|
|
1412
|
-
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
1413
|
-
return `skills/${slug}.md`;
|
|
1414
|
-
}
|
|
1415
|
-
function normalizeSkillRef(skillRef) {
|
|
1416
|
-
const raw = skillRef.trim();
|
|
1417
|
-
if (!raw) return raw;
|
|
1418
|
-
if (raw.includes("/")) {
|
|
1419
|
-
return raw.endsWith(".md") ? raw : `${raw}.md`;
|
|
1420
|
-
}
|
|
1421
|
-
const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
1422
|
-
return `skills/${slug}.md`;
|
|
1423
|
-
}
|
|
1424
|
-
function bumpPatchVersion(version) {
|
|
1425
|
-
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
1426
|
-
if (!match) return "0.1.0";
|
|
1427
|
-
const major = Number.parseInt(match[1], 10);
|
|
1428
|
-
const minor = Number.parseInt(match[2], 10);
|
|
1429
|
-
const patch = Number.parseInt(match[3], 10) + 1;
|
|
1430
|
-
return `${major}.${minor}.${patch}`;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
export {
|
|
1434
|
-
THREAD_STATUS_TRANSITIONS,
|
|
1435
|
-
ledger_exports,
|
|
1436
|
-
registry_exports,
|
|
1437
|
-
store_exports,
|
|
1438
|
-
thread_exports,
|
|
1439
|
-
bases_exports,
|
|
1440
|
-
workspace_exports,
|
|
1441
|
-
command_center_exports,
|
|
1442
|
-
skill_exports
|
|
1443
|
-
};
|