mdkg 0.1.0 → 0.1.2

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +108 -15
  3. package/dist/cli.js +566 -15
  4. package/dist/commands/archive.js +474 -0
  5. package/dist/commands/bundle.js +743 -0
  6. package/dist/commands/bundle_import.js +243 -0
  7. package/dist/commands/capability.js +162 -0
  8. package/dist/commands/doctor.js +233 -2
  9. package/dist/commands/format.js +38 -9
  10. package/dist/commands/index.js +11 -0
  11. package/dist/commands/init.js +188 -63
  12. package/dist/commands/init_manifest.js +19 -6
  13. package/dist/commands/list.js +5 -2
  14. package/dist/commands/new.js +6 -0
  15. package/dist/commands/next.js +7 -0
  16. package/dist/commands/node_card.js +4 -1
  17. package/dist/commands/pack.js +62 -2
  18. package/dist/commands/query_output.js +1 -0
  19. package/dist/commands/search.js +5 -2
  20. package/dist/commands/show.js +7 -14
  21. package/dist/commands/skill_mirror.js +22 -0
  22. package/dist/commands/task.js +3 -0
  23. package/dist/commands/upgrade.js +151 -13
  24. package/dist/commands/validate.js +19 -2
  25. package/dist/commands/work.js +365 -0
  26. package/dist/commands/workspace.js +12 -2
  27. package/dist/core/config.js +100 -1
  28. package/dist/graph/agent_file_types.js +78 -5
  29. package/dist/graph/archive_file.js +125 -0
  30. package/dist/graph/archive_integrity.js +66 -0
  31. package/dist/graph/bundle_imports.js +418 -0
  32. package/dist/graph/capabilities_index_cache.js +103 -0
  33. package/dist/graph/capabilities_indexer.js +231 -0
  34. package/dist/graph/frontmatter.js +19 -0
  35. package/dist/graph/index_cache.js +21 -4
  36. package/dist/graph/indexer.js +4 -1
  37. package/dist/graph/node.js +23 -4
  38. package/dist/graph/node_body.js +37 -0
  39. package/dist/graph/skills_indexer.js +8 -3
  40. package/dist/graph/template_schema.js +33 -5
  41. package/dist/graph/validate_graph.js +83 -7
  42. package/dist/graph/visibility.js +214 -0
  43. package/dist/graph/workspace_files.js +22 -0
  44. package/dist/init/AGENT_START.md +21 -0
  45. package/dist/init/CLI_COMMAND_MATRIX.md +58 -3
  46. package/dist/init/README.md +60 -3
  47. package/dist/init/config.json +13 -1
  48. package/dist/init/core/guide.md +6 -2
  49. package/dist/init/core/rule-3-cli-contract.md +71 -4
  50. package/dist/init/core/rule-4-repo-safety-and-ignores.md +20 -0
  51. package/dist/init/core/rule-6-templates-and-schemas.md +10 -1
  52. package/dist/init/init-manifest.json +19 -14
  53. package/dist/init/skills/default/build-pack-and-execute-task/SKILL.md +2 -1
  54. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +26 -0
  55. package/dist/init/templates/default/archive.md +33 -0
  56. package/dist/init/templates/default/receipt.md +15 -1
  57. package/dist/init/templates/default/work.md +6 -1
  58. package/dist/init/templates/default/work_order.md +15 -1
  59. package/dist/pack/export_md.js +3 -0
  60. package/dist/pack/export_xml.js +3 -0
  61. package/dist/pack/order.js +1 -0
  62. package/dist/pack/pack.js +3 -13
  63. package/dist/templates/builtin.js +38 -0
  64. package/dist/templates/loader.js +9 -16
  65. package/dist/util/argparse.js +30 -0
  66. package/dist/util/refs.js +40 -0
  67. package/dist/util/zip.js +153 -0
  68. package/package.json +8 -2
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveBundleImportsIndexPath = resolveBundleImportsIndexPath;
7
+ exports.buildBundleImportsIndex = buildBundleImportsIndex;
8
+ exports.writeBundleImportsIndex = writeBundleImportsIndex;
9
+ exports.isBundleImportsIndexStale = isBundleImportsIndexStale;
10
+ exports.mergeBundleImportsIntoIndex = mergeBundleImportsIntoIndex;
11
+ exports.importWarnings = importWarnings;
12
+ exports.buildImportedCapabilityRecords = buildImportedCapabilityRecords;
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const child_process_1 = require("child_process");
17
+ const paths_1 = require("../core/paths");
18
+ const zip_1 = require("../util/zip");
19
+ const IMPORTS_CACHE_VERSION = 1;
20
+ const MANIFEST_ENTRY = "manifest.json";
21
+ const GLOBAL_INDEX_ENTRY = ".mdkg/index/global.json";
22
+ const SKILLS_INDEX_ENTRY = ".mdkg/index/skills.json";
23
+ const CAPABILITIES_INDEX_ENTRY = ".mdkg/index/capabilities.json";
24
+ function toPosixPath(value) {
25
+ return value.split(path_1.default.sep).join("/");
26
+ }
27
+ function sha256Buffer(buffer) {
28
+ return `sha256:${crypto_1.default.createHash("sha256").update(buffer).digest("hex")}`;
29
+ }
30
+ function readJsonEntry(entries, entryPath) {
31
+ const data = entries.get(entryPath);
32
+ if (!data) {
33
+ throw new Error(`missing bundle entry: ${entryPath}`);
34
+ }
35
+ return JSON.parse(data.toString("utf8"));
36
+ }
37
+ function readBundleEntries(bundlePath) {
38
+ return new Map((0, zip_1.readZipEntries)(fs_1.default.readFileSync(bundlePath)).map((entry) => [entry.name, entry.data]));
39
+ }
40
+ function resolveBundlePath(root, entry) {
41
+ return path_1.default.resolve(root, entry.path);
42
+ }
43
+ function gitOutput(cwd, args) {
44
+ const result = (0, child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: "pipe" });
45
+ if (result.status !== 0) {
46
+ return null;
47
+ }
48
+ const output = result.stdout.trim();
49
+ return output.length > 0 ? output : null;
50
+ }
51
+ function sourcePathState(root, entry, manifest, warnings, errors) {
52
+ if (!entry.source_path) {
53
+ return false;
54
+ }
55
+ const sourceRoot = path_1.default.resolve(root, entry.source_path);
56
+ if (!fs_1.default.existsSync(sourceRoot)) {
57
+ errors.push(`source_path does not exist: ${entry.source_path}`);
58
+ return false;
59
+ }
60
+ const currentHead = gitOutput(sourceRoot, ["rev-parse", "HEAD"]);
61
+ const currentStatus = gitOutput(sourceRoot, ["status", "--porcelain"]);
62
+ let stale = false;
63
+ if (manifest.source?.git_head && currentHead && manifest.source.git_head !== currentHead) {
64
+ warnings.push(`source HEAD changed: ${manifest.source.git_head} -> ${currentHead}`);
65
+ stale = true;
66
+ }
67
+ if (currentStatus) {
68
+ warnings.push(`source_path has uncommitted changes: ${entry.source_path}`);
69
+ stale = true;
70
+ }
71
+ return stale;
72
+ }
73
+ function ageState(bundlePath, entry, warnings) {
74
+ if (entry.max_stale_seconds === undefined) {
75
+ return false;
76
+ }
77
+ const ageSeconds = Math.floor((Date.now() - fs_1.default.statSync(bundlePath).mtimeMs) / 1000);
78
+ if (ageSeconds <= entry.max_stale_seconds) {
79
+ return false;
80
+ }
81
+ warnings.push(`bundle age ${ageSeconds}s exceeds max_stale_seconds ${entry.max_stale_seconds}`);
82
+ return true;
83
+ }
84
+ function remapTarget(target, qidMap) {
85
+ if (!target) {
86
+ return undefined;
87
+ }
88
+ return qidMap.get(target) ?? target;
89
+ }
90
+ function remapTargets(targets, qidMap) {
91
+ return targets.map((target) => qidMap.get(target) ?? target);
92
+ }
93
+ function addReverseEdge(reverse, edgeKey, target, source) {
94
+ if (!target) {
95
+ return;
96
+ }
97
+ reverse[edgeKey] = reverse[edgeKey] ?? {};
98
+ reverse[edgeKey][target] = reverse[edgeKey][target] ?? [];
99
+ reverse[edgeKey][target].push(source);
100
+ }
101
+ function addReverseEdges(reverse, node) {
102
+ addReverseEdge(reverse, "epic", node.edges.epic, node.qid);
103
+ addReverseEdge(reverse, "parent", node.edges.parent, node.qid);
104
+ addReverseEdge(reverse, "prev", node.edges.prev, node.qid);
105
+ addReverseEdge(reverse, "next", node.edges.next, node.qid);
106
+ for (const target of node.edges.relates) {
107
+ addReverseEdge(reverse, "relates", target, node.qid);
108
+ }
109
+ for (const target of node.edges.blocked_by) {
110
+ addReverseEdge(reverse, "blocked_by", target, node.qid);
111
+ }
112
+ for (const target of node.edges.blocks) {
113
+ addReverseEdge(reverse, "blocks", target, node.qid);
114
+ }
115
+ }
116
+ function entryHashErrors(entries, manifest) {
117
+ const errors = [];
118
+ for (const file of manifest.files ?? []) {
119
+ const data = entries.get(file.path);
120
+ if (!data) {
121
+ errors.push(`missing bundle entry: ${file.path}`);
122
+ continue;
123
+ }
124
+ if (sha256Buffer(data) !== file.sha256) {
125
+ errors.push(`hash mismatch for ${file.path}`);
126
+ }
127
+ if (data.length !== file.size) {
128
+ errors.push(`size mismatch for ${file.path}`);
129
+ }
130
+ }
131
+ return errors;
132
+ }
133
+ function projectOneImport(root, alias, entry) {
134
+ const bundlePath = resolveBundlePath(root, entry);
135
+ const relativeBundlePath = toPosixPath(path_1.default.relative(root, bundlePath));
136
+ const warnings = [];
137
+ const errors = [];
138
+ const nodes = {};
139
+ const reverse_edges = {};
140
+ let manifest;
141
+ let sourceIndex;
142
+ let stale = false;
143
+ if (!entry.enabled) {
144
+ return {
145
+ health: {
146
+ alias,
147
+ path: entry.path,
148
+ enabled: false,
149
+ visibility: entry.visibility,
150
+ expected_profile: entry.expected_profile,
151
+ stale: false,
152
+ warning_count: 0,
153
+ error_count: 0,
154
+ warnings: [],
155
+ errors: [],
156
+ },
157
+ nodes,
158
+ reverse_edges,
159
+ };
160
+ }
161
+ try {
162
+ if (!fs_1.default.existsSync(bundlePath)) {
163
+ throw new Error(`bundle not found: ${entry.path}`);
164
+ }
165
+ const entries = readBundleEntries(bundlePath);
166
+ manifest = readJsonEntry(entries, MANIFEST_ENTRY);
167
+ sourceIndex = readJsonEntry(entries, GLOBAL_INDEX_ENTRY);
168
+ readJsonEntry(entries, SKILLS_INDEX_ENTRY);
169
+ readJsonEntry(entries, CAPABILITIES_INDEX_ENTRY);
170
+ errors.push(...entryHashErrors(entries, manifest));
171
+ if (manifest.profile !== entry.expected_profile) {
172
+ errors.push(`expected ${entry.expected_profile} bundle but found ${manifest.profile}`);
173
+ }
174
+ stale = sourcePathState(root, entry, manifest, warnings, errors) || stale;
175
+ stale = ageState(bundlePath, entry, warnings) || stale;
176
+ const idOwners = new Map();
177
+ for (const node of Object.values(sourceIndex.nodes)) {
178
+ const owners = idOwners.get(node.id) ?? [];
179
+ owners.push(node.qid);
180
+ idOwners.set(node.id, owners);
181
+ }
182
+ for (const [id, owners] of idOwners.entries()) {
183
+ if (owners.length > 1) {
184
+ errors.push(`duplicate projected id ${id} from ${owners.join(", ")}; create per-workspace bundles or use portable unique ids`);
185
+ }
186
+ }
187
+ if (errors.length === 0) {
188
+ const qidMap = new Map();
189
+ for (const node of Object.values(sourceIndex.nodes)) {
190
+ qidMap.set(node.qid, `${alias}:${node.id}`);
191
+ }
192
+ for (const node of Object.values(sourceIndex.nodes)) {
193
+ const projectedQid = qidMap.get(node.qid);
194
+ const projected = {
195
+ ...node,
196
+ ws: alias,
197
+ qid: projectedQid,
198
+ path: `${relativeBundlePath}#${toPosixPath(node.path)}`,
199
+ attributes: { ...(node.attributes ?? {}) },
200
+ edges: {
201
+ epic: remapTarget(node.edges.epic, qidMap),
202
+ parent: remapTarget(node.edges.parent, qidMap),
203
+ prev: remapTarget(node.edges.prev, qidMap),
204
+ next: remapTarget(node.edges.next, qidMap),
205
+ relates: remapTargets(node.edges.relates, qidMap),
206
+ blocked_by: remapTargets(node.edges.blocked_by, qidMap),
207
+ blocks: remapTargets(node.edges.blocks, qidMap),
208
+ },
209
+ source: {
210
+ imported: true,
211
+ read_only: true,
212
+ import_alias: alias,
213
+ original_qid: node.qid,
214
+ original_ws: node.ws,
215
+ original_path: toPosixPath(node.path),
216
+ bundle_path: relativeBundlePath,
217
+ bundle_hash: manifest.bundle_hash,
218
+ profile: manifest.profile,
219
+ visibility: entry.visibility,
220
+ stale,
221
+ warnings: [...warnings],
222
+ source_repo: entry.source_repo ?? manifest.source?.repo,
223
+ source_git_head: manifest.source?.git_head ?? null,
224
+ },
225
+ };
226
+ nodes[projected.qid] = projected;
227
+ addReverseEdges(reverse_edges, projected);
228
+ }
229
+ }
230
+ }
231
+ catch (err) {
232
+ errors.push(err instanceof Error ? err.message : String(err));
233
+ }
234
+ return {
235
+ health: {
236
+ alias,
237
+ path: entry.path,
238
+ enabled: entry.enabled,
239
+ visibility: entry.visibility,
240
+ expected_profile: entry.expected_profile,
241
+ profile: manifest?.profile,
242
+ bundle_hash: manifest?.bundle_hash,
243
+ source_git_head: manifest?.source?.git_head ?? null,
244
+ source_repo: entry.source_repo ?? manifest?.source?.repo,
245
+ stale,
246
+ warning_count: warnings.length,
247
+ error_count: errors.length,
248
+ warnings,
249
+ errors,
250
+ },
251
+ nodes,
252
+ reverse_edges,
253
+ };
254
+ }
255
+ function resolveBundleImportsIndexPath(root) {
256
+ return path_1.default.resolve(root, ".mdkg", "index", "imports.json");
257
+ }
258
+ function buildBundleImportsIndex(root, config) {
259
+ const imports = [];
260
+ const nodes = {};
261
+ const reverse_edges = {};
262
+ const workspaces = {};
263
+ for (const alias of Object.keys(config.bundle_imports).sort()) {
264
+ const entry = config.bundle_imports[alias];
265
+ const projected = projectOneImport(root, alias, entry);
266
+ imports.push(projected.health);
267
+ if (!entry.enabled || projected.health.error_count > 0) {
268
+ continue;
269
+ }
270
+ workspaces[alias] = { path: `bundle:${entry.path}`, enabled: true };
271
+ Object.assign(nodes, projected.nodes);
272
+ for (const [edgeKey, targets] of Object.entries(projected.reverse_edges)) {
273
+ reverse_edges[edgeKey] = reverse_edges[edgeKey] ?? {};
274
+ for (const [target, sources] of Object.entries(targets)) {
275
+ reverse_edges[edgeKey][target] = [
276
+ ...(reverse_edges[edgeKey][target] ?? []),
277
+ ...sources,
278
+ ].sort();
279
+ }
280
+ }
281
+ }
282
+ return {
283
+ index: {
284
+ meta: {
285
+ tool: config.tool,
286
+ schema_version: config.schema_version,
287
+ cache_version: IMPORTS_CACHE_VERSION,
288
+ generated_at: new Date().toISOString(),
289
+ root,
290
+ import_count: imports.length,
291
+ node_count: Object.keys(nodes).length,
292
+ },
293
+ imports,
294
+ nodes,
295
+ reverse_edges,
296
+ },
297
+ workspaces,
298
+ };
299
+ }
300
+ function writeBundleImportsIndex(indexPath, index) {
301
+ fs_1.default.mkdirSync(path_1.default.dirname(indexPath), { recursive: true });
302
+ fs_1.default.writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf8");
303
+ }
304
+ function isBundleImportsIndexStale(root, config) {
305
+ const indexPath = resolveBundleImportsIndexPath(root);
306
+ if (!fs_1.default.existsSync(indexPath)) {
307
+ return Object.keys(config.bundle_imports).length > 0;
308
+ }
309
+ const indexMtime = fs_1.default.statSync(indexPath).mtimeMs;
310
+ const cfgPath = (0, paths_1.configPath)(root);
311
+ if (fs_1.default.existsSync(cfgPath) && fs_1.default.statSync(cfgPath).mtimeMs > indexMtime) {
312
+ return true;
313
+ }
314
+ for (const entry of Object.values(config.bundle_imports)) {
315
+ const bundlePath = path_1.default.resolve(root, entry.path);
316
+ if (!fs_1.default.existsSync(bundlePath)) {
317
+ return true;
318
+ }
319
+ if (fs_1.default.statSync(bundlePath).mtimeMs > indexMtime) {
320
+ return true;
321
+ }
322
+ }
323
+ return false;
324
+ }
325
+ function mergeBundleImportsIntoIndex(base, projection) {
326
+ const nodes = { ...base.nodes, ...projection.index.nodes };
327
+ const reverse_edges = JSON.parse(JSON.stringify(base.reverse_edges));
328
+ for (const [edgeKey, targets] of Object.entries(projection.index.reverse_edges)) {
329
+ reverse_edges[edgeKey] = reverse_edges[edgeKey] ?? {};
330
+ for (const [target, sources] of Object.entries(targets)) {
331
+ reverse_edges[edgeKey][target] = [
332
+ ...(reverse_edges[edgeKey][target] ?? []),
333
+ ...sources,
334
+ ].sort();
335
+ }
336
+ }
337
+ return {
338
+ ...base,
339
+ meta: {
340
+ ...base.meta,
341
+ workspaces: Array.from(new Set([...base.meta.workspaces, ...Object.keys(projection.workspaces)])).sort(),
342
+ },
343
+ workspaces: {
344
+ ...base.workspaces,
345
+ ...projection.workspaces,
346
+ },
347
+ nodes,
348
+ reverse_edges,
349
+ };
350
+ }
351
+ function importWarnings(projection) {
352
+ const warnings = [];
353
+ for (const item of projection.index.imports) {
354
+ for (const warning of item.warnings) {
355
+ warnings.push(`bundle import ${item.alias}: ${warning}`);
356
+ }
357
+ for (const error of item.errors) {
358
+ warnings.push(`bundle import ${item.alias}: ${error}`);
359
+ }
360
+ }
361
+ return warnings;
362
+ }
363
+ function buildImportedCapabilityRecords(root, config) {
364
+ const records = [];
365
+ const warnings = [];
366
+ for (const alias of Object.keys(config.bundle_imports).sort()) {
367
+ const entry = config.bundle_imports[alias];
368
+ if (!entry.enabled) {
369
+ continue;
370
+ }
371
+ const projection = projectOneImport(root, alias, entry);
372
+ for (const warning of projection.health.warnings) {
373
+ warnings.push(`bundle import ${alias}: ${warning}`);
374
+ }
375
+ for (const error of projection.health.errors) {
376
+ warnings.push(`bundle import ${alias}: ${error}`);
377
+ }
378
+ if (projection.health.error_count > 0) {
379
+ continue;
380
+ }
381
+ try {
382
+ const bundlePath = resolveBundlePath(root, entry);
383
+ const relativeBundlePath = toPosixPath(path_1.default.relative(root, bundlePath));
384
+ const entries = readBundleEntries(bundlePath);
385
+ const capabilities = readJsonEntry(entries, CAPABILITIES_INDEX_ENTRY);
386
+ for (const record of capabilities.records ?? []) {
387
+ const id = String(record.id ?? "");
388
+ const originalQid = String(record.qid ?? id);
389
+ const originalWorkspace = String(record.workspace ?? "");
390
+ const originalPath = String(record.path ?? "");
391
+ records.push({
392
+ ...record,
393
+ workspace: alias,
394
+ visibility: entry.visibility,
395
+ qid: `${alias}:${id}`,
396
+ path: `${relativeBundlePath}#${originalPath}`,
397
+ source: {
398
+ imported: true,
399
+ read_only: true,
400
+ import_alias: alias,
401
+ original_qid: originalQid,
402
+ original_workspace: originalWorkspace,
403
+ original_path: originalPath,
404
+ bundle_path: relativeBundlePath,
405
+ bundle_hash: projection.health.bundle_hash,
406
+ profile: projection.health.profile,
407
+ stale: projection.health.stale,
408
+ warnings: [...projection.health.warnings],
409
+ },
410
+ });
411
+ }
412
+ }
413
+ catch (err) {
414
+ warnings.push(`bundle import ${alias}: ${err instanceof Error ? err.message : String(err)}`);
415
+ }
416
+ }
417
+ return { records, warnings };
418
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isCapabilitiesIndexStale = isCapabilitiesIndexStale;
7
+ exports.writeCapabilitiesIndex = writeCapabilitiesIndex;
8
+ exports.loadCapabilitiesIndex = loadCapabilitiesIndex;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const paths_1 = require("../core/paths");
12
+ const workspace_files_1 = require("./workspace_files");
13
+ const capabilities_indexer_1 = require("./capabilities_indexer");
14
+ function mtimeMs(filePath) {
15
+ return fs_1.default.statSync(filePath).mtimeMs;
16
+ }
17
+ function listFilesAndDirectories(dir) {
18
+ if (!fs_1.default.existsSync(dir)) {
19
+ return [];
20
+ }
21
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
22
+ const items = [dir];
23
+ for (const entry of entries) {
24
+ const fullPath = path_1.default.join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ items.push(...listFilesAndDirectories(fullPath));
27
+ continue;
28
+ }
29
+ if (entry.isFile()) {
30
+ items.push(fullPath);
31
+ }
32
+ }
33
+ return items;
34
+ }
35
+ function workspaceSkillsRoots(root, config) {
36
+ return Object.keys(config.workspaces)
37
+ .sort()
38
+ .filter((alias) => config.workspaces[alias].enabled)
39
+ .map((alias) => {
40
+ const workspace = config.workspaces[alias];
41
+ return path_1.default.resolve(root, workspace.path, workspace.mdkg_dir, "skills");
42
+ });
43
+ }
44
+ function isCapabilitiesIndexStale(root, config) {
45
+ const indexPath = (0, capabilities_indexer_1.resolveCapabilitiesIndexPath)(root, config);
46
+ if (!fs_1.default.existsSync(indexPath)) {
47
+ return true;
48
+ }
49
+ const indexMtime = mtimeMs(indexPath);
50
+ const cfgPath = (0, paths_1.configPath)(root);
51
+ if (fs_1.default.existsSync(cfgPath) && mtimeMs(cfgPath) > indexMtime) {
52
+ return true;
53
+ }
54
+ for (const filePath of (0, workspace_files_1.listWorkspaceDocFiles)(root, config)) {
55
+ if (mtimeMs(filePath) > indexMtime) {
56
+ return true;
57
+ }
58
+ }
59
+ for (const skillsRoot of workspaceSkillsRoots(root, config)) {
60
+ for (const item of listFilesAndDirectories(skillsRoot)) {
61
+ if (mtimeMs(item) > indexMtime) {
62
+ return true;
63
+ }
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+ function readCapabilitiesIndex(indexPath) {
69
+ try {
70
+ const raw = fs_1.default.readFileSync(indexPath, "utf8");
71
+ return JSON.parse(raw);
72
+ }
73
+ catch (err) {
74
+ const message = err instanceof Error ? err.message : "unknown error";
75
+ throw new Error(`failed to read capabilities index: ${message}`);
76
+ }
77
+ }
78
+ function writeCapabilitiesIndex(indexPath, index) {
79
+ fs_1.default.mkdirSync(path_1.default.dirname(indexPath), { recursive: true });
80
+ fs_1.default.writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf8");
81
+ }
82
+ function loadCapabilitiesIndex(options) {
83
+ const useCache = options.useCache ?? true;
84
+ const allowReindex = options.allowReindex ?? options.config.index.auto_reindex;
85
+ const indexPath = (0, capabilities_indexer_1.resolveCapabilitiesIndexPath)(options.root, options.config);
86
+ if (!useCache) {
87
+ const index = (0, capabilities_indexer_1.buildCapabilitiesIndex)(options.root, options.config);
88
+ return { index, rebuilt: true, stale: false };
89
+ }
90
+ const stale = isCapabilitiesIndexStale(options.root, options.config);
91
+ if (fs_1.default.existsSync(indexPath) && !stale) {
92
+ return { index: readCapabilitiesIndex(indexPath), rebuilt: false, stale: false };
93
+ }
94
+ if (allowReindex) {
95
+ const index = (0, capabilities_indexer_1.buildCapabilitiesIndex)(options.root, options.config);
96
+ writeCapabilitiesIndex(indexPath, index);
97
+ return { index, rebuilt: true, stale };
98
+ }
99
+ if (fs_1.default.existsSync(indexPath)) {
100
+ return { index: readCapabilitiesIndex(indexPath), rebuilt: false, stale: true };
101
+ }
102
+ throw new Error("capabilities index missing and auto-reindex is disabled");
103
+ }