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