@versatly/workgraph 0.3.0 → 1.1.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.
@@ -0,0 +1,3107 @@
1
+ import {
2
+ __export,
3
+ allClaims,
4
+ append,
5
+ checkpoint,
6
+ create,
7
+ createRun,
8
+ createThread,
9
+ historyOf,
10
+ keywordSearch,
11
+ list,
12
+ listReadyThreads,
13
+ listRuns,
14
+ listTypes,
15
+ loadPolicyRegistry,
16
+ loadRegistry,
17
+ queryPrimitives,
18
+ read,
19
+ readAll,
20
+ recent,
21
+ refreshWikiLinkGraphIndex,
22
+ release,
23
+ saveRegistry,
24
+ stop,
25
+ update
26
+ } from "./chunk-OTSECVE5.js";
27
+
28
+ // src/workspace.ts
29
+ var workspace_exports = {};
30
+ __export(workspace_exports, {
31
+ initWorkspace: () => initWorkspace,
32
+ isWorkgraphWorkspace: () => isWorkgraphWorkspace,
33
+ workspaceConfigPath: () => workspaceConfigPath
34
+ });
35
+ import fs2 from "fs";
36
+ import path2 from "path";
37
+
38
+ // src/bases.ts
39
+ var bases_exports = {};
40
+ __export(bases_exports, {
41
+ generateBasesFromPrimitiveRegistry: () => generateBasesFromPrimitiveRegistry,
42
+ primitiveRegistryManifestPath: () => primitiveRegistryManifestPath,
43
+ readPrimitiveRegistryManifest: () => readPrimitiveRegistryManifest,
44
+ syncPrimitiveRegistryManifest: () => syncPrimitiveRegistryManifest
45
+ });
46
+ import fs from "fs";
47
+ import path from "path";
48
+ import YAML from "yaml";
49
+ var REGISTRY_MANIFEST_FILE = ".workgraph/primitive-registry.yaml";
50
+ var DEFAULT_BASES_DIR = ".workgraph/bases";
51
+ function primitiveRegistryManifestPath(workspacePath) {
52
+ return path.join(workspacePath, REGISTRY_MANIFEST_FILE);
53
+ }
54
+ function readPrimitiveRegistryManifest(workspacePath) {
55
+ const manifestPath = primitiveRegistryManifestPath(workspacePath);
56
+ if (!fs.existsSync(manifestPath)) {
57
+ throw new Error(`Primitive registry manifest not found: ${manifestPath}`);
58
+ }
59
+ const raw = fs.readFileSync(manifestPath, "utf-8");
60
+ return YAML.parse(raw);
61
+ }
62
+ function syncPrimitiveRegistryManifest(workspacePath) {
63
+ const registry = loadRegistry(workspacePath);
64
+ const manifest = {
65
+ version: 1,
66
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
67
+ primitives: Object.values(registry.types).map((primitive) => ({
68
+ name: primitive.name,
69
+ directory: primitive.directory,
70
+ canonical: primitive.builtIn,
71
+ builtIn: primitive.builtIn,
72
+ fields: Object.entries(primitive.fields).map(([name, field]) => ({
73
+ name,
74
+ type: field.type,
75
+ ...field.required ? { required: true } : {},
76
+ ...field.description ? { description: field.description } : {}
77
+ }))
78
+ })).sort((a, b) => a.name.localeCompare(b.name))
79
+ };
80
+ const manifestPath = primitiveRegistryManifestPath(workspacePath);
81
+ ensureDirectory(path.dirname(manifestPath));
82
+ fs.writeFileSync(manifestPath, YAML.stringify(manifest), "utf-8");
83
+ return manifest;
84
+ }
85
+ function generateBasesFromPrimitiveRegistry(workspacePath, options = {}) {
86
+ const manifest = readPrimitiveRegistryManifest(workspacePath);
87
+ const includeNonCanonical = options.includeNonCanonical === true;
88
+ const outputDirectory = path.join(workspacePath, options.outputDirectory ?? DEFAULT_BASES_DIR);
89
+ ensureDirectory(outputDirectory);
90
+ const generated = [];
91
+ const primitives = manifest.primitives.filter(
92
+ (primitive) => includeNonCanonical ? true : primitive.canonical
93
+ );
94
+ for (const primitive of primitives) {
95
+ const relBasePath = `${primitive.name}.base`;
96
+ const absBasePath = path.join(outputDirectory, relBasePath);
97
+ const content = renderBaseFile(primitive);
98
+ fs.writeFileSync(absBasePath, content, "utf-8");
99
+ generated.push(path.relative(workspacePath, absBasePath).replace(/\\/g, "/"));
100
+ }
101
+ return {
102
+ outputDirectory: path.relative(workspacePath, outputDirectory).replace(/\\/g, "/"),
103
+ generated: generated.sort()
104
+ };
105
+ }
106
+ function renderBaseFile(primitive) {
107
+ const columnFields = primitive.fields.map((field) => field.name).filter((name, idx, arr) => arr.indexOf(name) === idx);
108
+ const baseDoc = {
109
+ id: primitive.name,
110
+ title: `${titleCase(primitive.name)} Base`,
111
+ source: {
112
+ type: "folder",
113
+ path: primitive.directory,
114
+ extension: "md"
115
+ },
116
+ views: [
117
+ {
118
+ id: "table",
119
+ type: "table",
120
+ name: "All",
121
+ columns: ["file.name", ...columnFields]
122
+ }
123
+ ]
124
+ };
125
+ return YAML.stringify(baseDoc);
126
+ }
127
+ function ensureDirectory(dirPath) {
128
+ if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
129
+ }
130
+ function titleCase(value) {
131
+ return value.split(/[-_]/g).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
132
+ }
133
+
134
+ // src/workspace.ts
135
+ var WORKGRAPH_CONFIG_FILE = ".workgraph.json";
136
+ function workspaceConfigPath(workspacePath) {
137
+ return path2.join(workspacePath, WORKGRAPH_CONFIG_FILE);
138
+ }
139
+ function isWorkgraphWorkspace(workspacePath) {
140
+ return fs2.existsSync(workspaceConfigPath(workspacePath));
141
+ }
142
+ function initWorkspace(targetPath, options = {}) {
143
+ const resolvedPath = path2.resolve(targetPath);
144
+ const configPath = workspaceConfigPath(resolvedPath);
145
+ if (fs2.existsSync(configPath)) {
146
+ throw new Error(`Workgraph workspace already initialized at ${resolvedPath}`);
147
+ }
148
+ const createdDirectories = [];
149
+ ensureDir(resolvedPath, createdDirectories);
150
+ ensureDir(path2.join(resolvedPath, ".workgraph"), createdDirectories);
151
+ const registry = loadRegistry(resolvedPath);
152
+ saveRegistry(resolvedPath, registry);
153
+ syncPrimitiveRegistryManifest(resolvedPath);
154
+ if (options.createTypeDirs !== false) {
155
+ const types = listTypes(resolvedPath);
156
+ for (const typeDef of types) {
157
+ ensureDir(path2.join(resolvedPath, typeDef.directory), createdDirectories);
158
+ }
159
+ }
160
+ const now = (/* @__PURE__ */ new Date()).toISOString();
161
+ const config = {
162
+ name: options.name ?? path2.basename(resolvedPath),
163
+ version: "1.0.0",
164
+ mode: "workgraph",
165
+ createdAt: now,
166
+ updatedAt: now
167
+ };
168
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
169
+ if (options.createReadme !== false) {
170
+ writeReadmeIfMissing(resolvedPath, config.name);
171
+ }
172
+ const bases = options.createBases === false ? { generated: [] } : generateBasesFromPrimitiveRegistry(resolvedPath);
173
+ loadPolicyRegistry(resolvedPath);
174
+ refreshWikiLinkGraphIndex(resolvedPath);
175
+ return {
176
+ workspacePath: resolvedPath,
177
+ configPath,
178
+ config,
179
+ createdDirectories,
180
+ seededTypes: listTypes(resolvedPath).map((t) => t.name),
181
+ generatedBases: bases.generated,
182
+ primitiveRegistryManifestPath: ".workgraph/primitive-registry.yaml"
183
+ };
184
+ }
185
+ function ensureDir(dirPath, createdDirectories) {
186
+ if (fs2.existsSync(dirPath)) return;
187
+ fs2.mkdirSync(dirPath, { recursive: true });
188
+ createdDirectories.push(dirPath);
189
+ }
190
+ function writeReadmeIfMissing(workspacePath, name) {
191
+ const readmePath = path2.join(workspacePath, "README.md");
192
+ if (fs2.existsSync(readmePath)) return;
193
+ const content = `# ${name}
194
+
195
+ Agent-first workgraph workspace for multi-agent coordination.
196
+
197
+ ## Quickstart
198
+
199
+ \`\`\`bash
200
+ workgraph thread list --json
201
+ workgraph thread next --claim --actor agent-a --json
202
+ workgraph ledger show --count 20 --json
203
+ \`\`\`
204
+ `;
205
+ fs2.writeFileSync(readmePath, content, "utf-8");
206
+ }
207
+
208
+ // src/command-center.ts
209
+ var command_center_exports = {};
210
+ __export(command_center_exports, {
211
+ generateCommandCenter: () => generateCommandCenter
212
+ });
213
+ import fs3 from "fs";
214
+ import path3 from "path";
215
+ function generateCommandCenter(workspacePath, options = {}) {
216
+ const actor = options.actor ?? "system";
217
+ const recentCount = options.recentCount ?? 15;
218
+ const relOutputPath = options.outputPath ?? "Command Center.md";
219
+ const absOutputPath = resolvePathWithinWorkspace(workspacePath, relOutputPath);
220
+ const normalizedOutputPath = path3.relative(workspacePath, absOutputPath).replace(/\\/g, "/");
221
+ const allThreads = list(workspacePath, "thread");
222
+ const openThreads = allThreads.filter((thread) => thread.fields.status === "open");
223
+ const activeThreads = allThreads.filter((thread) => thread.fields.status === "active");
224
+ const blockedThreads = allThreads.filter((thread) => thread.fields.status === "blocked");
225
+ const doneThreads = allThreads.filter((thread) => thread.fields.status === "done");
226
+ const claims = allClaims(workspacePath);
227
+ const recentEvents = recent(workspacePath, recentCount);
228
+ const content = renderCommandCenter({
229
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
230
+ openThreads,
231
+ activeThreads,
232
+ blockedThreads,
233
+ doneThreads,
234
+ claims: [...claims.entries()].map(([target, owner]) => ({ target, owner })),
235
+ recentEvents
236
+ });
237
+ const parentDir = path3.dirname(absOutputPath);
238
+ if (!fs3.existsSync(parentDir)) fs3.mkdirSync(parentDir, { recursive: true });
239
+ const existed = fs3.existsSync(absOutputPath);
240
+ fs3.writeFileSync(absOutputPath, content, "utf-8");
241
+ append(
242
+ workspacePath,
243
+ actor,
244
+ existed ? "update" : "create",
245
+ normalizedOutputPath,
246
+ "command-center",
247
+ {
248
+ generated: true,
249
+ open_threads: openThreads.length,
250
+ active_claims: claims.size,
251
+ recent_events: recentEvents.length
252
+ }
253
+ );
254
+ return {
255
+ outputPath: normalizedOutputPath,
256
+ stats: {
257
+ totalThreads: allThreads.length,
258
+ openThreads: openThreads.length,
259
+ activeThreads: activeThreads.length,
260
+ blockedThreads: blockedThreads.length,
261
+ doneThreads: doneThreads.length,
262
+ activeClaims: claims.size,
263
+ recentEvents: recentEvents.length
264
+ },
265
+ content
266
+ };
267
+ }
268
+ function resolvePathWithinWorkspace(workspacePath, outputPath) {
269
+ const base = path3.resolve(workspacePath);
270
+ const resolved = path3.resolve(base, outputPath);
271
+ if (!resolved.startsWith(base + path3.sep) && resolved !== base) {
272
+ throw new Error(`Invalid command-center output path: ${outputPath}`);
273
+ }
274
+ return resolved;
275
+ }
276
+ function renderCommandCenter(input) {
277
+ const header = [
278
+ "# Workgraph Command Center",
279
+ "",
280
+ `Generated: ${input.generatedAt}`,
281
+ ""
282
+ ];
283
+ const statusBlock = [
284
+ "## Thread Status",
285
+ "",
286
+ `- Open: ${input.openThreads.length}`,
287
+ `- Active: ${input.activeThreads.length}`,
288
+ `- Blocked: ${input.blockedThreads.length}`,
289
+ `- Done: ${input.doneThreads.length}`,
290
+ ""
291
+ ];
292
+ const openTable = [
293
+ "## Open Threads",
294
+ "",
295
+ "| Priority | Title | Path |",
296
+ "|---|---|---|",
297
+ ...input.openThreads.length > 0 ? input.openThreads.map((thread) => `| ${String(thread.fields.priority ?? "medium")} | ${String(thread.fields.title ?? "Untitled")} | \`${thread.path}\` |`) : ["| - | None | - |"],
298
+ ""
299
+ ];
300
+ const claimsSection = [
301
+ "## Active Claims",
302
+ "",
303
+ ...input.claims.length > 0 ? input.claims.map((claim) => `- ${claim.owner} -> \`${claim.target}\``) : ["- None"],
304
+ ""
305
+ ];
306
+ const blockedSection = [
307
+ "## Blocked Threads",
308
+ "",
309
+ ...input.blockedThreads.length > 0 ? input.blockedThreads.map((thread) => {
310
+ const deps = Array.isArray(thread.fields.deps) ? thread.fields.deps.join(", ") : "";
311
+ return `- ${String(thread.fields.title ?? thread.path)} (\`${thread.path}\`)${deps ? ` blocked by: ${deps}` : ""}`;
312
+ }) : ["- None"],
313
+ ""
314
+ ];
315
+ const recentSection = [
316
+ "## Recent Ledger Activity",
317
+ "",
318
+ ...input.recentEvents.length > 0 ? input.recentEvents.map((event) => `- ${event.ts} ${event.op} ${event.actor} -> \`${event.target}\``) : ["- No activity"],
319
+ ""
320
+ ];
321
+ return [
322
+ ...header,
323
+ ...statusBlock,
324
+ ...openTable,
325
+ ...claimsSection,
326
+ ...blockedSection,
327
+ ...recentSection
328
+ ].join("\n");
329
+ }
330
+
331
+ // src/skill.ts
332
+ var skill_exports = {};
333
+ __export(skill_exports, {
334
+ listSkills: () => listSkills,
335
+ loadSkill: () => loadSkill,
336
+ promoteSkill: () => promoteSkill,
337
+ proposeSkill: () => proposeSkill,
338
+ skillDiff: () => skillDiff,
339
+ skillHistory: () => skillHistory,
340
+ writeSkill: () => writeSkill
341
+ });
342
+ import fs4 from "fs";
343
+ import path4 from "path";
344
+ function writeSkill(workspacePath, title, body, actor, options = {}) {
345
+ const slug = skillSlug(title);
346
+ const bundleSkillPath = folderSkillPath(slug);
347
+ const legacyPath = legacySkillPath(slug);
348
+ const existing = read(workspacePath, bundleSkillPath) ?? read(workspacePath, legacyPath);
349
+ const status = options.status ?? existing?.fields.status ?? "draft";
350
+ if (existing && options.expectedUpdatedAt) {
351
+ const currentUpdatedAt = String(existing.fields.updated ?? "");
352
+ if (currentUpdatedAt !== options.expectedUpdatedAt) {
353
+ throw new Error(`Concurrent skill update detected for ${existing.path}. Expected updated="${options.expectedUpdatedAt}" but found "${currentUpdatedAt}".`);
354
+ }
355
+ }
356
+ if (!existing) {
357
+ ensureSkillBundleScaffold(workspacePath, slug);
358
+ const created = create(workspacePath, "skill", {
359
+ title,
360
+ owner: options.owner ?? actor,
361
+ version: options.version ?? "0.1.0",
362
+ status,
363
+ distribution: options.distribution ?? "tailscale-shared-vault",
364
+ tailscale_path: options.tailscalePath,
365
+ reviewers: options.reviewers ?? [],
366
+ depends_on: options.dependsOn ?? [],
367
+ tags: options.tags ?? []
368
+ }, body, actor, {
369
+ pathOverride: bundleSkillPath
370
+ });
371
+ writeSkillManifest(workspacePath, slug, created, actor);
372
+ return created;
373
+ }
374
+ const updated = update(workspacePath, existing.path, {
375
+ title,
376
+ owner: options.owner ?? existing.fields.owner ?? actor,
377
+ version: options.version ?? existing.fields.version ?? "0.1.0",
378
+ status,
379
+ distribution: options.distribution ?? existing.fields.distribution ?? "tailscale-shared-vault",
380
+ tailscale_path: options.tailscalePath ?? existing.fields.tailscale_path,
381
+ reviewers: options.reviewers ?? existing.fields.reviewers ?? [],
382
+ depends_on: options.dependsOn ?? existing.fields.depends_on ?? [],
383
+ tags: options.tags ?? existing.fields.tags ?? []
384
+ }, body, actor);
385
+ writeSkillManifest(workspacePath, slug, updated, actor);
386
+ return updated;
387
+ }
388
+ function loadSkill(workspacePath, skillRef) {
389
+ const normalizedCandidates = normalizeSkillRefCandidates(skillRef);
390
+ const skill = normalizedCandidates.map((candidate) => read(workspacePath, candidate)).find((entry) => entry !== null);
391
+ if (!skill) throw new Error(`Skill not found: ${skillRef}`);
392
+ if (skill.type !== "skill") throw new Error(`Target is not a skill primitive: ${skillRef}`);
393
+ return skill;
394
+ }
395
+ function listSkills(workspacePath, options = {}) {
396
+ let skills = list(workspacePath, "skill");
397
+ if (options.status) {
398
+ skills = skills.filter((skill) => skill.fields.status === options.status);
399
+ }
400
+ if (options.updatedSince) {
401
+ const threshold = Date.parse(options.updatedSince);
402
+ if (Number.isFinite(threshold)) {
403
+ skills = skills.filter((skill) => {
404
+ const updatedAt = Date.parse(String(skill.fields.updated ?? ""));
405
+ return Number.isFinite(updatedAt) && updatedAt >= threshold;
406
+ });
407
+ }
408
+ }
409
+ return skills;
410
+ }
411
+ function proposeSkill(workspacePath, skillRef, actor, options = {}) {
412
+ const skill = loadSkill(workspacePath, skillRef);
413
+ const slug = skillSlug(String(skill.fields.title ?? skillRef));
414
+ let proposalThread = options.proposalThread;
415
+ if (!proposalThread && options.createThreadIfMissing !== false) {
416
+ const createdThread = createThread(
417
+ workspacePath,
418
+ `Review skill: ${String(skill.fields.title)}`,
419
+ `Review and approve skill ${skill.path} for activation.`,
420
+ actor,
421
+ {
422
+ priority: "medium",
423
+ space: options.space,
424
+ context_refs: [skill.path]
425
+ }
426
+ );
427
+ proposalThread = createdThread.path;
428
+ }
429
+ const updated = update(workspacePath, skill.path, {
430
+ status: "proposed",
431
+ proposal_thread: proposalThread ?? skill.fields.proposal_thread,
432
+ proposed_at: (/* @__PURE__ */ new Date()).toISOString(),
433
+ reviewers: options.reviewers ?? skill.fields.reviewers ?? []
434
+ }, void 0, actor);
435
+ writeSkillManifest(workspacePath, slug, updated, actor);
436
+ return updated;
437
+ }
438
+ function skillHistory(workspacePath, skillRef, options = {}) {
439
+ const skill = loadSkill(workspacePath, skillRef);
440
+ const entries = historyOf(workspacePath, skill.path);
441
+ if (options.limit && options.limit > 0) {
442
+ return entries.slice(-options.limit);
443
+ }
444
+ return entries;
445
+ }
446
+ function skillDiff(workspacePath, skillRef) {
447
+ const skill = loadSkill(workspacePath, skillRef);
448
+ const entries = historyOf(workspacePath, skill.path).filter((entry) => entry.op === "create" || entry.op === "update");
449
+ const latest = entries.length > 0 ? entries[entries.length - 1] : null;
450
+ const previous = entries.length > 1 ? entries[entries.length - 2] : null;
451
+ const changedFields = Array.isArray(latest?.data?.changed) ? latest.data.changed.map((value) => String(value)) : latest?.op === "create" ? Object.keys(skill.fields) : [];
452
+ return {
453
+ path: skill.path,
454
+ latestEntryTs: latest?.ts ?? null,
455
+ previousEntryTs: previous?.ts ?? null,
456
+ changedFields
457
+ };
458
+ }
459
+ function promoteSkill(workspacePath, skillRef, actor, options = {}) {
460
+ const skill = loadSkill(workspacePath, skillRef);
461
+ const slug = skillSlug(String(skill.fields.title ?? skillRef));
462
+ const currentVersion = String(skill.fields.version ?? "0.1.0");
463
+ const nextVersion = options.version ?? bumpPatchVersion(currentVersion);
464
+ const updated = update(workspacePath, skill.path, {
465
+ status: "active",
466
+ version: nextVersion,
467
+ promoted_at: (/* @__PURE__ */ new Date()).toISOString()
468
+ }, void 0, actor);
469
+ writeSkillManifest(workspacePath, slug, updated, actor);
470
+ return updated;
471
+ }
472
+ function skillSlug(title) {
473
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
474
+ }
475
+ function normalizeSkillRefCandidates(skillRef) {
476
+ const raw = skillRef.trim();
477
+ if (!raw) return [];
478
+ if (raw.includes("/")) {
479
+ const normalized = raw.endsWith(".md") ? raw : `${raw}.md`;
480
+ if (normalized.endsWith("/SKILL.md")) return [normalized];
481
+ if (normalized.endsWith("/SKILL")) return [`${normalized}.md`];
482
+ if (normalized.endsWith(".md")) {
483
+ const noExt = normalized.slice(0, -3);
484
+ return [normalized, `${noExt}/SKILL.md`];
485
+ }
486
+ return [normalized, `${normalized}/SKILL.md`];
487
+ }
488
+ const slug = skillSlug(raw);
489
+ return [folderSkillPath(slug), legacySkillPath(slug)];
490
+ }
491
+ function bumpPatchVersion(version) {
492
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
493
+ if (!match) return "0.1.0";
494
+ const major = Number.parseInt(match[1], 10);
495
+ const minor = Number.parseInt(match[2], 10);
496
+ const patch = Number.parseInt(match[3], 10) + 1;
497
+ return `${major}.${minor}.${patch}`;
498
+ }
499
+ function folderSkillPath(slug) {
500
+ return `skills/${slug}/SKILL.md`;
501
+ }
502
+ function legacySkillPath(slug) {
503
+ return `skills/${slug}.md`;
504
+ }
505
+ function ensureSkillBundleScaffold(workspacePath, slug) {
506
+ const skillRoot = path4.join(workspacePath, "skills", slug);
507
+ fs4.mkdirSync(skillRoot, { recursive: true });
508
+ for (const subdir of ["scripts", "examples", "tests", "assets"]) {
509
+ fs4.mkdirSync(path4.join(skillRoot, subdir), { recursive: true });
510
+ }
511
+ }
512
+ function writeSkillManifest(workspacePath, slug, skill, actor) {
513
+ const manifestPath = path4.join(workspacePath, "skills", slug, "skill-manifest.json");
514
+ const dir = path4.dirname(manifestPath);
515
+ fs4.mkdirSync(dir, { recursive: true });
516
+ const manifest = {
517
+ version: 1,
518
+ slug,
519
+ title: String(skill.fields.title ?? slug),
520
+ primitivePath: skill.path,
521
+ owner: String(skill.fields.owner ?? actor),
522
+ skillVersion: String(skill.fields.version ?? "0.1.0"),
523
+ status: String(skill.fields.status ?? "draft"),
524
+ dependsOn: Array.isArray(skill.fields.depends_on) ? skill.fields.depends_on.map((value) => String(value)) : [],
525
+ components: {
526
+ skillDoc: "SKILL.md",
527
+ scriptsDir: "scripts/",
528
+ examplesDir: "examples/",
529
+ testsDir: "tests/",
530
+ assetsDir: "assets/"
531
+ },
532
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
533
+ };
534
+ fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
535
+ }
536
+
537
+ // src/lens.ts
538
+ var lens_exports = {};
539
+ __export(lens_exports, {
540
+ generateContextLens: () => generateContextLens,
541
+ listContextLenses: () => listContextLenses,
542
+ materializeContextLens: () => materializeContextLens
543
+ });
544
+ import fs5 from "fs";
545
+ import path5 from "path";
546
+ var DEFAULT_LOOKBACK_HOURS = 24;
547
+ var DEFAULT_STALE_HOURS = 24;
548
+ var DEFAULT_LIMIT = 10;
549
+ var PRIORITY_ORDER = {
550
+ urgent: 0,
551
+ high: 1,
552
+ medium: 2,
553
+ low: 3
554
+ };
555
+ var HIGH_RISK_PRIORITIES = /* @__PURE__ */ new Set(["urgent", "high"]);
556
+ var HIGH_RISK_SEVERITIES = /* @__PURE__ */ new Set(["sev0", "sev1", "sev2"]);
557
+ var BUILT_IN_LENSES = [
558
+ {
559
+ id: "my-work",
560
+ description: "Actor workload, blockers, stale claims, and ready-next queue"
561
+ },
562
+ {
563
+ id: "team-risk",
564
+ description: "High-risk blockers, stale active claims, failed runs, and incidents"
565
+ },
566
+ {
567
+ id: "customer-health",
568
+ description: "Customer-tagged delivery health, blockers, and related incidents"
569
+ },
570
+ {
571
+ id: "exec-brief",
572
+ description: "Top priorities, momentum, risks, and recent decisions"
573
+ }
574
+ ];
575
+ function listContextLenses() {
576
+ return BUILT_IN_LENSES.map((lens) => ({ ...lens }));
577
+ }
578
+ function generateContextLens(workspacePath, lensId, options = {}) {
579
+ const normalizedLensId = normalizeLensId(lensId);
580
+ const normalizedOptions = normalizeLensOptions(options);
581
+ switch (normalizedLensId) {
582
+ case "my-work":
583
+ return buildMyWorkLens(workspacePath, normalizedOptions);
584
+ case "team-risk":
585
+ return buildTeamRiskLens(workspacePath, normalizedOptions);
586
+ case "customer-health":
587
+ return buildCustomerHealthLens(workspacePath, normalizedOptions);
588
+ case "exec-brief":
589
+ return buildExecBriefLens(workspacePath, normalizedOptions);
590
+ default:
591
+ return assertNever(normalizedLensId);
592
+ }
593
+ }
594
+ function materializeContextLens(workspacePath, lensId, options) {
595
+ const result = generateContextLens(workspacePath, lensId, options);
596
+ const absOutputPath = resolvePathWithinWorkspace2(workspacePath, options.outputPath);
597
+ const relOutputPath = path5.relative(workspacePath, absOutputPath).replace(/\\/g, "/");
598
+ const parentDir = path5.dirname(absOutputPath);
599
+ if (!fs5.existsSync(parentDir)) fs5.mkdirSync(parentDir, { recursive: true });
600
+ const existed = fs5.existsSync(absOutputPath);
601
+ fs5.writeFileSync(absOutputPath, result.markdown, "utf-8");
602
+ append(
603
+ workspacePath,
604
+ options.actor ?? result.actor ?? "system",
605
+ existed ? "update" : "create",
606
+ relOutputPath,
607
+ "lens",
608
+ {
609
+ lens: result.lens,
610
+ sections: result.sections.length
611
+ }
612
+ );
613
+ return {
614
+ ...result,
615
+ outputPath: relOutputPath,
616
+ created: !existed
617
+ };
618
+ }
619
+ function buildMyWorkLens(workspacePath, options) {
620
+ const actor = options.actor;
621
+ const nowMs = Date.now();
622
+ const staleCutoffMs = nowMs - options.staleHours * 60 * 60 * 1e3;
623
+ const claims = [...allClaims(workspacePath).entries()];
624
+ const myClaimedThreads = claims.filter(([, owner]) => owner === actor).map(([target]) => read(workspacePath, target)).filter((instance) => !!instance && instance.type === "thread").sort(compareThreadsByPriorityThenUpdated);
625
+ const myBlockedThreads = myClaimedThreads.filter((instance) => String(instance.fields.status ?? "") === "blocked").slice(0, options.limit);
626
+ const staleClaims = myClaimedThreads.filter((instance) => isStale(instance, staleCutoffMs)).slice(0, options.limit);
627
+ const nextReady = listReadyThreads(workspacePath).filter((instance) => !instance.fields.owner).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
628
+ const sections = [
629
+ {
630
+ id: "my_claims",
631
+ title: `Claimed Threads (${actor})`,
632
+ items: myClaimedThreads.slice(0, options.limit).map((instance) => toThreadItem(instance, nowMs))
633
+ },
634
+ {
635
+ id: "my_blockers",
636
+ title: `Blocked Threads (${actor})`,
637
+ items: myBlockedThreads.map((instance) => toThreadItem(instance, nowMs))
638
+ },
639
+ {
640
+ id: "stale_claims",
641
+ title: `Stale Claims (${options.staleHours}h+)`,
642
+ items: staleClaims.map((instance) => toThreadItem(instance, nowMs))
643
+ },
644
+ {
645
+ id: "next_ready",
646
+ title: "Next Ready Threads",
647
+ items: nextReady.map((instance) => toThreadItem(instance, nowMs))
648
+ }
649
+ ];
650
+ return finalizeLensResult("my-work", {
651
+ actor,
652
+ options,
653
+ metrics: {
654
+ myClaims: myClaimedThreads.length,
655
+ blocked: myBlockedThreads.length,
656
+ staleClaims: staleClaims.length,
657
+ nextReady: nextReady.length
658
+ },
659
+ sections
660
+ });
661
+ }
662
+ function buildTeamRiskLens(workspacePath, options) {
663
+ const nowMs = Date.now();
664
+ const staleCutoffMs = nowMs - options.staleHours * 60 * 60 * 1e3;
665
+ const lookbackCutoffMs = nowMs - options.lookbackHours * 60 * 60 * 1e3;
666
+ const threads = list(workspacePath, "thread");
667
+ const blockedHighPriority = threads.filter((instance) => String(instance.fields.status ?? "") === "blocked").filter((instance) => HIGH_RISK_PRIORITIES.has(normalizePriority(instance.fields.priority))).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
668
+ const staleActiveClaims = [...allClaims(workspacePath).entries()].map(([target, owner]) => ({ owner, instance: read(workspacePath, target) })).filter((entry) => !!entry.instance && entry.instance.type === "thread").filter((entry) => String(entry.instance.fields.status ?? "") === "active").filter((entry) => isStale(entry.instance, staleCutoffMs)).slice(0, options.limit);
669
+ const failedRuns = listRuns(workspacePath, { status: "failed" }).filter((run) => parseTimestamp(run.updatedAt) >= lookbackCutoffMs).slice(0, options.limit);
670
+ const highSeverityIncidents = list(workspacePath, "incident").filter((incident) => String(incident.fields.status ?? "") === "active").filter((incident) => HIGH_RISK_SEVERITIES.has(normalizeSeverity(incident.fields.severity))).slice(0, options.limit);
671
+ const sections = [
672
+ {
673
+ id: "blocked_high_priority_threads",
674
+ title: "High Priority Blocked Threads",
675
+ items: blockedHighPriority.map((instance) => toThreadItem(instance, nowMs))
676
+ },
677
+ {
678
+ id: "stale_active_claims",
679
+ title: `Stale Active Claims (${options.staleHours}h+)`,
680
+ items: staleActiveClaims.map((entry) => ({
681
+ ...toThreadItem(entry.instance, nowMs),
682
+ owner: entry.owner
683
+ }))
684
+ },
685
+ {
686
+ id: "failed_runs",
687
+ title: `Failed Runs (${options.lookbackHours}h window)`,
688
+ items: failedRuns.map(toRunItem)
689
+ },
690
+ {
691
+ id: "active_high_severity_incidents",
692
+ title: "Active High-Severity Incidents",
693
+ items: highSeverityIncidents.map((incident) => toIncidentItem(incident, nowMs))
694
+ }
695
+ ];
696
+ return finalizeLensResult("team-risk", {
697
+ actor: options.actor,
698
+ options,
699
+ metrics: {
700
+ blockedHighPriority: blockedHighPriority.length,
701
+ staleActiveClaims: staleActiveClaims.length,
702
+ failedRuns: failedRuns.length,
703
+ activeHighSeverityIncidents: highSeverityIncidents.length
704
+ },
705
+ sections
706
+ });
707
+ }
708
+ function buildCustomerHealthLens(workspacePath, options) {
709
+ const nowMs = Date.now();
710
+ const customerThreads = list(workspacePath, "thread").filter(isCustomerLinked).sort(compareThreadsByPriorityThenUpdated);
711
+ const activeCustomerThreads = customerThreads.filter((instance) => ["open", "active"].includes(String(instance.fields.status ?? ""))).slice(0, options.limit);
712
+ const blockedCustomerThreads = customerThreads.filter((instance) => String(instance.fields.status ?? "") === "blocked").slice(0, options.limit);
713
+ const customerIncidents = list(workspacePath, "incident").filter((incident) => String(incident.fields.status ?? "") === "active").filter(isCustomerLinked).slice(0, options.limit);
714
+ const clients = list(workspacePath, "client").slice(0, options.limit);
715
+ const sections = [
716
+ {
717
+ id: "active_customer_threads",
718
+ title: "Active Customer Threads",
719
+ items: activeCustomerThreads.map((instance) => toThreadItem(instance, nowMs))
720
+ },
721
+ {
722
+ id: "blocked_customer_threads",
723
+ title: "Blocked Customer Threads",
724
+ items: blockedCustomerThreads.map((instance) => toThreadItem(instance, nowMs))
725
+ },
726
+ {
727
+ id: "customer_incidents",
728
+ title: "Customer Incidents",
729
+ items: customerIncidents.map((incident) => toIncidentItem(incident, nowMs))
730
+ },
731
+ {
732
+ id: "client_records",
733
+ title: "Client Records",
734
+ items: clients.map((instance) => ({
735
+ title: String(instance.fields.title ?? instance.path),
736
+ path: instance.path,
737
+ status: stringOrUndefined(instance.fields.status),
738
+ detail: stringOrUndefined(instance.fields.health ?? instance.fields.risk),
739
+ ageHours: ageHours(instance, nowMs)
740
+ }))
741
+ }
742
+ ];
743
+ return finalizeLensResult("customer-health", {
744
+ actor: options.actor,
745
+ options,
746
+ metrics: {
747
+ activeCustomerThreads: activeCustomerThreads.length,
748
+ blockedCustomerThreads: blockedCustomerThreads.length,
749
+ customerIncidents: customerIncidents.length,
750
+ clients: clients.length
751
+ },
752
+ sections
753
+ });
754
+ }
755
+ function buildExecBriefLens(workspacePath, options) {
756
+ const nowMs = Date.now();
757
+ const lookbackCutoffMs = nowMs - options.lookbackHours * 60 * 60 * 1e3;
758
+ const threads = list(workspacePath, "thread");
759
+ const topPriorities = threads.filter((instance) => ["open", "active"].includes(String(instance.fields.status ?? ""))).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
760
+ const momentum = threads.filter((instance) => String(instance.fields.status ?? "") === "done").filter((instance) => parseTimestamp(instance.fields.updated) >= lookbackCutoffMs).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
761
+ const blockedHighPriority = threads.filter((instance) => String(instance.fields.status ?? "") === "blocked").filter((instance) => HIGH_RISK_PRIORITIES.has(normalizePriority(instance.fields.priority))).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
762
+ const failedRuns = listRuns(workspacePath, { status: "failed" }).filter((run) => parseTimestamp(run.updatedAt) >= lookbackCutoffMs).slice(0, options.limit);
763
+ const decisions = list(workspacePath, "decision").filter((instance) => ["proposed", "approved", "active"].includes(String(instance.fields.status ?? ""))).filter((instance) => parseTimestamp(instance.fields.updated ?? instance.fields.date) >= lookbackCutoffMs).slice(0, options.limit);
764
+ const sections = [
765
+ {
766
+ id: "top_priorities",
767
+ title: "Top Priorities",
768
+ items: topPriorities.map((instance) => toThreadItem(instance, nowMs))
769
+ },
770
+ {
771
+ id: "momentum",
772
+ title: `Momentum (${options.lookbackHours}h completed)`,
773
+ items: momentum.map((instance) => toThreadItem(instance, nowMs))
774
+ },
775
+ {
776
+ id: "key_risks",
777
+ title: "Key Risks",
778
+ items: [
779
+ ...blockedHighPriority.map((instance) => toThreadItem(instance, nowMs)),
780
+ ...failedRuns.map(toRunItem)
781
+ ].slice(0, options.limit)
782
+ },
783
+ {
784
+ id: "recent_decisions",
785
+ title: `Decisions (${options.lookbackHours}h window)`,
786
+ items: decisions.map((instance) => ({
787
+ title: String(instance.fields.title ?? instance.path),
788
+ path: instance.path,
789
+ status: stringOrUndefined(instance.fields.status),
790
+ detail: stringOrUndefined(instance.fields.date),
791
+ ageHours: ageHours(instance, nowMs)
792
+ }))
793
+ }
794
+ ];
795
+ return finalizeLensResult("exec-brief", {
796
+ actor: options.actor,
797
+ options,
798
+ metrics: {
799
+ topPriorities: topPriorities.length,
800
+ momentumDone: momentum.length,
801
+ risks: blockedHighPriority.length + failedRuns.length,
802
+ decisions: decisions.length
803
+ },
804
+ sections
805
+ });
806
+ }
807
+ function finalizeLensResult(lens, input) {
808
+ const base = {
809
+ lens,
810
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
811
+ actor: input.actor,
812
+ options: {
813
+ lookbackHours: input.options.lookbackHours,
814
+ staleHours: input.options.staleHours,
815
+ limit: input.options.limit
816
+ },
817
+ metrics: input.metrics,
818
+ sections: input.sections
819
+ };
820
+ return {
821
+ ...base,
822
+ markdown: renderLensMarkdown(base)
823
+ };
824
+ }
825
+ function toThreadItem(instance, nowMs) {
826
+ return {
827
+ title: String(instance.fields.title ?? instance.path),
828
+ path: instance.path,
829
+ status: stringOrUndefined(instance.fields.status),
830
+ priority: stringOrUndefined(instance.fields.priority),
831
+ owner: stringOrUndefined(instance.fields.owner),
832
+ detail: renderThreadDependencies(instance),
833
+ ageHours: ageHours(instance, nowMs)
834
+ };
835
+ }
836
+ function toIncidentItem(instance, nowMs) {
837
+ return {
838
+ title: String(instance.fields.title ?? instance.path),
839
+ path: instance.path,
840
+ status: stringOrUndefined(instance.fields.status),
841
+ priority: stringOrUndefined(instance.fields.severity),
842
+ owner: stringOrUndefined(instance.fields.owner),
843
+ ageHours: ageHours(instance, nowMs)
844
+ };
845
+ }
846
+ function toRunItem(run) {
847
+ return {
848
+ title: run.objective,
849
+ path: `runs/${run.id}.md`,
850
+ status: run.status,
851
+ owner: run.actor,
852
+ detail: run.error ?? run.output,
853
+ ageHours: ageHoursFromIso(run.updatedAt)
854
+ };
855
+ }
856
+ function renderLensMarkdown(input) {
857
+ const lines = [
858
+ `# Workgraph Context Lens: ${input.lens}`,
859
+ "",
860
+ `Generated: ${input.generatedAt}`,
861
+ ...input.actor ? [`Actor: ${input.actor}`] : [],
862
+ `Lookback: ${input.options.lookbackHours}h`,
863
+ `Stale threshold: ${input.options.staleHours}h`,
864
+ `Section limit: ${input.options.limit}`,
865
+ "",
866
+ "## Metrics",
867
+ "",
868
+ ...Object.entries(input.metrics).map(([metric, value]) => `- ${metric}: ${value}`),
869
+ ""
870
+ ];
871
+ for (const section of input.sections) {
872
+ lines.push(`## ${section.title}`);
873
+ lines.push("");
874
+ if (section.items.length === 0) {
875
+ lines.push("- None");
876
+ lines.push("");
877
+ continue;
878
+ }
879
+ for (const item of section.items) {
880
+ lines.push(`- ${renderLensItem(item)}`);
881
+ }
882
+ lines.push("");
883
+ }
884
+ return lines.join("\n");
885
+ }
886
+ function renderLensItem(item) {
887
+ const components = [item.title];
888
+ if (item.path) components.push(`(\`${item.path}\`)`);
889
+ const metadata = [];
890
+ if (item.status) metadata.push(`status=${item.status}`);
891
+ if (item.priority) metadata.push(`priority=${item.priority}`);
892
+ if (item.owner) metadata.push(`owner=${item.owner}`);
893
+ if (typeof item.ageHours === "number") metadata.push(`age=${item.ageHours.toFixed(1)}h`);
894
+ if (metadata.length > 0) components.push(`[${metadata.join(", ")}]`);
895
+ if (item.detail) components.push(`- ${item.detail}`);
896
+ return components.join(" ");
897
+ }
898
+ function resolvePathWithinWorkspace2(workspacePath, outputPath) {
899
+ const base = path5.resolve(workspacePath);
900
+ const resolved = path5.resolve(base, outputPath);
901
+ if (!resolved.startsWith(base + path5.sep) && resolved !== base) {
902
+ throw new Error(`Invalid lens output path: ${outputPath}`);
903
+ }
904
+ return resolved;
905
+ }
906
+ function normalizeLensId(value) {
907
+ const normalized = String(value).trim().toLowerCase().replace(/^lens:\/\//, "");
908
+ if (normalized === "my-work") return "my-work";
909
+ if (normalized === "team-risk") return "team-risk";
910
+ if (normalized === "customer-health") return "customer-health";
911
+ if (normalized === "exec-brief") return "exec-brief";
912
+ const valid = BUILT_IN_LENSES.map((item) => item.id).join(", ");
913
+ throw new Error(`Unknown context lens "${value}". Valid lenses: ${valid}`);
914
+ }
915
+ function normalizeLensOptions(options) {
916
+ return {
917
+ actor: String(options.actor ?? "anonymous").trim() || "anonymous",
918
+ lookbackHours: parsePositiveNumber(options.lookbackHours, "lookbackHours", DEFAULT_LOOKBACK_HOURS),
919
+ staleHours: parsePositiveNumber(options.staleHours, "staleHours", DEFAULT_STALE_HOURS),
920
+ limit: parsePositiveInteger(options.limit, "limit", DEFAULT_LIMIT)
921
+ };
922
+ }
923
+ function parsePositiveNumber(value, fieldName, defaultValue) {
924
+ if (value === void 0 || value === null) return defaultValue;
925
+ const parsed = Number(value);
926
+ if (!Number.isFinite(parsed) || parsed <= 0) {
927
+ throw new Error(`Invalid ${fieldName}: expected a positive number.`);
928
+ }
929
+ return parsed;
930
+ }
931
+ function parsePositiveInteger(value, fieldName, defaultValue) {
932
+ if (value === void 0 || value === null) return defaultValue;
933
+ const parsed = Number.parseInt(String(value), 10);
934
+ if (!Number.isInteger(parsed) || parsed <= 0) {
935
+ throw new Error(`Invalid ${fieldName}: expected a positive integer.`);
936
+ }
937
+ return parsed;
938
+ }
939
+ function compareThreadsByPriorityThenUpdated(a, b) {
940
+ const priorityDelta = rankPriority(a) - rankPriority(b);
941
+ if (priorityDelta !== 0) return priorityDelta;
942
+ return parseTimestamp(b.fields.updated) - parseTimestamp(a.fields.updated);
943
+ }
944
+ function rankPriority(instance) {
945
+ const priority = normalizePriority(instance.fields.priority);
946
+ return PRIORITY_ORDER[priority] ?? PRIORITY_ORDER.medium;
947
+ }
948
+ function normalizePriority(value) {
949
+ return String(value ?? "medium").trim().toLowerCase();
950
+ }
951
+ function normalizeSeverity(value) {
952
+ return String(value ?? "sev4").trim().toLowerCase();
953
+ }
954
+ function isStale(instance, staleCutoffMs) {
955
+ const updatedAt = parseTimestamp(instance.fields.updated ?? instance.fields.created);
956
+ if (!Number.isFinite(updatedAt)) return false;
957
+ return updatedAt <= staleCutoffMs;
958
+ }
959
+ function parseTimestamp(value) {
960
+ const parsed = Date.parse(String(value ?? ""));
961
+ return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
962
+ }
963
+ function ageHours(instance, nowMs) {
964
+ const updatedAt = parseTimestamp(instance.fields.updated ?? instance.fields.created);
965
+ if (!Number.isFinite(updatedAt)) return void 0;
966
+ return Math.max(0, (nowMs - updatedAt) / (60 * 60 * 1e3));
967
+ }
968
+ function ageHoursFromIso(value) {
969
+ const nowMs = Date.now();
970
+ const ts = parseTimestamp(value);
971
+ if (!Number.isFinite(ts)) return void 0;
972
+ return Math.max(0, (nowMs - ts) / (60 * 60 * 1e3));
973
+ }
974
+ function renderThreadDependencies(instance) {
975
+ const deps = instance.fields.deps;
976
+ if (!Array.isArray(deps) || deps.length === 0) return void 0;
977
+ const visible = deps.slice(0, 3).map((value) => String(value));
978
+ const suffix = deps.length > visible.length ? ` +${deps.length - visible.length} more` : "";
979
+ return `deps: ${visible.join(", ")}${suffix}`;
980
+ }
981
+ function isCustomerLinked(instance) {
982
+ const tags = normalizeTags(instance.fields.tags);
983
+ if (tags.includes("customer") || tags.includes("client")) return true;
984
+ const candidateFields = ["client", "client_ref", "customer", "customer_ref", "account", "account_ref"];
985
+ return candidateFields.some((key) => {
986
+ const value = instance.fields[key];
987
+ return typeof value === "string" && value.trim().length > 0;
988
+ });
989
+ }
990
+ function normalizeTags(value) {
991
+ if (!Array.isArray(value)) return [];
992
+ return value.map((item) => String(item).trim().toLowerCase()).filter(Boolean);
993
+ }
994
+ function stringOrUndefined(value) {
995
+ if (value === void 0 || value === null) return void 0;
996
+ const normalized = String(value).trim();
997
+ return normalized.length > 0 ? normalized : void 0;
998
+ }
999
+ function assertNever(value) {
1000
+ throw new Error(`Unhandled lens variant: ${String(value)}`);
1001
+ }
1002
+
1003
+ // src/board.ts
1004
+ var board_exports = {};
1005
+ __export(board_exports, {
1006
+ generateKanbanBoard: () => generateKanbanBoard,
1007
+ syncKanbanBoard: () => syncKanbanBoard
1008
+ });
1009
+ import fs6 from "fs";
1010
+ import path6 from "path";
1011
+ function generateKanbanBoard(workspacePath, options = {}) {
1012
+ const threads = list(workspacePath, "thread");
1013
+ const grouped = groupThreads(threads);
1014
+ const includeCancelled = options.includeCancelled === true;
1015
+ const lanes = [
1016
+ { title: "Backlog", items: grouped.open, checkChar: " " },
1017
+ { title: "In Progress", items: grouped.active, checkChar: " " },
1018
+ { title: "Blocked", items: grouped.blocked, checkChar: " " },
1019
+ { title: "Done", items: grouped.done, checkChar: "x" }
1020
+ ];
1021
+ if (includeCancelled) {
1022
+ lanes.push({ title: "Cancelled", items: grouped.cancelled, checkChar: "x" });
1023
+ }
1024
+ const content = renderKanbanMarkdown(lanes);
1025
+ const relOutputPath = options.outputPath ?? "ops/Workgraph Board.md";
1026
+ const absOutputPath = resolvePathWithinWorkspace3(workspacePath, relOutputPath);
1027
+ const parentDir = path6.dirname(absOutputPath);
1028
+ if (!fs6.existsSync(parentDir)) fs6.mkdirSync(parentDir, { recursive: true });
1029
+ fs6.writeFileSync(absOutputPath, content, "utf-8");
1030
+ return {
1031
+ outputPath: path6.relative(workspacePath, absOutputPath).replace(/\\/g, "/"),
1032
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1033
+ counts: {
1034
+ backlog: grouped.open.length,
1035
+ inProgress: grouped.active.length,
1036
+ blocked: grouped.blocked.length,
1037
+ done: grouped.done.length,
1038
+ cancelled: grouped.cancelled.length
1039
+ },
1040
+ content
1041
+ };
1042
+ }
1043
+ function syncKanbanBoard(workspacePath, options = {}) {
1044
+ return generateKanbanBoard(workspacePath, options);
1045
+ }
1046
+ function groupThreads(threads) {
1047
+ const groups = {
1048
+ open: [],
1049
+ active: [],
1050
+ blocked: [],
1051
+ done: [],
1052
+ cancelled: []
1053
+ };
1054
+ for (const thread of threads) {
1055
+ const status = String(thread.fields.status ?? "open");
1056
+ switch (status) {
1057
+ case "active":
1058
+ groups.active.push(thread);
1059
+ break;
1060
+ case "blocked":
1061
+ groups.blocked.push(thread);
1062
+ break;
1063
+ case "done":
1064
+ groups.done.push(thread);
1065
+ break;
1066
+ case "cancelled":
1067
+ groups.cancelled.push(thread);
1068
+ break;
1069
+ case "open":
1070
+ default:
1071
+ groups.open.push(thread);
1072
+ break;
1073
+ }
1074
+ }
1075
+ const byPriority = (a, b) => {
1076
+ const rank = (value) => {
1077
+ switch (String(value ?? "medium")) {
1078
+ case "urgent":
1079
+ return 0;
1080
+ case "high":
1081
+ return 1;
1082
+ case "medium":
1083
+ return 2;
1084
+ case "low":
1085
+ return 3;
1086
+ default:
1087
+ return 4;
1088
+ }
1089
+ };
1090
+ return rank(a.fields.priority) - rank(b.fields.priority) || String(a.fields.title).localeCompare(String(b.fields.title));
1091
+ };
1092
+ groups.open.sort(byPriority);
1093
+ groups.active.sort(byPriority);
1094
+ groups.blocked.sort(byPriority);
1095
+ groups.done.sort(byPriority);
1096
+ groups.cancelled.sort(byPriority);
1097
+ return groups;
1098
+ }
1099
+ function renderKanbanMarkdown(lanes) {
1100
+ const settings = {
1101
+ "kanban-plugin": "board"
1102
+ };
1103
+ const lines = [
1104
+ "---",
1105
+ "kanban-plugin: board",
1106
+ "---",
1107
+ ""
1108
+ ];
1109
+ for (const lane of lanes) {
1110
+ lines.push(`## ${lane.title}`);
1111
+ lines.push("");
1112
+ for (const thread of lane.items) {
1113
+ const title = String(thread.fields.title ?? thread.path);
1114
+ const priority = String(thread.fields.priority ?? "medium");
1115
+ lines.push(`- [${lane.checkChar}] [[${thread.path}|${title}]] (#${priority})`);
1116
+ }
1117
+ lines.push("");
1118
+ lines.push("");
1119
+ lines.push("");
1120
+ }
1121
+ lines.push("%% kanban:settings");
1122
+ lines.push("```");
1123
+ lines.push(JSON.stringify(settings));
1124
+ lines.push("```");
1125
+ lines.push("%%");
1126
+ lines.push("");
1127
+ return lines.join("\n");
1128
+ }
1129
+ function resolvePathWithinWorkspace3(workspacePath, outputPath) {
1130
+ const base = path6.resolve(workspacePath);
1131
+ const resolved = path6.resolve(base, outputPath);
1132
+ if (!resolved.startsWith(base + path6.sep) && resolved !== base) {
1133
+ throw new Error(`Invalid board output path: ${outputPath}`);
1134
+ }
1135
+ return resolved;
1136
+ }
1137
+
1138
+ // src/agent.ts
1139
+ var agent_exports = {};
1140
+ __export(agent_exports, {
1141
+ getPresence: () => getPresence,
1142
+ heartbeat: () => heartbeat,
1143
+ list: () => list2
1144
+ });
1145
+ var PRESENCE_TYPE = "presence";
1146
+ var PRESENCE_STATUS_VALUES = /* @__PURE__ */ new Set(["online", "busy", "offline"]);
1147
+ function heartbeat(workspacePath, name, options = {}) {
1148
+ const existing = getPresence(workspacePath, name);
1149
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1150
+ const status = normalizeStatus(options.status ?? existing?.fields.status) ?? "online";
1151
+ const capabilities = normalizeCapabilities(options.capabilities ?? existing?.fields.capabilities);
1152
+ const actor = options.actor ?? name;
1153
+ const currentTask = options.currentTask !== void 0 ? normalizeTask(options.currentTask) : normalizeTask(existing?.fields.current_task);
1154
+ if (!existing) {
1155
+ return create(
1156
+ workspacePath,
1157
+ PRESENCE_TYPE,
1158
+ {
1159
+ name,
1160
+ status,
1161
+ current_task: currentTask,
1162
+ last_seen: now,
1163
+ capabilities
1164
+ },
1165
+ renderPresenceBody(name, status, currentTask, capabilities, now),
1166
+ actor
1167
+ );
1168
+ }
1169
+ return update(
1170
+ workspacePath,
1171
+ existing.path,
1172
+ {
1173
+ name,
1174
+ status,
1175
+ current_task: currentTask,
1176
+ last_seen: now,
1177
+ capabilities
1178
+ },
1179
+ renderPresenceBody(name, status, currentTask, capabilities, now),
1180
+ actor
1181
+ );
1182
+ }
1183
+ function list2(workspacePath) {
1184
+ return list(workspacePath, PRESENCE_TYPE).sort((a, b) => {
1185
+ const aSeen = Date.parse(String(a.fields.last_seen ?? ""));
1186
+ const bSeen = Date.parse(String(b.fields.last_seen ?? ""));
1187
+ const safeA = Number.isFinite(aSeen) ? aSeen : 0;
1188
+ const safeB = Number.isFinite(bSeen) ? bSeen : 0;
1189
+ if (safeA !== safeB) return safeB - safeA;
1190
+ return String(a.fields.name ?? "").localeCompare(String(b.fields.name ?? ""));
1191
+ });
1192
+ }
1193
+ function getPresence(workspacePath, name) {
1194
+ const target = normalizeName(name);
1195
+ return list2(workspacePath).find((entry) => normalizeName(entry.fields.name) === target) ?? null;
1196
+ }
1197
+ function normalizeStatus(value) {
1198
+ const normalized = String(value ?? "").trim().toLowerCase();
1199
+ if (!PRESENCE_STATUS_VALUES.has(normalized)) return null;
1200
+ return normalized;
1201
+ }
1202
+ function normalizeCapabilities(value) {
1203
+ if (!Array.isArray(value)) return [];
1204
+ return value.map((entry) => String(entry).trim()).filter(Boolean);
1205
+ }
1206
+ function normalizeTask(value) {
1207
+ const normalized = String(value ?? "").trim();
1208
+ return normalized ? normalized : null;
1209
+ }
1210
+ function normalizeName(value) {
1211
+ return String(value ?? "").trim().toLowerCase();
1212
+ }
1213
+ function renderPresenceBody(name, status, currentTask, capabilities, lastSeen) {
1214
+ const lines = [
1215
+ "## Presence",
1216
+ "",
1217
+ `- agent: ${name}`,
1218
+ `- status: ${status}`,
1219
+ `- last_seen: ${lastSeen}`,
1220
+ `- current_task: ${currentTask ?? "none"}`,
1221
+ "",
1222
+ "## Capabilities",
1223
+ "",
1224
+ ...capabilities.length > 0 ? capabilities.map((capability) => `- ${capability}`) : ["- none"],
1225
+ ""
1226
+ ];
1227
+ return lines.join("\n");
1228
+ }
1229
+
1230
+ // src/onboard.ts
1231
+ var onboard_exports = {};
1232
+ __export(onboard_exports, {
1233
+ onboardWorkspace: () => onboardWorkspace,
1234
+ updateOnboardingStatus: () => updateOnboardingStatus
1235
+ });
1236
+ function onboardWorkspace(workspacePath, options) {
1237
+ const spaces = options.spaces && options.spaces.length > 0 ? options.spaces : ["platform", "product", "operations"];
1238
+ const spacesCreated = [];
1239
+ for (const space of spaces) {
1240
+ const title = titleCase2(space);
1241
+ const created = create(
1242
+ workspacePath,
1243
+ "space",
1244
+ {
1245
+ title,
1246
+ description: `${title} workspace lane`,
1247
+ members: [options.actor],
1248
+ tags: ["onboarded"]
1249
+ },
1250
+ `# ${title}
1251
+
1252
+ Auto-created during onboarding.
1253
+ `,
1254
+ options.actor
1255
+ );
1256
+ spacesCreated.push(created.path);
1257
+ }
1258
+ const threadsCreated = [];
1259
+ if (options.createDemoThreads !== false) {
1260
+ const templates = [
1261
+ { title: "Review workspace policy gates", goal: "Validate sensitive transitions are governed.", space: spacesCreated[0] },
1262
+ { title: "Configure board sync cadence", goal: "Set board update expectations for all agents.", space: spacesCreated[1] ?? spacesCreated[0] },
1263
+ { title: "Establish daily checkpoint routine", goal: "Agents leave actionable hand-off notes.", space: spacesCreated[2] ?? spacesCreated[0] }
1264
+ ];
1265
+ for (const template of templates) {
1266
+ const created = create(
1267
+ workspacePath,
1268
+ "thread",
1269
+ {
1270
+ title: template.title,
1271
+ goal: template.goal,
1272
+ status: "open",
1273
+ priority: "medium",
1274
+ space: template.space,
1275
+ context_refs: [template.space],
1276
+ tags: ["onboarding"]
1277
+ },
1278
+ `## Goal
1279
+
1280
+ ${template.goal}
1281
+ `,
1282
+ options.actor
1283
+ );
1284
+ threadsCreated.push(created.path);
1285
+ }
1286
+ }
1287
+ const boardResult = generateKanbanBoard(workspacePath, { outputPath: "ops/Onboarding Board.md" });
1288
+ const commandCenterResult = generateCommandCenter(workspacePath, {
1289
+ outputPath: "ops/Onboarding Command Center.md",
1290
+ actor: options.actor
1291
+ });
1292
+ const checkpointResult = checkpoint(
1293
+ workspacePath,
1294
+ options.actor,
1295
+ "Onboarding completed and workspace views initialized.",
1296
+ {
1297
+ next: ["Claim your next ready thread via `workgraph thread next --claim`"],
1298
+ blocked: [],
1299
+ tags: ["onboarding"]
1300
+ }
1301
+ );
1302
+ const onboarding = create(
1303
+ workspacePath,
1304
+ "onboarding",
1305
+ {
1306
+ title: `Onboarding for ${options.actor}`,
1307
+ actor: options.actor,
1308
+ status: "active",
1309
+ spaces: spacesCreated,
1310
+ thread_refs: threadsCreated,
1311
+ board: boardResult.outputPath,
1312
+ command_center: commandCenterResult.outputPath,
1313
+ tags: ["onboarding"]
1314
+ },
1315
+ [
1316
+ "# Onboarding",
1317
+ "",
1318
+ `Actor: ${options.actor}`,
1319
+ "",
1320
+ "## Spaces",
1321
+ "",
1322
+ ...spacesCreated.map((space) => `- [[${space}]]`),
1323
+ "",
1324
+ "## Starter Threads",
1325
+ "",
1326
+ ...threadsCreated.map((threadRef) => `- [[${threadRef}]]`),
1327
+ "",
1328
+ `Board: [[${boardResult.outputPath}]]`,
1329
+ `Command Center: [[${commandCenterResult.outputPath}]]`,
1330
+ ""
1331
+ ].join("\n"),
1332
+ options.actor
1333
+ );
1334
+ return {
1335
+ actor: options.actor,
1336
+ spacesCreated,
1337
+ threadsCreated,
1338
+ boardPath: boardResult.outputPath,
1339
+ commandCenterPath: commandCenterResult.outputPath,
1340
+ checkpointPath: checkpointResult.path,
1341
+ onboardingPath: onboarding.path
1342
+ };
1343
+ }
1344
+ function updateOnboardingStatus(workspacePath, onboardingPath, status, actor) {
1345
+ const onboarding = read(workspacePath, onboardingPath);
1346
+ if (!onboarding) throw new Error(`Onboarding primitive not found: ${onboardingPath}`);
1347
+ if (onboarding.type !== "onboarding") {
1348
+ throw new Error(`Target is not an onboarding primitive: ${onboardingPath}`);
1349
+ }
1350
+ const current = String(onboarding.fields.status ?? "active");
1351
+ const allowed = ONBOARDING_STATUS_TRANSITIONS[current] ?? [];
1352
+ if (!allowed.includes(status)) {
1353
+ throw new Error(`Invalid onboarding transition: ${current} -> ${status}. Allowed: ${allowed.join(", ") || "none"}`);
1354
+ }
1355
+ return update(
1356
+ workspacePath,
1357
+ onboardingPath,
1358
+ { status },
1359
+ void 0,
1360
+ actor
1361
+ );
1362
+ }
1363
+ var ONBOARDING_STATUS_TRANSITIONS = {
1364
+ active: ["paused", "completed"],
1365
+ paused: ["active", "completed"],
1366
+ completed: []
1367
+ };
1368
+ function titleCase2(value) {
1369
+ return value.split(/[-_\s]/g).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
1370
+ }
1371
+
1372
+ // src/search-qmd-adapter.ts
1373
+ var search_qmd_adapter_exports = {};
1374
+ __export(search_qmd_adapter_exports, {
1375
+ search: () => search
1376
+ });
1377
+ function search(workspacePath, text, options = {}) {
1378
+ const requestedMode = options.mode ?? "auto";
1379
+ const qmdEnabled = process.env.WORKGRAPH_QMD_ENDPOINT && process.env.WORKGRAPH_QMD_ENDPOINT.trim().length > 0;
1380
+ if (requestedMode === "qmd" && !qmdEnabled) {
1381
+ const results = keywordSearch(workspacePath, text, {
1382
+ type: options.type,
1383
+ limit: options.limit
1384
+ });
1385
+ return {
1386
+ mode: "core",
1387
+ query: text,
1388
+ results,
1389
+ fallbackReason: "QMD mode requested but WORKGRAPH_QMD_ENDPOINT is not configured."
1390
+ };
1391
+ }
1392
+ if (requestedMode === "qmd" && qmdEnabled) {
1393
+ const results = keywordSearch(workspacePath, text, {
1394
+ type: options.type,
1395
+ limit: options.limit
1396
+ });
1397
+ return {
1398
+ mode: "qmd",
1399
+ query: text,
1400
+ results,
1401
+ fallbackReason: "QMD endpoint configured; using core-compatible local ranking in MVP."
1402
+ };
1403
+ }
1404
+ if (requestedMode === "auto" && qmdEnabled) {
1405
+ const results = keywordSearch(workspacePath, text, {
1406
+ type: options.type,
1407
+ limit: options.limit
1408
+ });
1409
+ return {
1410
+ mode: "qmd",
1411
+ query: text,
1412
+ results,
1413
+ fallbackReason: "Auto mode selected; QMD endpoint detected; using core-compatible local ranking in MVP."
1414
+ };
1415
+ }
1416
+ return {
1417
+ mode: "core",
1418
+ query: text,
1419
+ results: keywordSearch(workspacePath, text, {
1420
+ type: options.type,
1421
+ limit: options.limit
1422
+ })
1423
+ };
1424
+ }
1425
+
1426
+ // src/trigger.ts
1427
+ var trigger_exports = {};
1428
+ __export(trigger_exports, {
1429
+ fireTrigger: () => fireTrigger
1430
+ });
1431
+ import { createHash } from "crypto";
1432
+ function fireTrigger(workspacePath, triggerPath, options) {
1433
+ const trigger = read(workspacePath, triggerPath);
1434
+ if (!trigger) throw new Error(`Trigger not found: ${triggerPath}`);
1435
+ if (trigger.type !== "trigger") throw new Error(`Target is not a trigger primitive: ${triggerPath}`);
1436
+ const triggerStatus = String(trigger.fields.status ?? "draft");
1437
+ if (!["approved", "active"].includes(triggerStatus)) {
1438
+ throw new Error(`Trigger must be approved/active to fire. Current status: ${triggerStatus}`);
1439
+ }
1440
+ const objective = options.objective ?? `Trigger ${String(trigger.fields.title ?? triggerPath)} fired action ${String(trigger.fields.action ?? "run")}`;
1441
+ const eventSeed = options.eventKey ?? (/* @__PURE__ */ new Date()).toISOString();
1442
+ const idempotencyKey = buildIdempotencyKey(triggerPath, eventSeed, objective);
1443
+ const run = createRun(workspacePath, {
1444
+ actor: options.actor,
1445
+ objective,
1446
+ context: {
1447
+ trigger_path: triggerPath,
1448
+ trigger_event: String(trigger.fields.event ?? ""),
1449
+ ...options.context
1450
+ },
1451
+ idempotencyKey
1452
+ });
1453
+ append(workspacePath, options.actor, "create", triggerPath, "trigger", {
1454
+ fired: true,
1455
+ event_key: eventSeed,
1456
+ run_id: run.id,
1457
+ idempotency_key: idempotencyKey
1458
+ });
1459
+ return {
1460
+ triggerPath,
1461
+ run,
1462
+ idempotencyKey
1463
+ };
1464
+ }
1465
+ function buildIdempotencyKey(triggerPath, eventSeed, objective) {
1466
+ return createHash("sha256").update(`${triggerPath}:${eventSeed}:${objective}`).digest("hex").slice(0, 32);
1467
+ }
1468
+
1469
+ // src/clawdapus.ts
1470
+ var clawdapus_exports = {};
1471
+ __export(clawdapus_exports, {
1472
+ CLAWDAPUS_INTEGRATION_PROVIDER: () => CLAWDAPUS_INTEGRATION_PROVIDER,
1473
+ DEFAULT_CLAWDAPUS_SKILL_URL: () => DEFAULT_CLAWDAPUS_SKILL_URL,
1474
+ fetchClawdapusSkillMarkdown: () => fetchClawdapusSkillMarkdown,
1475
+ installClawdapusSkill: () => installClawdapusSkill
1476
+ });
1477
+
1478
+ // src/integration-core.ts
1479
+ async function installSkillIntegration(workspacePath, provider, options) {
1480
+ const actor = options.actor.trim();
1481
+ if (!actor) {
1482
+ throw new Error(`${provider.id} integration requires a non-empty actor.`);
1483
+ }
1484
+ const title = options.title?.trim() || provider.defaultTitle;
1485
+ const sourceUrl = options.sourceUrl?.trim() || provider.defaultSourceUrl;
1486
+ const existing = loadSkillIfExists(workspacePath, title);
1487
+ if (existing && !options.force) {
1488
+ throw new Error(
1489
+ `Skill "${title}" already exists at ${existing.path}. Use --force to refresh it from source.`
1490
+ );
1491
+ }
1492
+ const fetchSkillMarkdown = options.fetchSkillMarkdown ?? ((url) => fetchSkillMarkdownFromUrl(url, provider.userAgent));
1493
+ const markdown = await fetchSkillMarkdown(sourceUrl);
1494
+ if (!markdown.trim()) {
1495
+ throw new Error(`Downloaded ${provider.id} skill from ${sourceUrl} is empty.`);
1496
+ }
1497
+ const skill = writeSkill(workspacePath, title, markdown, actor, {
1498
+ owner: options.owner ?? actor,
1499
+ status: options.status,
1500
+ distribution: provider.distribution,
1501
+ tags: mergeTags(provider.defaultTags, options.tags)
1502
+ });
1503
+ return {
1504
+ provider: provider.id,
1505
+ skill,
1506
+ sourceUrl,
1507
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
1508
+ replacedExisting: existing !== null
1509
+ };
1510
+ }
1511
+ async function fetchSkillMarkdownFromUrl(sourceUrl, userAgent = "@versatly/workgraph optional-integration") {
1512
+ let response;
1513
+ try {
1514
+ response = await fetch(sourceUrl, {
1515
+ headers: {
1516
+ "user-agent": userAgent
1517
+ }
1518
+ });
1519
+ } catch (error) {
1520
+ throw new Error(
1521
+ `Failed to download skill from ${sourceUrl}: ${errorMessage(error)}`
1522
+ );
1523
+ }
1524
+ if (!response.ok) {
1525
+ throw new Error(
1526
+ `Failed to download skill from ${sourceUrl}: HTTP ${response.status} ${response.statusText}`
1527
+ );
1528
+ }
1529
+ return response.text();
1530
+ }
1531
+ function loadSkillIfExists(workspacePath, skillRef) {
1532
+ try {
1533
+ return loadSkill(workspacePath, skillRef);
1534
+ } catch (error) {
1535
+ const message = errorMessage(error);
1536
+ if (message.startsWith("Skill not found:")) {
1537
+ return null;
1538
+ }
1539
+ throw error;
1540
+ }
1541
+ }
1542
+ function mergeTags(defaultTags, tags) {
1543
+ const merged = /* @__PURE__ */ new Set(["optional-integration"]);
1544
+ for (const tag of defaultTags) {
1545
+ const normalized = tag.trim();
1546
+ if (normalized) merged.add(normalized);
1547
+ }
1548
+ for (const tag of tags ?? []) {
1549
+ const normalized = tag.trim();
1550
+ if (normalized) merged.add(normalized);
1551
+ }
1552
+ return [...merged];
1553
+ }
1554
+ function errorMessage(error) {
1555
+ return error instanceof Error ? error.message : String(error);
1556
+ }
1557
+
1558
+ // src/clawdapus.ts
1559
+ var DEFAULT_CLAWDAPUS_SKILL_URL = "https://raw.githubusercontent.com/mostlydev/clawdapus/master/skills/clawdapus/SKILL.md";
1560
+ var CLAWDAPUS_INTEGRATION_PROVIDER = {
1561
+ id: "clawdapus",
1562
+ defaultTitle: "clawdapus",
1563
+ defaultSourceUrl: DEFAULT_CLAWDAPUS_SKILL_URL,
1564
+ distribution: "clawdapus-optional-integration",
1565
+ defaultTags: ["clawdapus"],
1566
+ userAgent: "@versatly/workgraph clawdapus-optional-integration"
1567
+ };
1568
+ async function installClawdapusSkill(workspacePath, options) {
1569
+ return installSkillIntegration(
1570
+ workspacePath,
1571
+ CLAWDAPUS_INTEGRATION_PROVIDER,
1572
+ options
1573
+ );
1574
+ }
1575
+ async function fetchClawdapusSkillMarkdown(sourceUrl) {
1576
+ return fetchSkillMarkdownFromUrl(sourceUrl, CLAWDAPUS_INTEGRATION_PROVIDER.userAgent);
1577
+ }
1578
+
1579
+ // src/integration.ts
1580
+ var integration_exports = {};
1581
+ __export(integration_exports, {
1582
+ installIntegration: () => installIntegration,
1583
+ listIntegrations: () => listIntegrations
1584
+ });
1585
+ var INTEGRATIONS = {
1586
+ clawdapus: {
1587
+ provider: CLAWDAPUS_INTEGRATION_PROVIDER,
1588
+ description: "Infrastructure-layer governance skill import for AI agent containers.",
1589
+ install: installClawdapusSkill
1590
+ }
1591
+ };
1592
+ function listIntegrations() {
1593
+ return Object.values(INTEGRATIONS).map((integration) => ({
1594
+ id: integration.provider.id,
1595
+ description: integration.description,
1596
+ defaultTitle: integration.provider.defaultTitle,
1597
+ defaultSourceUrl: integration.provider.defaultSourceUrl
1598
+ }));
1599
+ }
1600
+ async function installIntegration(workspacePath, integrationId, options) {
1601
+ const integration = INTEGRATIONS[integrationId.trim().toLowerCase()];
1602
+ if (!integration) {
1603
+ throw new Error(
1604
+ `Unknown integration "${integrationId}". Supported integrations: ${supportedIntegrationList()}.`
1605
+ );
1606
+ }
1607
+ return integration.install(workspacePath, options);
1608
+ }
1609
+ function supportedIntegrationList() {
1610
+ return Object.keys(INTEGRATIONS).sort().join(", ");
1611
+ }
1612
+
1613
+ // src/diagnostics/index.ts
1614
+ var diagnostics_exports = {};
1615
+ __export(diagnostics_exports, {
1616
+ computeVaultStats: () => computeVaultStats,
1617
+ diagnoseVaultHealth: () => diagnoseVaultHealth,
1618
+ generateLedgerChangelog: () => generateLedgerChangelog,
1619
+ renderChangelogText: () => renderChangelogText,
1620
+ renderDoctorReport: () => renderDoctorReport,
1621
+ renderReplayText: () => renderReplayText,
1622
+ renderStatsReport: () => renderStatsReport,
1623
+ replayLedger: () => replayLedger,
1624
+ visualizeVaultGraph: () => visualizeVaultGraph
1625
+ });
1626
+
1627
+ // src/diagnostics/doctor.ts
1628
+ import fs7 from "fs";
1629
+ import path8 from "path";
1630
+ import YAML2 from "yaml";
1631
+
1632
+ // src/diagnostics/format.ts
1633
+ var ANSI = {
1634
+ reset: "\x1B[0m",
1635
+ dim: "\x1B[2m",
1636
+ red: "\x1B[31m",
1637
+ green: "\x1B[32m",
1638
+ yellow: "\x1B[33m",
1639
+ blue: "\x1B[34m",
1640
+ magenta: "\x1B[35m",
1641
+ cyan: "\x1B[36m",
1642
+ gray: "\x1B[90m"
1643
+ };
1644
+ function supportsColor(enabledByOption) {
1645
+ if (!enabledByOption) return false;
1646
+ if (process.env.NO_COLOR) return false;
1647
+ return process.stdout.isTTY === true;
1648
+ }
1649
+ function colorize(text, color, enabled) {
1650
+ if (!enabled) return text;
1651
+ if (!ANSI[color]) return text;
1652
+ return `${ANSI[color]}${text}${ANSI.reset}`;
1653
+ }
1654
+ function dim(text, enabled) {
1655
+ return colorize(text, "dim", enabled);
1656
+ }
1657
+ function parseDateToTimestamp(value, optionName) {
1658
+ const parsed = Date.parse(value);
1659
+ if (!Number.isFinite(parsed)) {
1660
+ throw new Error(`Invalid ${optionName} value "${value}". Expected an ISO-8601 date/time.`);
1661
+ }
1662
+ return parsed;
1663
+ }
1664
+ function parsePositiveInt(rawValue, fallback, optionName) {
1665
+ if (rawValue === void 0) return fallback;
1666
+ const parsed = Number.parseInt(String(rawValue), 10);
1667
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1668
+ throw new Error(`Invalid ${optionName} value "${rawValue}". Expected a positive integer.`);
1669
+ }
1670
+ return parsed;
1671
+ }
1672
+ function formatDurationHours(hours) {
1673
+ if (!Number.isFinite(hours) || hours < 0) return "0h";
1674
+ if (hours < 1) {
1675
+ const minutes = Math.round(hours * 60);
1676
+ return `${minutes}m`;
1677
+ }
1678
+ if (hours < 24) {
1679
+ return `${hours.toFixed(2)}h`;
1680
+ }
1681
+ return `${(hours / 24).toFixed(2)}d`;
1682
+ }
1683
+ function inferPrimitiveTypeFromPath(targetPath) {
1684
+ const normalized = String(targetPath).replace(/\\/g, "/");
1685
+ const segment = normalized.split("/")[0]?.trim();
1686
+ if (!segment) return null;
1687
+ if (!normalized.endsWith(".md")) return null;
1688
+ const singular = segment.endsWith("s") ? segment.slice(0, -1) : segment;
1689
+ return singular || null;
1690
+ }
1691
+
1692
+ // src/diagnostics/primitives.ts
1693
+ import path7 from "path";
1694
+ function loadPrimitiveInventory(workspacePath) {
1695
+ const registry = loadRegistry(workspacePath);
1696
+ const allPrimitives = queryPrimitives(workspacePath);
1697
+ const byPath = /* @__PURE__ */ new Map();
1698
+ const byType = /* @__PURE__ */ new Map();
1699
+ const slugToPaths = /* @__PURE__ */ new Map();
1700
+ const typeByDirectory = /* @__PURE__ */ new Map();
1701
+ const typeDefs = /* @__PURE__ */ new Map();
1702
+ for (const typeDef of Object.values(registry.types)) {
1703
+ typeByDirectory.set(typeDef.directory, typeDef.name);
1704
+ typeDefs.set(typeDef.name, typeDef);
1705
+ }
1706
+ const primitives = allPrimitives.map((instance) => {
1707
+ const typeDef = typeDefs.get(instance.type);
1708
+ const requiredFields = Object.entries(typeDef?.fields ?? {}).filter(([, fieldDef]) => fieldDef.required === true).map(([fieldName]) => fieldName);
1709
+ const presentCount = requiredFields.filter((fieldName) => hasRequiredValue(instance.fields[fieldName])).length;
1710
+ const frontmatterCompleteness = requiredFields.length === 0 ? 1 : presentCount / requiredFields.length;
1711
+ const slug = path7.basename(instance.path, ".md");
1712
+ return {
1713
+ ...instance,
1714
+ slug,
1715
+ requiredFields,
1716
+ frontmatterCompleteness
1717
+ };
1718
+ });
1719
+ for (const primitive of primitives) {
1720
+ byPath.set(primitive.path, primitive);
1721
+ const existingByType = byType.get(primitive.type) ?? [];
1722
+ existingByType.push(primitive);
1723
+ byType.set(primitive.type, existingByType);
1724
+ const existingBySlug = slugToPaths.get(primitive.slug) ?? [];
1725
+ existingBySlug.push(primitive.path);
1726
+ slugToPaths.set(primitive.slug, existingBySlug);
1727
+ }
1728
+ for (const list3 of byType.values()) {
1729
+ list3.sort((a, b) => a.path.localeCompare(b.path));
1730
+ }
1731
+ for (const [slug, pathsForSlug] of slugToPaths.entries()) {
1732
+ slugToPaths.set(slug, pathsForSlug.slice().sort((a, b) => a.localeCompare(b)));
1733
+ }
1734
+ return {
1735
+ registry,
1736
+ primitives: primitives.slice().sort((a, b) => a.path.localeCompare(b.path)),
1737
+ byPath,
1738
+ byType,
1739
+ slugToPaths,
1740
+ typeByDirectory,
1741
+ typeDefs
1742
+ };
1743
+ }
1744
+ function buildPrimitiveWikiGraph(workspacePath, inventoryInput) {
1745
+ const inventory = inventoryInput ?? loadPrimitiveInventory(workspacePath);
1746
+ const edgeSet = /* @__PURE__ */ new Set();
1747
+ const outgoing = /* @__PURE__ */ new Map();
1748
+ const incoming = /* @__PURE__ */ new Map();
1749
+ const missingLinks = [];
1750
+ const ambiguousLinks = [];
1751
+ for (const primitive of inventory.primitives) {
1752
+ if (!outgoing.has(primitive.path)) outgoing.set(primitive.path, /* @__PURE__ */ new Set());
1753
+ if (!incoming.has(primitive.path)) incoming.set(primitive.path, /* @__PURE__ */ new Set());
1754
+ for (const link of extractWikiLinks(primitive.body)) {
1755
+ const resolved = resolvePrimitiveWikiTarget(link.rawTarget, inventory);
1756
+ if (resolved.status === "resolved") {
1757
+ const key = `${primitive.path}=>${resolved.path}`;
1758
+ if (edgeSet.has(key)) continue;
1759
+ edgeSet.add(key);
1760
+ outgoing.get(primitive.path).add(resolved.path);
1761
+ if (!incoming.has(resolved.path)) incoming.set(resolved.path, /* @__PURE__ */ new Set());
1762
+ incoming.get(resolved.path).add(primitive.path);
1763
+ } else if (resolved.status === "missing") {
1764
+ missingLinks.push({
1765
+ from: primitive.path,
1766
+ token: link.token,
1767
+ rawTarget: link.rawTarget,
1768
+ normalizedTarget: resolved.normalizedTarget
1769
+ });
1770
+ } else if (resolved.status === "ambiguous") {
1771
+ ambiguousLinks.push({
1772
+ from: primitive.path,
1773
+ token: link.token,
1774
+ rawTarget: link.rawTarget,
1775
+ normalizedTarget: resolved.normalizedTarget,
1776
+ candidates: resolved.candidates
1777
+ });
1778
+ }
1779
+ }
1780
+ }
1781
+ const edges = [...edgeSet].map((key) => {
1782
+ const [from, to] = key.split("=>");
1783
+ return { from, to };
1784
+ }).sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
1785
+ const outgoingRecord = mapToSortedRecord(outgoing);
1786
+ const incomingRecord = mapToSortedRecord(incoming);
1787
+ const hubs = inventory.primitives.map((primitive) => ({
1788
+ path: primitive.path,
1789
+ degree: (outgoingRecord[primitive.path]?.length ?? 0) + (incomingRecord[primitive.path]?.length ?? 0)
1790
+ })).filter((entry) => entry.degree > 0).sort((a, b) => b.degree - a.degree || a.path.localeCompare(b.path));
1791
+ const orphanNodes = inventory.primitives.map((primitive) => primitive.path).filter((nodePath) => (outgoingRecord[nodePath]?.length ?? 0) === 0 && (incomingRecord[nodePath]?.length ?? 0) === 0).sort((a, b) => a.localeCompare(b));
1792
+ return {
1793
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1794
+ nodes: inventory.primitives.map((primitive) => primitive.path),
1795
+ edges,
1796
+ outgoing: outgoingRecord,
1797
+ incoming: incomingRecord,
1798
+ hubs,
1799
+ orphanNodes,
1800
+ missingLinks: missingLinks.slice().sort((a, b) => a.from.localeCompare(b.from) || a.token.localeCompare(b.token)),
1801
+ ambiguousLinks: ambiguousLinks.slice().sort((a, b) => a.from.localeCompare(b.from) || a.token.localeCompare(b.token))
1802
+ };
1803
+ }
1804
+ function extractWikiLinks(markdown) {
1805
+ const matches = markdown.matchAll(/\[\[([^[\]]+)\]\]/g);
1806
+ const links = [];
1807
+ for (const match of matches) {
1808
+ const token = match[0];
1809
+ const rawTarget = match[1]?.trim();
1810
+ if (!token || !rawTarget) continue;
1811
+ links.push({ token, rawTarget });
1812
+ }
1813
+ return links;
1814
+ }
1815
+ function resolvePrimitiveWikiTarget(rawTarget, inventory) {
1816
+ const primary = rawTarget.split("|")[0]?.split("#")[0]?.trim() ?? "";
1817
+ if (!primary) {
1818
+ return { status: "non-primitive", normalizedTarget: "" };
1819
+ }
1820
+ if (/^https?:\/\//i.test(primary)) {
1821
+ return { status: "external", normalizedTarget: primary };
1822
+ }
1823
+ const normalized = normalizeWikiTarget(primary);
1824
+ if (normalized.includes("/")) {
1825
+ if (inventory.byPath.has(normalized)) {
1826
+ return { status: "resolved", normalizedTarget: normalized, path: normalized };
1827
+ }
1828
+ const directory = normalized.split("/")[0];
1829
+ if (inventory.typeByDirectory.has(directory)) {
1830
+ return { status: "missing", normalizedTarget: normalized };
1831
+ }
1832
+ return { status: "non-primitive", normalizedTarget: normalized };
1833
+ }
1834
+ const slug = normalized.replace(/\.md$/i, "");
1835
+ const candidates = inventory.slugToPaths.get(slug) ?? [];
1836
+ if (candidates.length === 1) {
1837
+ return { status: "resolved", normalizedTarget: normalized, path: candidates[0] };
1838
+ }
1839
+ if (candidates.length > 1) {
1840
+ return { status: "ambiguous", normalizedTarget: normalized, candidates };
1841
+ }
1842
+ return { status: "missing", normalizedTarget: normalized };
1843
+ }
1844
+ function normalizeWikiTarget(value) {
1845
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
1846
+ if (!normalized) return normalized;
1847
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
1848
+ }
1849
+ function hasRequiredValue(value) {
1850
+ if (value === void 0 || value === null) return false;
1851
+ if (typeof value === "string") return value.trim().length > 0;
1852
+ return true;
1853
+ }
1854
+ function mapToSortedRecord(source) {
1855
+ const output = {};
1856
+ const sortedKeys = [...source.keys()].sort((a, b) => a.localeCompare(b));
1857
+ for (const key of sortedKeys) {
1858
+ output[key] = [...source.get(key) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
1859
+ }
1860
+ return output;
1861
+ }
1862
+
1863
+ // src/diagnostics/doctor.ts
1864
+ var DEFAULT_STALE_AFTER_MS = 60 * 60 * 1e3;
1865
+ var DOCTOR_ACTOR = "workgraph-doctor";
1866
+ function diagnoseVaultHealth(workspacePath, options = {}) {
1867
+ const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
1868
+ const fixEnabled = options.fix === true;
1869
+ const fixActor = options.actor ?? DOCTOR_ACTOR;
1870
+ const fixSummary = {
1871
+ enabled: fixEnabled,
1872
+ orphanLinksRemoved: 0,
1873
+ staleClaimsReleased: 0,
1874
+ staleRunsCancelled: 0,
1875
+ filesUpdated: [],
1876
+ errors: []
1877
+ };
1878
+ let findings = collectDoctorFindings(workspacePath, staleAfterMs);
1879
+ if (fixEnabled) {
1880
+ const orphanFix = removeOrphanLinks(workspacePath, findings.orphanLinks);
1881
+ fixSummary.orphanLinksRemoved = orphanFix.removedLinks;
1882
+ fixSummary.filesUpdated.push(...orphanFix.filesUpdated);
1883
+ fixSummary.errors.push(...orphanFix.errors);
1884
+ const staleClaimFix = releaseStaleClaims(workspacePath, findings.staleClaims);
1885
+ fixSummary.staleClaimsReleased = staleClaimFix.released;
1886
+ fixSummary.errors.push(...staleClaimFix.errors);
1887
+ const staleRunFix = cancelStaleRuns(workspacePath, findings.staleRuns, fixActor);
1888
+ fixSummary.staleRunsCancelled = staleRunFix.cancelled;
1889
+ fixSummary.errors.push(...staleRunFix.errors);
1890
+ if (fixSummary.orphanLinksRemoved > 0) {
1891
+ refreshWikiLinkGraphIndex(workspacePath);
1892
+ }
1893
+ findings = collectDoctorFindings(workspacePath, staleAfterMs);
1894
+ }
1895
+ const warnings = findings.issues.filter((issue) => issue.severity === "warning").length;
1896
+ const errors = findings.issues.filter((issue) => issue.severity === "error").length;
1897
+ return {
1898
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1899
+ workspacePath,
1900
+ ok: errors === 0,
1901
+ summary: { errors, warnings },
1902
+ checks: findings.checks,
1903
+ issues: findings.issues,
1904
+ fixes: {
1905
+ ...fixSummary,
1906
+ filesUpdated: fixSummary.filesUpdated.slice().sort((a, b) => a.localeCompare(b))
1907
+ }
1908
+ };
1909
+ }
1910
+ function collectDoctorFindings(workspacePath, staleAfterMs) {
1911
+ const issues = [];
1912
+ const now = Date.now();
1913
+ let inventory = null;
1914
+ try {
1915
+ inventory = loadPrimitiveInventory(workspacePath);
1916
+ } catch (error) {
1917
+ issues.push({
1918
+ code: "primitive-inventory-load-failed",
1919
+ severity: "error",
1920
+ message: `Failed to load primitive inventory: ${errorMessage2(error)}`
1921
+ });
1922
+ }
1923
+ const primitiveGraph = inventory ? buildPrimitiveWikiGraph(workspacePath, inventory) : {
1924
+ missingLinks: []
1925
+ };
1926
+ for (const orphan of primitiveGraph.missingLinks) {
1927
+ issues.push({
1928
+ code: "orphan-wiki-link",
1929
+ severity: "warning",
1930
+ message: `Orphan wiki-link in ${orphan.from}: ${orphan.token} -> ${orphan.normalizedTarget}`,
1931
+ path: orphan.from,
1932
+ details: {
1933
+ token: orphan.token,
1934
+ target: orphan.normalizedTarget
1935
+ }
1936
+ });
1937
+ }
1938
+ if (inventory) {
1939
+ for (const primitive of inventory.primitives) {
1940
+ for (const requiredField of primitive.requiredFields) {
1941
+ if (isMissingRequiredValue(primitive.fields[requiredField])) {
1942
+ issues.push({
1943
+ code: "missing-required-field",
1944
+ severity: "error",
1945
+ message: `Missing required frontmatter field "${requiredField}" on ${primitive.path}`,
1946
+ path: primitive.path,
1947
+ details: {
1948
+ field: requiredField,
1949
+ type: primitive.type
1950
+ }
1951
+ });
1952
+ }
1953
+ }
1954
+ }
1955
+ for (const [slug, pathsForSlug] of inventory.slugToPaths.entries()) {
1956
+ if (pathsForSlug.length <= 1) continue;
1957
+ issues.push({
1958
+ code: "duplicate-slug",
1959
+ severity: "error",
1960
+ message: `Duplicate slug "${slug}" is used by: ${pathsForSlug.join(", ")}`,
1961
+ details: { slug, paths: pathsForSlug }
1962
+ });
1963
+ }
1964
+ }
1965
+ const staleClaims = collectStaleClaims(workspacePath, staleAfterMs, now);
1966
+ for (const staleClaim of staleClaims) {
1967
+ issues.push({
1968
+ code: "stale-claim",
1969
+ severity: "warning",
1970
+ message: `Stale claim on ${staleClaim.target} by ${staleClaim.owner} (${formatDurationHours(staleClaim.ageMs / 36e5)} old)`,
1971
+ path: staleClaim.target,
1972
+ details: {
1973
+ owner: staleClaim.owner,
1974
+ claimedAt: staleClaim.claimedAt
1975
+ }
1976
+ });
1977
+ }
1978
+ const staleRuns = collectStaleRuns(workspacePath, staleAfterMs, now);
1979
+ for (const staleRun of staleRuns) {
1980
+ issues.push({
1981
+ code: "stale-run",
1982
+ severity: "warning",
1983
+ message: `Run ${staleRun.id} is stuck in running for ${formatDurationHours(staleRun.ageMs / 36e5)}`,
1984
+ details: {
1985
+ runId: staleRun.id,
1986
+ actor: staleRun.actor,
1987
+ updatedAt: staleRun.updatedAt
1988
+ }
1989
+ });
1990
+ }
1991
+ const registryIssues = collectPrimitiveRegistryReferenceIssues(workspacePath, inventory);
1992
+ issues.push(...registryIssues);
1993
+ if (inventory) {
1994
+ const emptyDirectoryIssues = collectEmptyPrimitiveDirectoryIssues(workspacePath, inventory);
1995
+ issues.push(...emptyDirectoryIssues);
1996
+ }
1997
+ const checks = {
1998
+ orphanWikiLinks: countIssues(issues, "orphan-wiki-link"),
1999
+ staleClaims: countIssues(issues, "stale-claim"),
2000
+ staleRuns: countIssues(issues, "stale-run"),
2001
+ missingRequiredFields: countIssues(issues, "missing-required-field"),
2002
+ brokenPrimitiveRegistryReferences: countIssues(issues, "broken-primitive-registry-reference"),
2003
+ emptyPrimitiveDirectories: countIssues(issues, "empty-primitive-directory"),
2004
+ duplicateSlugs: countIssues(issues, "duplicate-slug")
2005
+ };
2006
+ return {
2007
+ issues: issues.sort((a, b) => severityRank(a.severity) - severityRank(b.severity) || a.code.localeCompare(b.code)),
2008
+ checks,
2009
+ orphanLinks: primitiveGraph.missingLinks,
2010
+ staleClaims,
2011
+ staleRuns
2012
+ };
2013
+ }
2014
+ function collectStaleClaims(workspacePath, staleAfterMs, now) {
2015
+ const staleClaims = [];
2016
+ const claims = allClaims(workspacePath);
2017
+ for (const [target, owner] of claims.entries()) {
2018
+ const history = historyOf(workspacePath, target);
2019
+ const lastClaim = history.slice().reverse().find((entry) => entry.op === "claim");
2020
+ if (!lastClaim) continue;
2021
+ const claimTs = Date.parse(lastClaim.ts);
2022
+ if (!Number.isFinite(claimTs)) continue;
2023
+ const ageMs = now - claimTs;
2024
+ if (ageMs <= staleAfterMs) continue;
2025
+ staleClaims.push({
2026
+ target,
2027
+ owner,
2028
+ claimedAt: lastClaim.ts,
2029
+ ageMs
2030
+ });
2031
+ }
2032
+ return staleClaims.sort((a, b) => b.ageMs - a.ageMs || a.target.localeCompare(b.target));
2033
+ }
2034
+ function collectStaleRuns(workspacePath, staleAfterMs, now) {
2035
+ const runs = readDispatchRunsSnapshot(workspacePath).filter((run) => run.status === "running");
2036
+ const staleRuns = [];
2037
+ for (const run of runs) {
2038
+ const updatedTs = Date.parse(run.updatedAt);
2039
+ if (!Number.isFinite(updatedTs)) continue;
2040
+ const ageMs = now - updatedTs;
2041
+ if (ageMs <= staleAfterMs) continue;
2042
+ staleRuns.push({
2043
+ id: run.id,
2044
+ actor: run.actor,
2045
+ updatedAt: run.updatedAt,
2046
+ ageMs
2047
+ });
2048
+ }
2049
+ return staleRuns.sort((a, b) => b.ageMs - a.ageMs || a.id.localeCompare(b.id));
2050
+ }
2051
+ function collectPrimitiveRegistryReferenceIssues(workspacePath, inventory) {
2052
+ const issues = [];
2053
+ const manifestPath = path8.join(workspacePath, ".workgraph", "primitive-registry.yaml");
2054
+ if (!fs7.existsSync(manifestPath)) {
2055
+ issues.push({
2056
+ code: "broken-primitive-registry-reference",
2057
+ severity: "error",
2058
+ message: "Missing .workgraph/primitive-registry.yaml",
2059
+ path: ".workgraph/primitive-registry.yaml"
2060
+ });
2061
+ return issues;
2062
+ }
2063
+ let parsed;
2064
+ try {
2065
+ parsed = YAML2.parse(fs7.readFileSync(manifestPath, "utf-8"));
2066
+ } catch (error) {
2067
+ issues.push({
2068
+ code: "broken-primitive-registry-reference",
2069
+ severity: "error",
2070
+ message: `Unable to parse primitive-registry.yaml: ${errorMessage2(error)}`,
2071
+ path: ".workgraph/primitive-registry.yaml"
2072
+ });
2073
+ return issues;
2074
+ }
2075
+ const primitives = parsed?.primitives;
2076
+ if (!Array.isArray(primitives)) {
2077
+ issues.push({
2078
+ code: "broken-primitive-registry-reference",
2079
+ severity: "error",
2080
+ message: 'primitive-registry.yaml is missing a "primitives" array.',
2081
+ path: ".workgraph/primitive-registry.yaml"
2082
+ });
2083
+ return issues;
2084
+ }
2085
+ const seenNames = /* @__PURE__ */ new Map();
2086
+ for (const primitiveEntry of primitives) {
2087
+ const name = String(primitiveEntry.name ?? "").trim();
2088
+ const directory = String(primitiveEntry.directory ?? "").trim();
2089
+ if (!name || !directory) {
2090
+ issues.push({
2091
+ code: "broken-primitive-registry-reference",
2092
+ severity: "error",
2093
+ message: "primitive-registry.yaml contains an entry with missing name or directory.",
2094
+ path: ".workgraph/primitive-registry.yaml"
2095
+ });
2096
+ continue;
2097
+ }
2098
+ seenNames.set(name, (seenNames.get(name) ?? 0) + 1);
2099
+ const registryType = inventory?.typeDefs.get(name);
2100
+ if (!registryType) {
2101
+ issues.push({
2102
+ code: "broken-primitive-registry-reference",
2103
+ severity: "error",
2104
+ message: `primitive-registry.yaml references unknown primitive "${name}".`,
2105
+ path: ".workgraph/primitive-registry.yaml"
2106
+ });
2107
+ continue;
2108
+ }
2109
+ if (registryType.directory !== directory) {
2110
+ issues.push({
2111
+ code: "broken-primitive-registry-reference",
2112
+ severity: "error",
2113
+ message: `primitive-registry.yaml directory mismatch for "${name}": expected "${registryType.directory}", got "${directory}".`,
2114
+ path: ".workgraph/primitive-registry.yaml"
2115
+ });
2116
+ }
2117
+ if (!fs7.existsSync(path8.join(workspacePath, directory))) {
2118
+ issues.push({
2119
+ code: "broken-primitive-registry-reference",
2120
+ severity: "error",
2121
+ message: `primitive-registry.yaml references missing directory "${directory}/".`,
2122
+ path: ".workgraph/primitive-registry.yaml"
2123
+ });
2124
+ }
2125
+ }
2126
+ for (const [name, count] of seenNames.entries()) {
2127
+ if (count <= 1) continue;
2128
+ issues.push({
2129
+ code: "broken-primitive-registry-reference",
2130
+ severity: "error",
2131
+ message: `primitive-registry.yaml has duplicate entries for primitive "${name}".`,
2132
+ path: ".workgraph/primitive-registry.yaml"
2133
+ });
2134
+ }
2135
+ if (inventory) {
2136
+ const manifestNames = new Set(primitives.map((entry) => String(entry.name ?? "").trim()).filter(Boolean));
2137
+ for (const typeName of inventory.typeDefs.keys()) {
2138
+ if (manifestNames.has(typeName)) continue;
2139
+ issues.push({
2140
+ code: "broken-primitive-registry-reference",
2141
+ severity: "warning",
2142
+ message: `Registry type "${typeName}" is missing from primitive-registry.yaml.`,
2143
+ path: ".workgraph/primitive-registry.yaml"
2144
+ });
2145
+ }
2146
+ }
2147
+ return issues;
2148
+ }
2149
+ function collectEmptyPrimitiveDirectoryIssues(workspacePath, inventory) {
2150
+ const issues = [];
2151
+ for (const typeDef of inventory.typeDefs.values()) {
2152
+ const directoryPath = path8.join(workspacePath, typeDef.directory);
2153
+ if (!fs7.existsSync(directoryPath)) continue;
2154
+ const markdownCount = listMarkdownFilesRecursive(directoryPath).length;
2155
+ if (markdownCount > 0) continue;
2156
+ issues.push({
2157
+ code: "empty-primitive-directory",
2158
+ severity: "warning",
2159
+ message: `Primitive directory "${typeDef.directory}/" is empty.`,
2160
+ path: `${typeDef.directory}/`,
2161
+ details: {
2162
+ type: typeDef.name
2163
+ }
2164
+ });
2165
+ }
2166
+ return issues;
2167
+ }
2168
+ function removeOrphanLinks(workspacePath, orphanLinks) {
2169
+ const errors = [];
2170
+ const filesUpdated = [];
2171
+ if (orphanLinks.length === 0) {
2172
+ return { removedLinks: 0, filesUpdated, errors };
2173
+ }
2174
+ const tokensBySource = /* @__PURE__ */ new Map();
2175
+ for (const orphan of orphanLinks) {
2176
+ const tokenSet = tokensBySource.get(orphan.from) ?? /* @__PURE__ */ new Set();
2177
+ tokenSet.add(orphan.token);
2178
+ tokensBySource.set(orphan.from, tokenSet);
2179
+ }
2180
+ let removedLinks = 0;
2181
+ for (const [sourcePath, tokenSet] of tokensBySource.entries()) {
2182
+ const absPath = path8.join(workspacePath, sourcePath);
2183
+ if (!fs7.existsSync(absPath)) continue;
2184
+ try {
2185
+ const raw = fs7.readFileSync(absPath, "utf-8");
2186
+ let fileRemoved = 0;
2187
+ const updated = raw.replace(/\[\[([^[\]]+)\]\]/g, (token) => {
2188
+ if (!tokenSet.has(token)) return token;
2189
+ fileRemoved += 1;
2190
+ return "";
2191
+ });
2192
+ if (fileRemoved === 0) continue;
2193
+ fs7.writeFileSync(absPath, updated, "utf-8");
2194
+ removedLinks += fileRemoved;
2195
+ filesUpdated.push(sourcePath);
2196
+ } catch (error) {
2197
+ errors.push(`Failed to remove orphan links from ${sourcePath}: ${errorMessage2(error)}`);
2198
+ }
2199
+ }
2200
+ return {
2201
+ removedLinks,
2202
+ filesUpdated: filesUpdated.sort((a, b) => a.localeCompare(b)),
2203
+ errors
2204
+ };
2205
+ }
2206
+ function releaseStaleClaims(workspacePath, staleClaims) {
2207
+ const errors = [];
2208
+ let released = 0;
2209
+ for (const staleClaim of staleClaims) {
2210
+ try {
2211
+ release(
2212
+ workspacePath,
2213
+ staleClaim.target,
2214
+ staleClaim.owner,
2215
+ "Auto-release stale claim by workgraph doctor"
2216
+ );
2217
+ released += 1;
2218
+ } catch (error) {
2219
+ const fallbackActor = staleClaim.owner || DOCTOR_ACTOR;
2220
+ try {
2221
+ append(workspacePath, fallbackActor, "release", staleClaim.target, "thread", {
2222
+ reason: "Auto-release stale claim by workgraph doctor"
2223
+ });
2224
+ const existing = read(workspacePath, staleClaim.target);
2225
+ if (existing) {
2226
+ update(
2227
+ workspacePath,
2228
+ staleClaim.target,
2229
+ { status: "open", owner: null },
2230
+ void 0,
2231
+ fallbackActor
2232
+ );
2233
+ }
2234
+ released += 1;
2235
+ } catch (fallbackError) {
2236
+ errors.push(
2237
+ `Failed to release stale claim ${staleClaim.target}: ${errorMessage2(error)} / fallback: ${errorMessage2(fallbackError)}`
2238
+ );
2239
+ }
2240
+ }
2241
+ }
2242
+ return { released, errors };
2243
+ }
2244
+ function cancelStaleRuns(workspacePath, staleRuns, actor) {
2245
+ const errors = [];
2246
+ let cancelled = 0;
2247
+ for (const staleRun of staleRuns) {
2248
+ try {
2249
+ stop(workspacePath, staleRun.id, actor);
2250
+ cancelled += 1;
2251
+ } catch (error) {
2252
+ errors.push(`Failed to cancel stale run ${staleRun.id}: ${errorMessage2(error)}`);
2253
+ }
2254
+ }
2255
+ return { cancelled, errors };
2256
+ }
2257
+ function readDispatchRunsSnapshot(workspacePath) {
2258
+ const runsPath = path8.join(workspacePath, ".workgraph", "dispatch-runs.json");
2259
+ if (!fs7.existsSync(runsPath)) return [];
2260
+ try {
2261
+ const parsed = JSON.parse(fs7.readFileSync(runsPath, "utf-8"));
2262
+ return Array.isArray(parsed.runs) ? parsed.runs : [];
2263
+ } catch {
2264
+ return [];
2265
+ }
2266
+ }
2267
+ function listMarkdownFilesRecursive(rootDirectory) {
2268
+ const files = [];
2269
+ const stack = [rootDirectory];
2270
+ while (stack.length > 0) {
2271
+ const current = stack.pop();
2272
+ const entries = fs7.readdirSync(current, { withFileTypes: true });
2273
+ for (const entry of entries) {
2274
+ const absPath = path8.join(current, entry.name);
2275
+ if (entry.isDirectory()) {
2276
+ stack.push(absPath);
2277
+ continue;
2278
+ }
2279
+ if (entry.isFile() && entry.name.endsWith(".md")) {
2280
+ files.push(absPath);
2281
+ }
2282
+ }
2283
+ }
2284
+ return files;
2285
+ }
2286
+ function isMissingRequiredValue(value) {
2287
+ if (value === void 0 || value === null) return true;
2288
+ if (typeof value === "string") return value.trim().length === 0;
2289
+ return false;
2290
+ }
2291
+ function countIssues(issues, code) {
2292
+ return issues.filter((issue) => issue.code === code).length;
2293
+ }
2294
+ function severityRank(severity) {
2295
+ return severity === "error" ? 0 : 1;
2296
+ }
2297
+ function errorMessage2(error) {
2298
+ if (error instanceof Error) return error.message;
2299
+ return String(error);
2300
+ }
2301
+
2302
+ // src/diagnostics/replay.ts
2303
+ function replayLedger(workspacePath, options = {}) {
2304
+ const sinceTs = options.since ? parseDateToTimestamp(options.since, "--since") : null;
2305
+ const untilTs = options.until ? parseDateToTimestamp(options.until, "--until") : null;
2306
+ if (options.type && !isReplayTypeFilter(options.type)) {
2307
+ throw new Error(`Invalid --type "${options.type}". Expected create|update|transition.`);
2308
+ }
2309
+ const allEntries = readAll(workspacePath);
2310
+ const ordered = allEntries.map((entry, index) => ({ entry, index })).sort((a, b) => {
2311
+ const aTs = Date.parse(a.entry.ts);
2312
+ const bTs = Date.parse(b.entry.ts);
2313
+ const safeA = Number.isFinite(aTs) ? aTs : Number.MAX_SAFE_INTEGER;
2314
+ const safeB = Number.isFinite(bTs) ? bTs : Number.MAX_SAFE_INTEGER;
2315
+ return safeA - safeB || a.index - b.index;
2316
+ }).map((item) => item.entry);
2317
+ const events = ordered.filter((entry) => matchesReplayFilters(entry, options, sinceTs, untilTs)).map((entry) => mapReplayEvent(entry));
2318
+ return {
2319
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2320
+ workspacePath,
2321
+ filters: {
2322
+ ...options.type ? { type: options.type } : {},
2323
+ ...options.actor ? { actor: options.actor } : {},
2324
+ ...options.primitive ? { primitive: options.primitive } : {},
2325
+ ...options.since ? { since: options.since } : {},
2326
+ ...options.until ? { until: options.until } : {}
2327
+ },
2328
+ totalEvents: allEntries.length,
2329
+ events
2330
+ };
2331
+ }
2332
+ function renderReplayText(report, options = {}) {
2333
+ if (report.events.length === 0) {
2334
+ return ["No ledger events matched the provided filters."];
2335
+ }
2336
+ const colorEnabled = supportsColor(options.color !== false);
2337
+ const lines = [];
2338
+ for (const event of report.events) {
2339
+ const categoryColor = event.category === "create" ? "green" : event.category === "update" ? "yellow" : "cyan";
2340
+ const categoryTag = colorize(event.category.toUpperCase().padEnd(10, " "), categoryColor, colorEnabled);
2341
+ const ts = dim(event.ts, colorEnabled);
2342
+ lines.push(`${ts} ${categoryTag} ${event.op.padEnd(8, " ")} ${event.actor} -> ${event.target}`);
2343
+ if (event.diff) {
2344
+ if (event.diff.changedFields.length > 0) {
2345
+ lines.push(` ${dim("\u0394 changed", colorEnabled)}: ${event.diff.changedFields.join(", ")}`);
2346
+ }
2347
+ if (event.diff.statusTransition) {
2348
+ lines.push(
2349
+ ` ${dim("\u0394 status", colorEnabled)}: ${String(event.diff.statusTransition.from)} -> ${String(event.diff.statusTransition.to)}`
2350
+ );
2351
+ }
2352
+ }
2353
+ }
2354
+ return lines;
2355
+ }
2356
+ function mapReplayEvent(entry) {
2357
+ const category = categoryForOp(entry.op);
2358
+ const diff = entry.op === "update" ? summarizeUpdateDiff(entry) : void 0;
2359
+ return {
2360
+ ts: entry.ts,
2361
+ actor: entry.actor,
2362
+ op: entry.op,
2363
+ target: entry.target,
2364
+ primitiveType: entry.type,
2365
+ category,
2366
+ ...diff ? { diff } : {}
2367
+ };
2368
+ }
2369
+ function summarizeUpdateDiff(entry) {
2370
+ const changed = Array.isArray(entry.data?.changed) ? entry.data?.changed.map((field) => String(field)) : [];
2371
+ const fromStatus = toNullableString(entry.data?.from_status);
2372
+ const toStatus = toNullableString(entry.data?.to_status);
2373
+ if (changed.length === 0 && fromStatus === void 0 && toStatus === void 0) {
2374
+ return void 0;
2375
+ }
2376
+ return {
2377
+ changedFields: changed,
2378
+ ...fromStatus !== void 0 || toStatus !== void 0 ? {
2379
+ statusTransition: {
2380
+ from: fromStatus ?? null,
2381
+ to: toStatus ?? null
2382
+ }
2383
+ } : {}
2384
+ };
2385
+ }
2386
+ function matchesReplayFilters(entry, options, sinceTs, untilTs) {
2387
+ if (options.type && categoryForOp(entry.op) !== options.type) return false;
2388
+ if (options.actor && entry.actor !== options.actor) return false;
2389
+ if (options.primitive) {
2390
+ const primitiveFilter = options.primitive.toLowerCase();
2391
+ const target = entry.target.toLowerCase();
2392
+ const type = String(entry.type ?? "").toLowerCase();
2393
+ if (!target.includes(primitiveFilter) && type !== primitiveFilter) return false;
2394
+ }
2395
+ if (sinceTs !== null || untilTs !== null) {
2396
+ const eventTs = Date.parse(entry.ts);
2397
+ if (!Number.isFinite(eventTs)) return false;
2398
+ if (sinceTs !== null && eventTs < sinceTs) return false;
2399
+ if (untilTs !== null && eventTs > untilTs) return false;
2400
+ }
2401
+ return true;
2402
+ }
2403
+ function categoryForOp(op) {
2404
+ if (op === "create") return "create";
2405
+ if (op === "update") return "update";
2406
+ return "transition";
2407
+ }
2408
+ function isReplayTypeFilter(value) {
2409
+ return value === "create" || value === "update" || value === "transition";
2410
+ }
2411
+ function toNullableString(value) {
2412
+ if (value === void 0) return void 0;
2413
+ if (value === null) return null;
2414
+ return String(value);
2415
+ }
2416
+
2417
+ // src/diagnostics/viz.ts
2418
+ import path9 from "path";
2419
+ var TYPE_COLORS = ["cyan", "magenta", "yellow", "green", "blue", "red"];
2420
+ function visualizeVaultGraph(workspacePath, options = {}) {
2421
+ const inventory = loadPrimitiveInventory(workspacePath);
2422
+ const primitiveGraph = buildPrimitiveWikiGraph(workspacePath, inventory);
2423
+ const depth = normalizeDepth(options.depth);
2424
+ const top = normalizeTop(options.top);
2425
+ const colorEnabled = supportsColor(options.color !== false);
2426
+ const typeColorMap = buildTypeColorMap(inventory);
2427
+ const labelForNode = (nodePath) => {
2428
+ const primitive = inventory.byPath.get(nodePath);
2429
+ const typeName = primitive?.type ?? "unknown";
2430
+ const base = `${nodePath} [${typeName}]`;
2431
+ const typeColor = typeColorMap.get(typeName) ?? "gray";
2432
+ return colorize(base, typeColor, colorEnabled);
2433
+ };
2434
+ let rendered = "";
2435
+ let focusPath;
2436
+ if (options.focus) {
2437
+ focusPath = resolveFocusPath(options.focus, inventory);
2438
+ rendered = renderFocusedGraph(focusPath, primitiveGraph, depth, labelForNode, colorEnabled);
2439
+ } else {
2440
+ rendered = renderTopHubGraph(primitiveGraph, depth, top, labelForNode, colorEnabled);
2441
+ }
2442
+ return {
2443
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2444
+ workspacePath,
2445
+ nodeCount: primitiveGraph.nodes.length,
2446
+ edgeCount: primitiveGraph.edges.length,
2447
+ hubs: primitiveGraph.hubs,
2448
+ ...focusPath ? { focus: focusPath } : {},
2449
+ rendered
2450
+ };
2451
+ }
2452
+ function renderFocusedGraph(focusPath, graph, depth, labelForNode, colorEnabled) {
2453
+ const outgoing = graph.outgoing[focusPath] ?? [];
2454
+ const incoming = graph.incoming[focusPath] ?? [];
2455
+ const lines = [];
2456
+ lines.push(labelForNode(focusPath));
2457
+ const hasOutgoing = outgoing.length > 0;
2458
+ const hasIncoming = incoming.length > 0;
2459
+ if (!hasOutgoing && !hasIncoming) {
2460
+ lines.push(`\u2514\u2500 ${dim("(no links)", colorEnabled)}`);
2461
+ return lines.join("\n");
2462
+ }
2463
+ const sections = [];
2464
+ if (outgoing.length > 0) {
2465
+ sections.push({ title: "Outgoing", neighbors: outgoing, map: graph.outgoing, arrow: "\u25B6" });
2466
+ }
2467
+ if (incoming.length > 0) {
2468
+ sections.push({ title: "Incoming", neighbors: incoming, map: graph.incoming, arrow: "\u25C0" });
2469
+ }
2470
+ sections.forEach((section, index) => {
2471
+ const isLastSection = index === sections.length - 1;
2472
+ lines.push(`${isLastSection ? "\u2514" : "\u251C"}\u2500 ${section.title}`);
2473
+ renderNeighbors({
2474
+ lines,
2475
+ map: section.map,
2476
+ neighbors: section.neighbors,
2477
+ depthRemaining: depth,
2478
+ prefix: isLastSection ? " " : "\u2502 ",
2479
+ arrow: section.arrow,
2480
+ labelForNode,
2481
+ colorEnabled,
2482
+ ancestors: /* @__PURE__ */ new Set([focusPath])
2483
+ });
2484
+ });
2485
+ return lines.join("\n");
2486
+ }
2487
+ function renderTopHubGraph(graph, depth, top, labelForNode, colorEnabled) {
2488
+ const lines = [];
2489
+ const hubs = graph.hubs.slice(0, top);
2490
+ const roots = graph.nodes.length > top ? hubs.map((hub) => hub.path) : graph.nodes.slice().sort((a, b) => a.localeCompare(b));
2491
+ const isTruncated = graph.nodes.length > top;
2492
+ roots.forEach((root, rootIndex) => {
2493
+ lines.push(labelForNode(root));
2494
+ const neighbors = graph.outgoing[root] ?? [];
2495
+ if (neighbors.length === 0) {
2496
+ lines.push(`\u2514\u2500 ${dim("(no outgoing links)", colorEnabled)}`);
2497
+ } else {
2498
+ renderNeighbors({
2499
+ lines,
2500
+ map: graph.outgoing,
2501
+ neighbors,
2502
+ depthRemaining: depth,
2503
+ prefix: "",
2504
+ arrow: "\u25B6",
2505
+ labelForNode,
2506
+ colorEnabled,
2507
+ ancestors: /* @__PURE__ */ new Set([root])
2508
+ });
2509
+ }
2510
+ if (rootIndex !== roots.length - 1) {
2511
+ lines.push("");
2512
+ }
2513
+ });
2514
+ if (isTruncated) {
2515
+ lines.push("");
2516
+ lines.push(dim(`Showing top ${roots.length} most-connected nodes of ${graph.nodes.length}.`, colorEnabled));
2517
+ }
2518
+ return lines.join("\n");
2519
+ }
2520
+ function renderNeighbors(params) {
2521
+ if (params.depthRemaining <= 0) return;
2522
+ const sortedNeighbors = params.neighbors.slice().sort((a, b) => a.localeCompare(b));
2523
+ sortedNeighbors.forEach((neighbor, index) => {
2524
+ const isLast = index === sortedNeighbors.length - 1;
2525
+ const branch = isLast ? "\u2514" : "\u251C";
2526
+ const cycle = params.ancestors.has(neighbor);
2527
+ const cycleTag = cycle ? ` ${dim("(cycle)", params.colorEnabled)}` : "";
2528
+ params.lines.push(`${params.prefix}${branch}\u2500${params.arrow} ${params.labelForNode(neighbor)}${cycleTag}`);
2529
+ if (cycle || params.depthRemaining <= 1) return;
2530
+ const nextPrefix = `${params.prefix}${isLast ? " " : "\u2502 "}`;
2531
+ const nextAncestors = new Set(params.ancestors);
2532
+ nextAncestors.add(neighbor);
2533
+ renderNeighbors({
2534
+ ...params,
2535
+ neighbors: params.map[neighbor] ?? [],
2536
+ depthRemaining: params.depthRemaining - 1,
2537
+ prefix: nextPrefix,
2538
+ ancestors: nextAncestors
2539
+ });
2540
+ });
2541
+ }
2542
+ function normalizeDepth(depth) {
2543
+ if (depth === void 0) return 2;
2544
+ return parsePositiveInt(String(depth), 2, "--depth");
2545
+ }
2546
+ function normalizeTop(top) {
2547
+ if (top === void 0) return 10;
2548
+ return parsePositiveInt(String(top), 10, "--top");
2549
+ }
2550
+ function resolveFocusPath(focusInput, inventory) {
2551
+ const normalized = focusInput.replace(/\\/g, "/").trim();
2552
+ if (!normalized) {
2553
+ throw new Error("--focus value cannot be empty.");
2554
+ }
2555
+ const directCandidate = normalized.endsWith(".md") ? normalized : `${normalized}.md`;
2556
+ if (inventory.byPath.has(normalized)) return normalized;
2557
+ if (inventory.byPath.has(directCandidate)) return directCandidate;
2558
+ const slug = path9.basename(normalized, ".md");
2559
+ const candidates = inventory.slugToPaths.get(slug) ?? [];
2560
+ if (candidates.length === 1) return candidates[0];
2561
+ if (candidates.length > 1) {
2562
+ throw new Error(`Focus slug "${focusInput}" is ambiguous: ${candidates.join(", ")}`);
2563
+ }
2564
+ throw new Error(`Focus node "${focusInput}" was not found.`);
2565
+ }
2566
+ function buildTypeColorMap(inventory) {
2567
+ const map = /* @__PURE__ */ new Map();
2568
+ const typeNames = [...inventory.typeDefs.keys()].sort((a, b) => a.localeCompare(b));
2569
+ typeNames.forEach((typeName, index) => {
2570
+ map.set(typeName, TYPE_COLORS[index % TYPE_COLORS.length]);
2571
+ });
2572
+ return map;
2573
+ }
2574
+
2575
+ // src/diagnostics/stats.ts
2576
+ function computeVaultStats(workspacePath) {
2577
+ const inventory = loadPrimitiveInventory(workspacePath);
2578
+ const primitiveGraph = buildPrimitiveWikiGraph(workspacePath, inventory);
2579
+ const byType = buildPrimitiveCountByType(inventory.primitives);
2580
+ const frontmatter = computeFrontmatterStats(inventory.primitives);
2581
+ const allEntries = readAll(workspacePath);
2582
+ const eventRate = computeEventRatePerDay(allEntries);
2583
+ const threadVelocity = computeThreadVelocity(workspacePath, inventory.byType.get("thread") ?? []);
2584
+ const nodeCount = primitiveGraph.nodes.length;
2585
+ const edgeCount = primitiveGraph.edges.length;
2586
+ const possibleDirectedEdges = nodeCount > 1 ? nodeCount * (nodeCount - 1) : 0;
2587
+ return {
2588
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2589
+ workspacePath,
2590
+ primitives: {
2591
+ total: inventory.primitives.length,
2592
+ byType
2593
+ },
2594
+ links: {
2595
+ total: edgeCount,
2596
+ wikiLinkDensity: nodeCount > 0 ? edgeCount / nodeCount : 0,
2597
+ graphDensityRatio: possibleDirectedEdges > 0 ? edgeCount / possibleDirectedEdges : 0,
2598
+ orphanCount: primitiveGraph.missingLinks.length,
2599
+ orphanNodeCount: primitiveGraph.orphanNodes.length,
2600
+ mostConnectedNodes: primitiveGraph.hubs.slice(0, 10)
2601
+ },
2602
+ frontmatter,
2603
+ ledger: {
2604
+ totalEvents: allEntries.length,
2605
+ eventRatePerDay: eventRate
2606
+ },
2607
+ threads: threadVelocity
2608
+ };
2609
+ }
2610
+ function buildPrimitiveCountByType(primitives) {
2611
+ const byType = primitives.reduce((acc, primitive) => {
2612
+ acc[primitive.type] = (acc[primitive.type] ?? 0) + 1;
2613
+ return acc;
2614
+ }, {});
2615
+ return Object.keys(byType).sort((a, b) => a.localeCompare(b)).reduce((acc, typeName) => {
2616
+ acc[typeName] = byType[typeName];
2617
+ return acc;
2618
+ }, {});
2619
+ }
2620
+ function computeFrontmatterStats(primitives) {
2621
+ if (primitives.length === 0) {
2622
+ return {
2623
+ averageCompleteness: 1,
2624
+ byType: {}
2625
+ };
2626
+ }
2627
+ const totalsByType = /* @__PURE__ */ new Map();
2628
+ let sum = 0;
2629
+ for (const primitive of primitives) {
2630
+ sum += primitive.frontmatterCompleteness;
2631
+ const current = totalsByType.get(primitive.type) ?? { sum: 0, count: 0 };
2632
+ current.sum += primitive.frontmatterCompleteness;
2633
+ current.count += 1;
2634
+ totalsByType.set(primitive.type, current);
2635
+ }
2636
+ const byType = [...totalsByType.entries()].sort((a, b) => a[0].localeCompare(b[0])).reduce((acc, [typeName, stats]) => {
2637
+ acc[typeName] = stats.count > 0 ? stats.sum / stats.count : 1;
2638
+ return acc;
2639
+ }, {});
2640
+ return {
2641
+ averageCompleteness: sum / primitives.length,
2642
+ byType
2643
+ };
2644
+ }
2645
+ function computeEventRatePerDay(entries) {
2646
+ if (entries.length === 0) {
2647
+ return {
2648
+ average: 0,
2649
+ byDay: []
2650
+ };
2651
+ }
2652
+ const byDay = /* @__PURE__ */ new Map();
2653
+ for (const entry of entries) {
2654
+ const day = entry.ts.slice(0, 10);
2655
+ if (!day) continue;
2656
+ byDay.set(day, (byDay.get(day) ?? 0) + 1);
2657
+ }
2658
+ const dayCounts = [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, count]) => ({ day, count }));
2659
+ const totalCount = dayCounts.reduce((acc, item) => acc + item.count, 0);
2660
+ return {
2661
+ average: dayCounts.length > 0 ? totalCount / dayCounts.length : 0,
2662
+ byDay: dayCounts
2663
+ };
2664
+ }
2665
+ function computeThreadVelocity(workspacePath, threads) {
2666
+ const durationsHours = [];
2667
+ for (const thread of threads) {
2668
+ const history = historyOf(workspacePath, thread.path);
2669
+ const createEntry = history.find((entry) => entry.op === "create");
2670
+ const completionEntry = history.find(
2671
+ (entry) => entry.op === "done" || entry.op === "update" && String(entry.data?.to_status ?? "") === "done"
2672
+ );
2673
+ if (!createEntry || !completionEntry) continue;
2674
+ const start = Date.parse(createEntry.ts);
2675
+ const end = Date.parse(completionEntry.ts);
2676
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) continue;
2677
+ durationsHours.push((end - start) / (1e3 * 60 * 60));
2678
+ }
2679
+ const sum = durationsHours.reduce((acc, value) => acc + value, 0);
2680
+ return {
2681
+ completedCount: durationsHours.length,
2682
+ averageOpenToDoneHours: durationsHours.length > 0 ? sum / durationsHours.length : 0
2683
+ };
2684
+ }
2685
+
2686
+ // src/diagnostics/changelog.ts
2687
+ function generateLedgerChangelog(workspacePath, options) {
2688
+ const sinceTs = parseDateToTimestamp(options.since, "--since");
2689
+ const untilTs = options.until ? parseDateToTimestamp(options.until, "--until") : null;
2690
+ const allEntries = readAll(workspacePath);
2691
+ const grouped = /* @__PURE__ */ new Map();
2692
+ let matchedEventCount = 0;
2693
+ for (const entry of allEntries) {
2694
+ const eventTs = Date.parse(entry.ts);
2695
+ if (!Number.isFinite(eventTs)) continue;
2696
+ if (eventTs < sinceTs) continue;
2697
+ if (untilTs !== null && eventTs > untilTs) continue;
2698
+ const action = categorizeEntry(entry);
2699
+ if (!action) continue;
2700
+ matchedEventCount += 1;
2701
+ const day = entry.ts.slice(0, 10);
2702
+ const primitiveType = entry.type ?? inferPrimitiveTypeFromPath(entry.target) ?? "unknown";
2703
+ const dayGroup = grouped.get(day) ?? {
2704
+ created: /* @__PURE__ */ new Map(),
2705
+ updated: /* @__PURE__ */ new Map(),
2706
+ completed: /* @__PURE__ */ new Map()
2707
+ };
2708
+ const byType = dayGroup[action];
2709
+ const items = byType.get(primitiveType) ?? [];
2710
+ items.push({
2711
+ ts: entry.ts,
2712
+ actor: entry.actor,
2713
+ op: entry.op,
2714
+ target: entry.target,
2715
+ summary: buildEntrySummary(entry)
2716
+ });
2717
+ byType.set(primitiveType, items);
2718
+ grouped.set(day, dayGroup);
2719
+ }
2720
+ const days = [...grouped.entries()].sort((a, b) => b[0].localeCompare(a[0])).map(([day, dayGroup]) => ({
2721
+ day,
2722
+ created: normalizeTypeGroups(dayGroup.created),
2723
+ updated: normalizeTypeGroups(dayGroup.updated),
2724
+ completed: normalizeTypeGroups(dayGroup.completed)
2725
+ }));
2726
+ return {
2727
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2728
+ workspacePath,
2729
+ since: options.since,
2730
+ ...options.until ? { until: options.until } : {},
2731
+ totalEvents: matchedEventCount,
2732
+ days
2733
+ };
2734
+ }
2735
+ function renderChangelogText(report) {
2736
+ if (report.days.length === 0) {
2737
+ return [`No changelog activity found since ${report.since}.`];
2738
+ }
2739
+ const lines = [];
2740
+ lines.push(`Changelog since ${report.since}${report.until ? ` until ${report.until}` : ""}`);
2741
+ lines.push("");
2742
+ for (const day of report.days) {
2743
+ lines.push(`${day.day}`);
2744
+ lines.push(...renderActionGroup("Created", day.created));
2745
+ lines.push(...renderActionGroup("Updated", day.updated));
2746
+ lines.push(...renderActionGroup("Completed", day.completed));
2747
+ lines.push("");
2748
+ }
2749
+ return lines;
2750
+ }
2751
+ function renderActionGroup(title, groups) {
2752
+ if (groups.length === 0) {
2753
+ return [` ${title}: none`];
2754
+ }
2755
+ const lines = [` ${title}:`];
2756
+ for (const group of groups) {
2757
+ lines.push(` - ${group.primitiveType}:`);
2758
+ for (const item of group.items) {
2759
+ const time = item.ts.slice(11, 19);
2760
+ const summarySuffix = item.summary ? ` \u2014 ${item.summary}` : "";
2761
+ lines.push(` - [${time}] ${item.target} (${item.actor})${summarySuffix}`);
2762
+ }
2763
+ }
2764
+ return lines;
2765
+ }
2766
+ function normalizeTypeGroups(byType) {
2767
+ return [...byType.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([primitiveType, items]) => ({
2768
+ primitiveType,
2769
+ items: items.slice().sort((a, b) => a.ts.localeCompare(b.ts) || a.target.localeCompare(b.target))
2770
+ }));
2771
+ }
2772
+ function categorizeEntry(entry) {
2773
+ if (entry.op === "create") return "created";
2774
+ if (entry.op === "done") return "completed";
2775
+ if (entry.op === "update") {
2776
+ const toStatus = String(entry.data?.to_status ?? "");
2777
+ if (isCompletedStatus(toStatus)) return "completed";
2778
+ return "updated";
2779
+ }
2780
+ return null;
2781
+ }
2782
+ function isCompletedStatus(status) {
2783
+ const normalized = status.toLowerCase();
2784
+ return normalized === "done" || normalized === "succeeded" || normalized === "completed" || normalized === "closed";
2785
+ }
2786
+ function buildEntrySummary(entry) {
2787
+ if (entry.op === "create") {
2788
+ return entry.data?.title ? `title: ${String(entry.data.title)}` : void 0;
2789
+ }
2790
+ if (entry.op === "update") {
2791
+ const changed = Array.isArray(entry.data?.changed) ? entry.data?.changed.map((value) => String(value)) : [];
2792
+ if (changed.length > 0) return `changed: ${changed.join(", ")}`;
2793
+ if (entry.data?.to_status) return `status: ${String(entry.data?.to_status)}`;
2794
+ return void 0;
2795
+ }
2796
+ if (entry.op === "done" && entry.data?.output) {
2797
+ return `output: ${String(entry.data.output)}`;
2798
+ }
2799
+ return void 0;
2800
+ }
2801
+
2802
+ // src/diagnostics/render.ts
2803
+ function renderDoctorReport(report) {
2804
+ const lines = [];
2805
+ lines.push(`Vault health: ${report.ok ? "OK" : "NOT OK"}`);
2806
+ lines.push(`Errors: ${report.summary.errors} Warnings: ${report.summary.warnings}`);
2807
+ lines.push(
2808
+ `Checks: orphan_links=${report.checks.orphanWikiLinks} stale_claims=${report.checks.staleClaims} stale_runs=${report.checks.staleRuns} missing_required=${report.checks.missingRequiredFields} broken_registry_refs=${report.checks.brokenPrimitiveRegistryReferences} empty_dirs=${report.checks.emptyPrimitiveDirectories} duplicate_slugs=${report.checks.duplicateSlugs}`
2809
+ );
2810
+ if (report.fixes.enabled) {
2811
+ lines.push(
2812
+ `Auto-fix: removed_orphan_links=${report.fixes.orphanLinksRemoved} released_stale_claims=${report.fixes.staleClaimsReleased} cancelled_stale_runs=${report.fixes.staleRunsCancelled}`
2813
+ );
2814
+ if (report.fixes.filesUpdated.length > 0) {
2815
+ lines.push(`Updated files: ${report.fixes.filesUpdated.join(", ")}`);
2816
+ }
2817
+ if (report.fixes.errors.length > 0) {
2818
+ lines.push(`Fix errors: ${report.fixes.errors.length}`);
2819
+ for (const error of report.fixes.errors) {
2820
+ lines.push(` - ${error}`);
2821
+ }
2822
+ }
2823
+ }
2824
+ if (report.issues.length === 0) {
2825
+ lines.push("No issues detected.");
2826
+ return lines;
2827
+ }
2828
+ lines.push("Issues:");
2829
+ for (const issue of report.issues) {
2830
+ const pathSuffix = issue.path ? ` (${issue.path})` : "";
2831
+ lines.push(`- [${issue.severity.toUpperCase()}] ${issue.code}${pathSuffix}: ${issue.message}`);
2832
+ }
2833
+ return lines;
2834
+ }
2835
+ function renderStatsReport(stats) {
2836
+ const lines = [];
2837
+ lines.push(`Primitives: total=${stats.primitives.total}`);
2838
+ lines.push(
2839
+ `By type: ${Object.entries(stats.primitives.byType).map(([type, count]) => `${type}=${count}`).join(", ") || "none"}`
2840
+ );
2841
+ lines.push(
2842
+ `Links: total=${stats.links.total} density=${stats.links.wikiLinkDensity.toFixed(2)} orphan_links=${stats.links.orphanCount} orphan_nodes=${stats.links.orphanNodeCount}`
2843
+ );
2844
+ lines.push(
2845
+ `Top hubs: ${stats.links.mostConnectedNodes.slice(0, 5).map((hub) => `${hub.path}(${hub.degree})`).join(", ") || "none"}`
2846
+ );
2847
+ lines.push(
2848
+ `Frontmatter completeness: avg=${(stats.frontmatter.averageCompleteness * 100).toFixed(1)}%`
2849
+ );
2850
+ lines.push(
2851
+ `Ledger event rate/day: avg=${stats.ledger.eventRatePerDay.average.toFixed(2)} over ${stats.ledger.eventRatePerDay.byDay.length} day(s)`
2852
+ );
2853
+ lines.push(
2854
+ `Thread velocity: completed=${stats.threads.completedCount} avg_open_to_done=${formatDurationHours(stats.threads.averageOpenToDoneHours)}`
2855
+ );
2856
+ return lines;
2857
+ }
2858
+
2859
+ // src/autonomy-daemon.ts
2860
+ var autonomy_daemon_exports = {};
2861
+ __export(autonomy_daemon_exports, {
2862
+ readAutonomyDaemonStatus: () => readAutonomyDaemonStatus,
2863
+ startAutonomyDaemon: () => startAutonomyDaemon,
2864
+ stopAutonomyDaemon: () => stopAutonomyDaemon
2865
+ });
2866
+ import fs8 from "fs";
2867
+ import path10 from "path";
2868
+ import { spawn } from "child_process";
2869
+ var DAEMON_DIR = ".workgraph/daemon";
2870
+ var AUTONOMY_PID_FILE = "autonomy.pid";
2871
+ var AUTONOMY_HEARTBEAT_FILE = "autonomy-heartbeat.json";
2872
+ var AUTONOMY_LOG_FILE = "autonomy.log";
2873
+ var AUTONOMY_META_FILE = "autonomy-process.json";
2874
+ function startAutonomyDaemon(workspacePath, input) {
2875
+ const daemonDir = ensureDaemonDir(workspacePath);
2876
+ const pidPath = path10.join(daemonDir, AUTONOMY_PID_FILE);
2877
+ const heartbeatPath = input.heartbeatPath ? resolvePathWithinWorkspace4(workspacePath, input.heartbeatPath) : path10.join(daemonDir, AUTONOMY_HEARTBEAT_FILE);
2878
+ const logPath = input.logPath ? resolvePathWithinWorkspace4(workspacePath, input.logPath) : path10.join(daemonDir, AUTONOMY_LOG_FILE);
2879
+ const metaPath = path10.join(daemonDir, AUTONOMY_META_FILE);
2880
+ const existing = readAutonomyDaemonStatus(workspacePath, { cleanupStalePidFile: true });
2881
+ if (existing.running) {
2882
+ throw new Error(`Autonomy daemon already running (pid=${existing.pid}). Stop it before starting a new one.`);
2883
+ }
2884
+ const logFd = fs8.openSync(logPath, "a");
2885
+ const args = buildAutonomyDaemonArgs(workspacePath, input, heartbeatPath);
2886
+ const child = spawn(process.execPath, args, {
2887
+ detached: true,
2888
+ stdio: ["ignore", logFd, logFd],
2889
+ env: process.env
2890
+ });
2891
+ fs8.closeSync(logFd);
2892
+ child.unref();
2893
+ if (!child.pid) {
2894
+ throw new Error("Failed to start autonomy daemon: missing child process pid.");
2895
+ }
2896
+ fs8.writeFileSync(pidPath, `${child.pid}
2897
+ `, "utf-8");
2898
+ fs8.writeFileSync(metaPath, JSON.stringify({
2899
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2900
+ pid: child.pid,
2901
+ args,
2902
+ actor: input.actor,
2903
+ adapter: input.adapter ?? "cursor-cloud",
2904
+ logPath,
2905
+ heartbeatPath
2906
+ }, null, 2) + "\n", "utf-8");
2907
+ return readAutonomyDaemonStatus(workspacePath, { cleanupStalePidFile: true });
2908
+ }
2909
+ async function stopAutonomyDaemon(workspacePath, input = {}) {
2910
+ const status = readAutonomyDaemonStatus(workspacePath, { cleanupStalePidFile: false });
2911
+ if (!status.pid) {
2912
+ return {
2913
+ stopped: true,
2914
+ previouslyRunning: false,
2915
+ status: readAutonomyDaemonStatus(workspacePath, { cleanupStalePidFile: true })
2916
+ };
2917
+ }
2918
+ const pid = status.pid;
2919
+ const signal = input.signal ?? "SIGTERM";
2920
+ const timeoutMs = clampInt(input.timeoutMs, 5e3, 250, 6e4);
2921
+ const previouslyRunning = isProcessAlive(pid);
2922
+ if (previouslyRunning) {
2923
+ process.kill(pid, signal);
2924
+ }
2925
+ await waitForProcessExit(pid, timeoutMs);
2926
+ let stopped = !isProcessAlive(pid);
2927
+ if (!stopped && signal !== "SIGKILL") {
2928
+ process.kill(pid, "SIGKILL");
2929
+ await waitForProcessExit(pid, 1500);
2930
+ stopped = !isProcessAlive(pid);
2931
+ }
2932
+ const pidPath = path10.join(ensureDaemonDir(workspacePath), AUTONOMY_PID_FILE);
2933
+ if (stopped && fs8.existsSync(pidPath)) {
2934
+ fs8.rmSync(pidPath, { force: true });
2935
+ }
2936
+ return {
2937
+ stopped,
2938
+ previouslyRunning,
2939
+ pid,
2940
+ status: readAutonomyDaemonStatus(workspacePath, { cleanupStalePidFile: true })
2941
+ };
2942
+ }
2943
+ function readAutonomyDaemonStatus(workspacePath, options = {}) {
2944
+ const daemonDir = ensureDaemonDir(workspacePath);
2945
+ const pidPath = path10.join(daemonDir, AUTONOMY_PID_FILE);
2946
+ const meta = readDaemonMeta(path10.join(daemonDir, AUTONOMY_META_FILE));
2947
+ const logPath = meta?.logPath ? String(meta.logPath) : path10.join(daemonDir, AUTONOMY_LOG_FILE);
2948
+ const heartbeatPath = meta?.heartbeatPath ? String(meta.heartbeatPath) : path10.join(daemonDir, AUTONOMY_HEARTBEAT_FILE);
2949
+ const pid = readPid(pidPath);
2950
+ const running = pid ? isProcessAlive(pid) : false;
2951
+ if (!running && pid && options.cleanupStalePidFile !== false && fs8.existsSync(pidPath)) {
2952
+ fs8.rmSync(pidPath, { force: true });
2953
+ }
2954
+ return {
2955
+ running,
2956
+ pid: running ? pid : void 0,
2957
+ pidPath,
2958
+ logPath,
2959
+ heartbeatPath,
2960
+ heartbeat: readHeartbeat(heartbeatPath)
2961
+ };
2962
+ }
2963
+ function buildAutonomyDaemonArgs(workspacePath, input, heartbeatPath) {
2964
+ const args = [
2965
+ path10.resolve(input.cliEntrypointPath),
2966
+ "autonomy",
2967
+ "run",
2968
+ "-w",
2969
+ workspacePath,
2970
+ "--actor",
2971
+ input.actor,
2972
+ "--adapter",
2973
+ input.adapter ?? "cursor-cloud",
2974
+ "--watch",
2975
+ "--poll-ms",
2976
+ String(clampInt(input.pollMs, 2e3, 100, 6e4)),
2977
+ "--max-idle-cycles",
2978
+ String(clampInt(input.maxIdleCycles, 2, 1, 1e4)),
2979
+ "--max-steps",
2980
+ String(clampInt(input.maxSteps, 200, 1, 5e3)),
2981
+ "--step-delay-ms",
2982
+ String(clampInt(input.stepDelayMs, 25, 0, 5e3)),
2983
+ "--stale-claim-minutes",
2984
+ String(clampInt(input.staleClaimMinutes, 30, 1, 24 * 60)),
2985
+ "--heartbeat-file",
2986
+ heartbeatPath,
2987
+ "--json"
2988
+ ];
2989
+ if (typeof input.maxCycles === "number") {
2990
+ args.push("--max-cycles", String(clampInt(input.maxCycles, 1, 1, Number.MAX_SAFE_INTEGER)));
2991
+ }
2992
+ if (input.agents && input.agents.length > 0) {
2993
+ args.push("--agents", input.agents.join(","));
2994
+ }
2995
+ if (input.space) {
2996
+ args.push("--space", input.space);
2997
+ }
2998
+ if (input.executeTriggers === false) {
2999
+ args.push("--no-execute-triggers");
3000
+ }
3001
+ if (input.executeReadyThreads === false) {
3002
+ args.push("--no-execute-ready-threads");
3003
+ }
3004
+ return args;
3005
+ }
3006
+ function waitForProcessExit(pid, timeoutMs) {
3007
+ return new Promise((resolve) => {
3008
+ const startedAt = Date.now();
3009
+ const timer = setInterval(() => {
3010
+ if (!isProcessAlive(pid)) {
3011
+ clearInterval(timer);
3012
+ resolve();
3013
+ return;
3014
+ }
3015
+ if (Date.now() - startedAt >= timeoutMs) {
3016
+ clearInterval(timer);
3017
+ resolve();
3018
+ }
3019
+ }, 100);
3020
+ });
3021
+ }
3022
+ function ensureDaemonDir(workspacePath) {
3023
+ const daemonDir = path10.join(workspacePath, DAEMON_DIR);
3024
+ if (!fs8.existsSync(daemonDir)) fs8.mkdirSync(daemonDir, { recursive: true });
3025
+ return daemonDir;
3026
+ }
3027
+ function readPid(pidPath) {
3028
+ if (!fs8.existsSync(pidPath)) return void 0;
3029
+ const raw = fs8.readFileSync(pidPath, "utf-8").trim();
3030
+ if (!raw) return void 0;
3031
+ const parsed = Number(raw);
3032
+ if (!Number.isInteger(parsed) || parsed <= 0) return void 0;
3033
+ return parsed;
3034
+ }
3035
+ function readHeartbeat(heartbeatPath) {
3036
+ if (!fs8.existsSync(heartbeatPath)) return void 0;
3037
+ try {
3038
+ const parsed = JSON.parse(fs8.readFileSync(heartbeatPath, "utf-8"));
3039
+ if (!parsed || typeof parsed !== "object") return void 0;
3040
+ return parsed;
3041
+ } catch {
3042
+ return void 0;
3043
+ }
3044
+ }
3045
+ function readDaemonMeta(metaPath) {
3046
+ if (!fs8.existsSync(metaPath)) return void 0;
3047
+ try {
3048
+ const parsed = JSON.parse(fs8.readFileSync(metaPath, "utf-8"));
3049
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
3050
+ return parsed;
3051
+ } catch {
3052
+ return void 0;
3053
+ }
3054
+ }
3055
+ function isProcessAlive(pid) {
3056
+ if (isZombieProcess(pid)) return false;
3057
+ try {
3058
+ process.kill(pid, 0);
3059
+ return true;
3060
+ } catch {
3061
+ return false;
3062
+ }
3063
+ }
3064
+ function isZombieProcess(pid) {
3065
+ const statPath = `/proc/${pid}/stat`;
3066
+ if (!fs8.existsSync(statPath)) return false;
3067
+ try {
3068
+ const stat = fs8.readFileSync(statPath, "utf-8");
3069
+ const closingIdx = stat.indexOf(")");
3070
+ if (closingIdx === -1 || closingIdx + 2 >= stat.length) return false;
3071
+ const state = stat.slice(closingIdx + 2, closingIdx + 3);
3072
+ return state === "Z";
3073
+ } catch {
3074
+ return false;
3075
+ }
3076
+ }
3077
+ function resolvePathWithinWorkspace4(workspacePath, filePath) {
3078
+ const base = path10.resolve(workspacePath);
3079
+ const resolved = path10.resolve(base, filePath);
3080
+ if (!resolved.startsWith(base + path10.sep) && resolved !== base) {
3081
+ throw new Error(`Invalid path outside workspace: ${filePath}`);
3082
+ }
3083
+ return resolved;
3084
+ }
3085
+ function clampInt(value, fallback, min, max) {
3086
+ const raw = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
3087
+ return Math.min(max, Math.max(min, raw));
3088
+ }
3089
+
3090
+ export {
3091
+ bases_exports,
3092
+ workspace_exports,
3093
+ command_center_exports,
3094
+ skill_exports,
3095
+ lens_exports,
3096
+ board_exports,
3097
+ agent_exports,
3098
+ onboard_exports,
3099
+ search_qmd_adapter_exports,
3100
+ trigger_exports,
3101
+ installSkillIntegration,
3102
+ fetchSkillMarkdownFromUrl,
3103
+ clawdapus_exports,
3104
+ integration_exports,
3105
+ diagnostics_exports,
3106
+ autonomy_daemon_exports
3107
+ };