mdkg 0.4.2 → 0.5.1

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 (70) hide show
  1. package/CHANGELOG.md +88 -15
  2. package/CLI_COMMAND_MATRIX.md +74 -5
  3. package/README.md +30 -2
  4. package/dist/cli.js +132 -5
  5. package/dist/command-contract.json +778 -16
  6. package/dist/commands/archive.js +196 -42
  7. package/dist/commands/bundle.js +180 -44
  8. package/dist/commands/capability.js +1 -0
  9. package/dist/commands/checkpoint.js +6 -5
  10. package/dist/commands/event_support.js +16 -7
  11. package/dist/commands/fix.js +11 -8
  12. package/dist/commands/git.js +41 -10
  13. package/dist/commands/goal.js +23 -23
  14. package/dist/commands/graph.js +64 -10
  15. package/dist/commands/handoff.js +4 -2
  16. package/dist/commands/init.js +28 -23
  17. package/dist/commands/loop.js +1668 -0
  18. package/dist/commands/loop_descriptors.js +219 -0
  19. package/dist/commands/mcp.js +101 -11
  20. package/dist/commands/new.js +49 -3
  21. package/dist/commands/pack.js +14 -7
  22. package/dist/commands/search.js +10 -1
  23. package/dist/commands/skill.js +12 -9
  24. package/dist/commands/skill_mirror.js +39 -31
  25. package/dist/commands/subgraph.js +59 -26
  26. package/dist/commands/task.js +77 -4
  27. package/dist/commands/upgrade.js +13 -15
  28. package/dist/commands/validate.js +20 -7
  29. package/dist/commands/work.js +44 -26
  30. package/dist/commands/workspace.js +10 -4
  31. package/dist/core/config.js +45 -17
  32. package/dist/core/filesystem_authority.js +281 -0
  33. package/dist/core/project_db_events.js +4 -0
  34. package/dist/core/project_db_materializer.js +2 -0
  35. package/dist/core/project_db_migrations.js +238 -181
  36. package/dist/core/project_db_snapshot.js +64 -14
  37. package/dist/core/workspace_path.js +7 -0
  38. package/dist/graph/archive_integrity.js +1 -1
  39. package/dist/graph/capabilities_index_cache.js +4 -1
  40. package/dist/graph/frontmatter.js +38 -0
  41. package/dist/graph/goal_scope.js +4 -1
  42. package/dist/graph/index_cache.js +37 -7
  43. package/dist/graph/loop_bindings.js +39 -0
  44. package/dist/graph/node.js +209 -1
  45. package/dist/graph/node_body.js +52 -6
  46. package/dist/graph/skills_index_cache.js +41 -6
  47. package/dist/graph/skills_indexer.js +5 -2
  48. package/dist/graph/sqlite_index.js +118 -105
  49. package/dist/graph/subgraphs.js +142 -5
  50. package/dist/graph/validate_graph.js +109 -13
  51. package/dist/graph/workspace_files.js +39 -9
  52. package/dist/init/AGENT_START.md +16 -0
  53. package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
  54. package/dist/init/config.json +7 -1
  55. package/dist/init/init-manifest.json +49 -4
  56. package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
  57. package/dist/init/templates/default/loop.md +160 -0
  58. package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
  59. package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
  60. package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
  61. package/dist/init/templates/loops/security-audit.loop.md +140 -0
  62. package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
  63. package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
  64. package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
  65. package/dist/pack/order.js +3 -1
  66. package/dist/pack/pack.js +139 -6
  67. package/dist/templates/loader.js +9 -3
  68. package/dist/util/lock.js +10 -9
  69. package/dist/util/zip.js +163 -9
  70. package/package.json +8 -4
@@ -7,12 +7,12 @@ exports.resolveContainedProjectDbPath = resolveContainedProjectDbPath;
7
7
  exports.sealProjectDbSnapshot = sealProjectDbSnapshot;
8
8
  exports.verifyProjectDbSnapshot = verifyProjectDbSnapshot;
9
9
  exports.projectDbSnapshotStatus = projectDbSnapshotStatus;
10
- exports.canonicalDumpForSnapshot = canonicalDumpForSnapshot;
11
10
  exports.dumpProjectDbSnapshot = dumpProjectDbSnapshot;
12
11
  exports.diffProjectDbSnapshots = diffProjectDbSnapshots;
13
12
  const crypto_1 = __importDefault(require("crypto"));
14
13
  const fs_1 = __importDefault(require("fs"));
15
14
  const path_1 = __importDefault(require("path"));
15
+ const filesystem_authority_1 = require("./filesystem_authority");
16
16
  const project_db_1 = require("./project_db");
17
17
  const version_1 = require("./version");
18
18
  const atomic_1 = require("../util/atomic");
@@ -166,7 +166,11 @@ function readManifest(filePath) {
166
166
  typeof manifest.snapshot_sha256 !== "string" ||
167
167
  typeof manifest.byte_size !== "number" ||
168
168
  !Array.isArray(manifest.table_counts) ||
169
- !Array.isArray(manifest.migrations)) {
169
+ !Array.isArray(manifest.migrations) ||
170
+ (manifest.queue_policy !== "drain" && manifest.queue_policy !== "paused") ||
171
+ typeof manifest.queue_summary !== "object" ||
172
+ manifest.queue_summary === null ||
173
+ Array.isArray(manifest.queue_summary)) {
170
174
  throw new errors_1.ValidationError("snapshot manifest has unsupported shape");
171
175
  }
172
176
  return manifest;
@@ -251,7 +255,9 @@ function sealProjectDbSnapshot(root, config, queuePolicy = "drain") {
251
255
  }
252
256
  try {
253
257
  const runtimeHash = sha256File(layout.runtimeFile);
254
- const manifest = buildManifest(root, config, tempSnapshot, runtimeHash, queuePolicy, queueSummary);
258
+ const sealedQueueSummary = (0, project_db_queue_1.readProjectQueueSnapshotSummary)(tempSnapshot);
259
+ assertQueueSnapshotPolicy(queuePolicy, sealedQueueSummary);
260
+ const manifest = buildManifest(root, config, tempSnapshot, runtimeHash, queuePolicy, sealedQueueSummary);
255
261
  (0, atomic_1.atomicWriteFile)(tempManifest, `${JSON.stringify(manifest, null, 2)}\n`);
256
262
  fs_1.default.renameSync(tempSnapshot, layout.stateFile);
257
263
  fs_1.default.renameSync(tempManifest, layout.stateManifest);
@@ -267,7 +273,7 @@ function sealProjectDbSnapshot(root, config, queuePolicy = "drain") {
267
273
  table_counts: manifest.table_counts,
268
274
  migrations: manifest.migrations,
269
275
  queue_policy: queuePolicy,
270
- queue_summary: queueSummary,
276
+ queue_summary: sealedQueueSummary,
271
277
  warnings: warningListFromVerify(root, config),
272
278
  };
273
279
  }
@@ -384,7 +390,18 @@ function verifyProjectDbSnapshot(root, config) {
384
390
  warnings: [],
385
391
  });
386
392
  }
387
- if (manifest.source_runtime_sha256 && fs_1.default.existsSync(layout.runtimeFile)) {
393
+ if (fs_1.default.existsSync(layout.runtimeFile) && !manifest.source_runtime_sha256) {
394
+ checks.push({
395
+ name: "runtime-freshness",
396
+ ok: false,
397
+ level: "fail",
398
+ path: rel(root, layout.runtimeFile),
399
+ detail: "snapshot source runtime hash is missing",
400
+ errors: ["snapshot manifest must include source_runtime_sha256 when the runtime database exists"],
401
+ warnings: [],
402
+ });
403
+ }
404
+ else if (manifest.source_runtime_sha256 && fs_1.default.existsSync(layout.runtimeFile)) {
388
405
  const runtimeHash = sha256File(layout.runtimeFile);
389
406
  checks.push({
390
407
  name: "runtime-freshness",
@@ -396,6 +413,30 @@ function verifyProjectDbSnapshot(root, config) {
396
413
  warnings: runtimeHash === manifest.source_runtime_sha256 ? [] : ["runtime database hash differs from sealed snapshot source hash"],
397
414
  });
398
415
  }
416
+ try {
417
+ const sealedQueueSummary = (0, project_db_queue_1.readProjectQueueSnapshotSummary)(layout.stateFile);
418
+ assertQueueSnapshotPolicy(manifest.queue_policy, sealedQueueSummary);
419
+ const matchesManifest = compareJson(sealedQueueSummary, manifest.queue_summary);
420
+ checks.push({
421
+ name: "queue-policy",
422
+ ok: matchesManifest,
423
+ level: matchesManifest ? "ok" : "fail",
424
+ detail: matchesManifest ? "sealed queue state satisfies policy and matches manifest" : "sealed queue summary differs from manifest",
425
+ errors: matchesManifest ? [] : ["sealed queue summary does not match snapshot manifest"],
426
+ warnings: [],
427
+ });
428
+ }
429
+ catch (err) {
430
+ const message = err instanceof Error ? err.message : String(err);
431
+ checks.push({
432
+ name: "queue-policy",
433
+ ok: false,
434
+ level: "fail",
435
+ detail: "sealed queue state violates snapshot policy",
436
+ errors: [message],
437
+ warnings: [],
438
+ });
439
+ }
399
440
  }
400
441
  const errors = checks.flatMap((check) => check.errors.map((error) => `${check.name}: ${error}`));
401
442
  const warnings = checks.flatMap((check) => check.warnings.map((warning) => `${check.name}: ${warning}`));
@@ -485,17 +526,24 @@ function canonicalDumpForSnapshot(root, snapshotPath) {
485
526
  db.close();
486
527
  }
487
528
  }
529
+ function canonicalDumpForContainedSnapshot(root, rawPath, label) {
530
+ const resolved = resolveContainedProjectDbPath(root, rawPath, label);
531
+ const relativePath = rel(root, resolved);
532
+ return (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath, operation: "read" }, ({ absolutePath }) => ({
533
+ absolutePath,
534
+ dump: canonicalDumpForSnapshot(root, absolutePath),
535
+ }));
536
+ }
488
537
  function dumpProjectDbSnapshot(root, config, snapshotPath, outputPath) {
489
538
  const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
490
- const resolvedSnapshot = snapshotPath
491
- ? resolveContainedProjectDbPath(root, snapshotPath, "--snapshot")
492
- : layout.stateFile;
493
- const dump = canonicalDumpForSnapshot(root, resolvedSnapshot);
539
+ const snapshot = canonicalDumpForContainedSnapshot(root, snapshotPath ?? layout.stateFile, "--snapshot");
540
+ const resolvedSnapshot = snapshot.absolutePath;
541
+ const dump = snapshot.dump;
494
542
  const dumpHash = sha256Buffer(dump);
495
543
  let output = null;
496
544
  if (outputPath) {
497
545
  const resolvedOutput = resolveContainedProjectDbPath(root, outputPath, "--output");
498
- (0, atomic_1.atomicWriteFile)(resolvedOutput, dump);
546
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: rel(root, resolvedOutput) }, dump);
499
547
  output = rel(root, resolvedOutput);
500
548
  }
501
549
  return {
@@ -509,10 +557,12 @@ function dumpProjectDbSnapshot(root, config, snapshotPath, outputPath) {
509
557
  };
510
558
  }
511
559
  function diffProjectDbSnapshots(root, leftPath, rightPath) {
512
- const left = resolveContainedProjectDbPath(root, leftPath, "left snapshot");
513
- const right = resolveContainedProjectDbPath(root, rightPath, "right snapshot");
514
- const leftDump = canonicalDumpForSnapshot(root, left);
515
- const rightDump = canonicalDumpForSnapshot(root, right);
560
+ const leftSnapshot = canonicalDumpForContainedSnapshot(root, leftPath, "left snapshot");
561
+ const rightSnapshot = canonicalDumpForContainedSnapshot(root, rightPath, "right snapshot");
562
+ const left = leftSnapshot.absolutePath;
563
+ const right = rightSnapshot.absolutePath;
564
+ const leftDump = leftSnapshot.dump;
565
+ const rightDump = rightSnapshot.dump;
516
566
  const leftLines = leftDump.trimEnd().split("\n");
517
567
  const rightLines = rightDump.trimEnd().split("\n");
518
568
  const leftSet = new Set(leftLines);
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.normalizeContainedWorkspacePath = normalizeContainedWorkspacePath;
7
7
  exports.isRootWorkspacePath = isRootWorkspacePath;
8
8
  exports.workspaceDocumentRootKey = workspaceDocumentRootKey;
9
+ exports.workspaceDocumentRelativePath = workspaceDocumentRelativePath;
9
10
  const path_1 = __importDefault(require("path"));
10
11
  function isAbsoluteOnSupportedPlatform(value) {
11
12
  return path_1.default.isAbsolute(value) || path_1.default.posix.isAbsolute(value) || path_1.default.win32.isAbsolute(value);
@@ -39,3 +40,9 @@ function workspaceDocumentRootKey(workspacePath, mdkgDir) {
39
40
  .filter((part) => part && part !== ".")
40
41
  .join("/");
41
42
  }
43
+ function workspaceDocumentRelativePath(workspacePath, mdkgDir, ...suffix) {
44
+ return [workspacePath, mdkgDir, ...suffix]
45
+ .flatMap((value) => value.trim().split(/[\\/]+/))
46
+ .filter((part) => part && part !== ".")
47
+ .join("/");
48
+ }
@@ -36,7 +36,7 @@ function checkArchiveIntegrity(options) {
36
36
  result.errors.push(`compressed_sha256 mismatch: ${actualCompressedHash}`);
37
37
  }
38
38
  try {
39
- const unzipped = (0, zip_1.readSingleFileZip)(fs_1.default.readFileSync(options.zipPath));
39
+ const unzipped = (0, zip_1.readSingleFileZipPath)(options.zipPath);
40
40
  const unzippedHash = hashArchiveBuffer(unzipped.data);
41
41
  if (unzippedHash !== options.expectedRawHash) {
42
42
  result.errors.push(`zip payload sha256 mismatch: ${unzippedHash}`);
@@ -82,6 +82,7 @@ function writeCapabilitiesIndex(indexPath, index) {
82
82
  function loadCapabilitiesIndex(options) {
83
83
  const useCache = options.useCache ?? true;
84
84
  const allowReindex = options.allowReindex ?? options.config.index.auto_reindex;
85
+ const persistReindex = options.persistReindex ?? true;
85
86
  const indexPath = (0, capabilities_indexer_1.resolveCapabilitiesIndexPath)(options.root, options.config);
86
87
  if (!useCache) {
87
88
  const index = (0, capabilities_indexer_1.buildCapabilitiesIndex)(options.root, options.config);
@@ -93,7 +94,9 @@ function loadCapabilitiesIndex(options) {
93
94
  }
94
95
  if (allowReindex) {
95
96
  const index = (0, capabilities_indexer_1.buildCapabilitiesIndex)(options.root, options.config);
96
- writeCapabilitiesIndex(indexPath, index);
97
+ if (persistReindex) {
98
+ writeCapabilitiesIndex(indexPath, index);
99
+ }
97
100
  return { index, rebuilt: true, stale };
98
101
  }
99
102
  if (fs_1.default.existsSync(indexPath)) {
@@ -4,6 +4,9 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = void 0;
4
4
  exports.parseFrontmatter = parseFrontmatter;
5
5
  exports.formatFrontmatter = formatFrontmatter;
6
6
  const KEY_RE = /^[a-z][a-z0-9_]*$/;
7
+ const MAX_FRONTMATTER_LINES = 1000;
8
+ const MAX_FRONTMATTER_LINE_BYTES = 64 * 1024;
9
+ const MAX_FRONTMATTER_LIST_ITEMS = 10000;
7
10
  exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
8
11
  "id",
9
12
  "type",
@@ -20,6 +23,32 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
20
23
  "required_checks",
21
24
  "max_iterations",
22
25
  "blocked_after_attempts",
26
+ "loop_mode",
27
+ "loop_role",
28
+ "scope_description",
29
+ "template_refs",
30
+ "materialization_mode",
31
+ "child_refs",
32
+ "pre_run_questions",
33
+ "question_answer_refs",
34
+ "pre_approved_actions",
35
+ "approval_gated_actions",
36
+ "required_actions",
37
+ "requested_actions",
38
+ "prohibited_actions",
39
+ "action_approval_refs",
40
+ "evidence_lanes",
41
+ "evidence_lane_refs",
42
+ "lane_waiver_refs",
43
+ "lane_waiver_decision_refs",
44
+ "lane_waiver_approval_refs",
45
+ "run_refs",
46
+ "decision_refs",
47
+ "output_refs",
48
+ "approval_refs",
49
+ "evaluation_refs",
50
+ "definition_of_done",
51
+ "blocker_policy",
23
52
  "epic",
24
53
  "parent",
25
54
  "prev",
@@ -116,6 +145,9 @@ function parseList(valueRaw, filePath, lineNumber) {
116
145
  return [];
117
146
  }
118
147
  const parts = inner.split(",");
148
+ if (parts.length > MAX_FRONTMATTER_LIST_ITEMS) {
149
+ throw formatError(filePath, lineNumber, `list exceeds ${MAX_FRONTMATTER_LIST_ITEMS} items`);
150
+ }
119
151
  const items = [];
120
152
  for (let i = 0; i < parts.length; i += 1) {
121
153
  const item = parts[i].trim();
@@ -156,10 +188,16 @@ function parseFrontmatter(content, filePath) {
156
188
  if (endIndex === -1) {
157
189
  throw formatError(filePath, 1, "frontmatter closing --- not found");
158
190
  }
191
+ if (endIndex > MAX_FRONTMATTER_LINES) {
192
+ throw formatError(filePath, 1, `frontmatter exceeds ${MAX_FRONTMATTER_LINES} lines`);
193
+ }
159
194
  const frontmatter = {};
160
195
  for (let i = 1; i < endIndex; i += 1) {
161
196
  const line = lines[i];
162
197
  const lineNumber = i + 1;
198
+ if (Buffer.byteLength(line, "utf8") > MAX_FRONTMATTER_LINE_BYTES) {
199
+ throw formatError(filePath, lineNumber, `frontmatter line exceeds ${MAX_FRONTMATTER_LINE_BYTES} bytes`);
200
+ }
163
201
  if (line.trim().length === 0) {
164
202
  throw formatError(filePath, lineNumber, "frontmatter lines must not be blank");
165
203
  }
@@ -4,7 +4,7 @@ exports.GOAL_SCOPE_ALLOWED_TYPES = exports.GOAL_SCOPE_ACTIONABLE_TYPES = exports
4
4
  exports.goalScopeRefs = goalScopeRefs;
5
5
  exports.collectGoalScope = collectGoalScope;
6
6
  const qid_1 = require("../util/qid");
7
- exports.GOAL_SCOPE_CONTAINER_TYPES = new Set(["epic", "feat"]);
7
+ exports.GOAL_SCOPE_CONTAINER_TYPES = new Set(["loop", "epic", "feat"]);
8
8
  exports.GOAL_SCOPE_ACTIONABLE_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
9
9
  exports.GOAL_SCOPE_ALLOWED_TYPES = new Set([
10
10
  ...exports.GOAL_SCOPE_CONTAINER_TYPES,
@@ -75,6 +75,9 @@ function collectGoalScope(index, goal, options = {}) {
75
75
  if (queued.has(qid)) {
76
76
  return;
77
77
  }
78
+ if (options.maxNodes !== undefined && queued.size >= options.maxNodes) {
79
+ throw new Error(`goal scope traversal exceeds node limit: ${options.maxNodes}`);
80
+ }
78
81
  queued.add(qid);
79
82
  queue.push(qid);
80
83
  }
@@ -7,21 +7,47 @@ exports.writeIndex = writeIndex;
7
7
  exports.loadIndex = loadIndex;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const filesystem_authority_1 = require("../core/filesystem_authority");
10
11
  const sort_1 = require("../util/sort");
11
12
  const atomic_1 = require("../util/atomic");
12
13
  const indexer_1 = require("./indexer");
13
14
  const staleness_1 = require("./staleness");
14
15
  const subgraphs_1 = require("./subgraphs");
15
- function readIndex(indexPath) {
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ function readIndex(root, indexPath) {
16
20
  try {
17
- const raw = fs_1.default.readFileSync(indexPath, "utf8");
18
- return JSON.parse(raw);
21
+ const relativePath = path_1.default.relative(root, indexPath).split(path_1.default.sep).join("/");
22
+ const parsed = JSON.parse((0, filesystem_authority_1.readContainedFile)({ root, relativePath }, "utf8"));
23
+ if (!isRecord(parsed) || !isRecord(parsed.meta) || !isRecord(parsed.workspaces) || !isRecord(parsed.nodes) || !isRecord(parsed.reverse_edges)) {
24
+ throw new Error("index cache has an invalid shape");
25
+ }
26
+ return parsed;
19
27
  }
20
28
  catch (err) {
21
29
  const message = err instanceof Error ? err.message : "unknown error";
22
30
  throw new Error(`failed to read index: ${message}`);
23
31
  }
24
32
  }
33
+ function validateCachedNodePaths(root, config, cached) {
34
+ for (const node of Object.values(cached.nodes)) {
35
+ if (node.qid !== `${node.ws}:${node.id}` || node.source?.imported)
36
+ throw new Error(`invalid cached node identity: ${node.qid}`);
37
+ const workspace = config.workspaces[node.ws];
38
+ if (!workspace?.enabled)
39
+ throw new Error(`invalid cached node workspace: ${node.ws}`);
40
+ const normalized = node.path.split(path_1.default.sep).join("/");
41
+ const workspaceRoot = path_1.default.resolve(root, workspace.path, workspace.mdkg_dir);
42
+ const absolutePath = path_1.default.resolve(root, normalized);
43
+ const relativeWorkspacePath = path_1.default.relative(workspaceRoot, absolutePath);
44
+ if (path_1.default.isAbsolute(normalized) || relativeWorkspacePath.startsWith("..") || path_1.default.isAbsolute(relativeWorkspacePath)) {
45
+ throw new Error(`cached node path escapes workspace root: ${node.qid}`);
46
+ }
47
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: normalized, operation: "read" }, () => undefined);
48
+ }
49
+ return cached;
50
+ }
25
51
  function writeIndex(indexPath, index) {
26
52
  const sortedIndex = { ...index, nodes: (0, sort_1.sortIndexNodes)(index.nodes) };
27
53
  (0, atomic_1.atomicWriteFile)(indexPath, JSON.stringify(sortedIndex, null, 2));
@@ -31,13 +57,14 @@ function loadIndex(options) {
31
57
  const allowReindex = options.allowReindex ?? options.config.index.auto_reindex;
32
58
  const tolerant = options.tolerant ?? options.config.index.tolerant;
33
59
  const includeImports = options.includeImports ?? true;
60
+ const persistReindex = options.persistReindex ?? true;
34
61
  const indexPath = path_1.default.resolve(options.root, options.config.index.global_index_path);
35
62
  const withSubgraphs = (index, rebuilt, stale) => {
36
63
  if (!includeImports || Object.keys(options.config.subgraphs).length === 0) {
37
64
  return { index, rebuilt, stale, warnings: [] };
38
65
  }
39
66
  const subgraphs = (0, subgraphs_1.buildSubgraphsIndex)(options.root, options.config);
40
- if (allowReindex) {
67
+ if (allowReindex && persistReindex) {
41
68
  (0, subgraphs_1.writeSubgraphsIndex)((0, subgraphs_1.resolveSubgraphsIndexPath)(options.root), subgraphs.index);
42
69
  }
43
70
  return {
@@ -53,15 +80,18 @@ function loadIndex(options) {
53
80
  }
54
81
  const stale = (0, staleness_1.isIndexStale)(options.root, options.config);
55
82
  if (fs_1.default.existsSync(indexPath) && !stale) {
56
- return withSubgraphs(readIndex(indexPath), false, false);
83
+ return withSubgraphs(validateCachedNodePaths(options.root, options.config, readIndex(options.root, indexPath)), false, false);
57
84
  }
58
85
  if (allowReindex) {
59
86
  const index = (0, indexer_1.buildIndex)(options.root, options.config, { tolerant });
60
- writeIndex(indexPath, index);
87
+ if (persistReindex) {
88
+ writeIndex(indexPath, index);
89
+ }
61
90
  return withSubgraphs(index, true, stale);
62
91
  }
63
92
  if (fs_1.default.existsSync(indexPath)) {
64
- return withSubgraphs(readIndex(indexPath), false, true);
93
+ const cached = validateCachedNodePaths(options.root, options.config, readIndex(options.root, indexPath));
94
+ return withSubgraphs(cached, false, true);
65
95
  }
66
96
  throw new Error("index missing and auto-reindex is disabled");
67
97
  }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLoopIdentity = isLoopIdentity;
4
+ exports.parseLoopRefBinding = parseLoopRefBinding;
5
+ exports.parseLoopRefBindings = parseLoopRefBindings;
6
+ exports.groupLoopRefBindings = groupLoopRefBindings;
7
+ const LOOP_IDENTITY_RE = /^[a-z0-9][a-z0-9._-]*$/;
8
+ function isLoopIdentity(value) {
9
+ return LOOP_IDENTITY_RE.test(value);
10
+ }
11
+ function parseLoopRefBinding(value) {
12
+ const separator = value.indexOf("=");
13
+ if (separator <= 0 || separator === value.length - 1) {
14
+ return undefined;
15
+ }
16
+ const identity = value.slice(0, separator).trim().toLowerCase();
17
+ const ref = value.slice(separator + 1).trim();
18
+ if (!isLoopIdentity(identity) || !ref) {
19
+ return undefined;
20
+ }
21
+ return { identity, ref };
22
+ }
23
+ function parseLoopRefBindings(values) {
24
+ return values.flatMap((value) => {
25
+ const binding = parseLoopRefBinding(value);
26
+ return binding ? [binding] : [];
27
+ });
28
+ }
29
+ function groupLoopRefBindings(bindings) {
30
+ const grouped = new Map();
31
+ for (const binding of bindings) {
32
+ const refs = grouped.get(binding.identity) ?? [];
33
+ if (!refs.includes(binding.ref)) {
34
+ refs.push(binding.ref);
35
+ }
36
+ grouped.set(binding.identity, refs);
37
+ }
38
+ return grouped;
39
+ }
@@ -8,9 +8,10 @@ const agent_file_types_1 = require("./agent_file_types");
8
8
  const archive_file_1 = require("./archive_file");
9
9
  const id_1 = require("../util/id");
10
10
  const refs_1 = require("../util/refs");
11
+ const loop_bindings_1 = require("./loop_bindings");
11
12
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
12
13
  const DEC_ID_RE = /^dec-[0-9]+$/;
13
- exports.WORK_TYPES = new Set(["goal", "epic", "feat", "task", "bug", "spike", "checkpoint", "test"]);
14
+ exports.WORK_TYPES = new Set(["goal", "loop", "epic", "feat", "task", "bug", "spike", "checkpoint", "test"]);
14
15
  exports.DEC_TYPES = new Set(["dec"]);
15
16
  exports.ALLOWED_TYPES = new Set([
16
17
  "rule",
@@ -19,6 +20,7 @@ exports.ALLOWED_TYPES = new Set([
19
20
  "dec",
20
21
  "prop",
21
22
  "goal",
23
+ "loop",
22
24
  "epic",
23
25
  "feat",
24
26
  "task",
@@ -43,6 +45,45 @@ const GOAL_ATTRIBUTE_KEYS = [
43
45
  "max_iterations",
44
46
  "blocked_after_attempts",
45
47
  ];
48
+ const LOOP_MODES = new Set([
49
+ "readonly",
50
+ "planning",
51
+ "patch_proposal",
52
+ "write_with_approval",
53
+ "autonomous_local",
54
+ ]);
55
+ const LOOP_ROLES = new Set(["template", "scoped", "run_bearing"]);
56
+ const LOOP_MATERIALIZATION_MODES = new Set(["default_children", "planning_only", "manual"]);
57
+ const LOOP_BLOCKER_POLICIES = new Set(["spike_proposal_recommendation_continue"]);
58
+ const LOOP_ATTRIBUTE_KEYS = [
59
+ "loop_mode",
60
+ "loop_role",
61
+ "scope_refs",
62
+ "scope_description",
63
+ "template_refs",
64
+ "materialization_mode",
65
+ "child_refs",
66
+ "pre_run_questions",
67
+ "question_answer_refs",
68
+ "pre_approved_actions",
69
+ "approval_gated_actions",
70
+ "required_actions",
71
+ "requested_actions",
72
+ "prohibited_actions",
73
+ "action_approval_refs",
74
+ "evidence_lanes",
75
+ "evidence_lane_refs",
76
+ "lane_waiver_refs",
77
+ "lane_waiver_decision_refs",
78
+ "lane_waiver_approval_refs",
79
+ "run_refs",
80
+ "decision_refs",
81
+ "output_refs",
82
+ "approval_refs",
83
+ "evaluation_refs",
84
+ "definition_of_done",
85
+ "blocker_policy",
86
+ ];
46
87
  function formatError(filePath, message) {
47
88
  return new Error(`${filePath}: ${message}`);
48
89
  }
@@ -215,6 +256,158 @@ function validateGoalFrontmatter(type, frontmatter, filePath) {
215
256
  parsePositiveIntegerString(blockedAfterAttempts, "blocked_after_attempts", filePath);
216
257
  }
217
258
  }
259
+ function validateLoopRefList(frontmatter, key, filePath) {
260
+ const values = optionalList(frontmatter, key, filePath);
261
+ for (const [index, value] of values.entries()) {
262
+ if (typeof value !== "string" || value.trim().length === 0) {
263
+ throw formatError(filePath, `${key}[${index}] must not be empty`);
264
+ }
265
+ const normalized = value.includes("://") ? value : requireLowercase(value, `${key}[${index}]`, filePath);
266
+ if (!(0, refs_1.validatePortableOrUriRef)(normalized)) {
267
+ throw formatError(filePath, `${key}[${index}] must be a portable id, qid, or URI ref`);
268
+ }
269
+ }
270
+ }
271
+ function validateLoopStringList(frontmatter, key, filePath) {
272
+ const values = optionalList(frontmatter, key, filePath);
273
+ const seen = new Set();
274
+ for (const [index, value] of values.entries()) {
275
+ if (typeof value !== "string" || value.trim().length === 0) {
276
+ throw formatError(filePath, `${key}[${index}] must not be empty`);
277
+ }
278
+ const normalized = value.trim().toLowerCase();
279
+ if (!(0, loop_bindings_1.isLoopIdentity)(normalized)) {
280
+ throw formatError(filePath, `${key}[${index}] must be a stable lowercase identity`);
281
+ }
282
+ if (value.trim() !== normalized) {
283
+ throw formatError(filePath, `${key}[${index}] must be lowercase; use ${normalized}`);
284
+ }
285
+ if (seen.has(normalized)) {
286
+ throw formatError(filePath, `${key}[${index}] duplicates identity ${normalized}`);
287
+ }
288
+ seen.add(normalized);
289
+ }
290
+ }
291
+ function validateLoopBindingList(frontmatter, key, identityKey, refKeys, filePath, requireEveryRefKey = false) {
292
+ const values = optionalList(frontmatter, key, filePath);
293
+ const identities = new Set(optionalList(frontmatter, identityKey, filePath).map((value) => value.toLowerCase()));
294
+ const refsByKey = refKeys.map((refKey) => new Set(optionalList(frontmatter, refKey, filePath)));
295
+ const seen = new Set();
296
+ for (const [index, value] of values.entries()) {
297
+ if (typeof value !== "string") {
298
+ throw formatError(filePath, `${key}[${index}] must use <identity>=<ref>`);
299
+ }
300
+ const binding = (0, loop_bindings_1.parseLoopRefBinding)(value);
301
+ if (!binding) {
302
+ throw formatError(filePath, `${key}[${index}] must use <identity>=<ref>`);
303
+ }
304
+ if (!identities.has(binding.identity)) {
305
+ throw formatError(filePath, `${key}[${index}] references undeclared ${identityKey} identity ${binding.identity}`);
306
+ }
307
+ const normalizedRef = binding.ref.includes("://")
308
+ ? binding.ref
309
+ : requireLowercase(binding.ref, `${key}[${index}]`, filePath);
310
+ if (!(0, refs_1.validatePortableOrUriRef)(normalizedRef)) {
311
+ throw formatError(filePath, `${key}[${index}] must reference a portable id, qid, or URI ref`);
312
+ }
313
+ const included = requireEveryRefKey
314
+ ? refsByKey.every((refs) => refs.has(binding.ref))
315
+ : refsByKey.some((refs) => refs.has(binding.ref));
316
+ if (!included) {
317
+ const operator = requireEveryRefKey ? "each of" : "one of";
318
+ throw formatError(filePath, `${key}[${index}] ref ${binding.ref} must also appear in ${operator} ${refKeys.join(", ")}`);
319
+ }
320
+ const pair = `${binding.identity}\u0000${binding.ref}`;
321
+ if (seen.has(pair)) {
322
+ throw formatError(filePath, `${key}[${index}] duplicates ${binding.identity}=${binding.ref}`);
323
+ }
324
+ seen.add(pair);
325
+ }
326
+ }
327
+ function validateLoopActionSets(frontmatter, filePath) {
328
+ const normalized = (key) => optionalList(frontmatter, key, filePath).map((value) => value.trim().toLowerCase());
329
+ const preApproved = new Set(normalized("pre_approved_actions"));
330
+ const approvalGated = new Set(normalized("approval_gated_actions"));
331
+ const required = normalized("required_actions");
332
+ const requested = normalized("requested_actions");
333
+ const prohibited = new Set(normalized("prohibited_actions"));
334
+ for (const action of preApproved) {
335
+ if (approvalGated.has(action)) {
336
+ throw formatError(filePath, `action ${action} cannot be both pre_approved_actions and approval_gated_actions`);
337
+ }
338
+ if (prohibited.has(action)) {
339
+ throw formatError(filePath, `action ${action} cannot be both pre_approved_actions and prohibited_actions`);
340
+ }
341
+ }
342
+ for (const action of approvalGated) {
343
+ if (prohibited.has(action)) {
344
+ throw formatError(filePath, `action ${action} cannot be both approval_gated_actions and prohibited_actions`);
345
+ }
346
+ }
347
+ for (const [key, actions] of [["required_actions", required], ["requested_actions", requested]]) {
348
+ for (const action of actions) {
349
+ if (!preApproved.has(action) && !approvalGated.has(action)) {
350
+ throw formatError(filePath, `${key} references undeclared action ${action}`);
351
+ }
352
+ if (prohibited.has(action)) {
353
+ throw formatError(filePath, `${key} cannot include prohibited action ${action}`);
354
+ }
355
+ }
356
+ }
357
+ }
358
+ function validateLoopFrontmatter(type, frontmatter, filePath) {
359
+ if (type !== "loop") {
360
+ return;
361
+ }
362
+ const loopMode = requireLowercase(expectString(frontmatter, "loop_mode", filePath), "loop_mode", filePath);
363
+ if (!LOOP_MODES.has(loopMode)) {
364
+ throw formatError(filePath, `loop_mode must be one of ${Array.from(LOOP_MODES).join(", ")}`);
365
+ }
366
+ const loopRole = requireLowercase(expectString(frontmatter, "loop_role", filePath), "loop_role", filePath);
367
+ if (!LOOP_ROLES.has(loopRole)) {
368
+ throw formatError(filePath, `loop_role must be one of ${Array.from(LOOP_ROLES).join(", ")}`);
369
+ }
370
+ const materializationMode = requireLowercase(expectString(frontmatter, "materialization_mode", filePath), "materialization_mode", filePath);
371
+ if (!LOOP_MATERIALIZATION_MODES.has(materializationMode)) {
372
+ throw formatError(filePath, `materialization_mode must be one of ${Array.from(LOOP_MATERIALIZATION_MODES).join(", ")}`);
373
+ }
374
+ const blockerPolicy = requireLowercase(expectString(frontmatter, "blocker_policy", filePath), "blocker_policy", filePath);
375
+ if (!LOOP_BLOCKER_POLICIES.has(blockerPolicy)) {
376
+ throw formatError(filePath, `blocker_policy must be one of ${Array.from(LOOP_BLOCKER_POLICIES).join(", ")}`);
377
+ }
378
+ expectString(frontmatter, "definition_of_done", filePath);
379
+ optionalString(frontmatter, "scope_description", filePath);
380
+ for (const key of [
381
+ "pre_run_questions",
382
+ "pre_approved_actions",
383
+ "approval_gated_actions",
384
+ "required_actions",
385
+ "requested_actions",
386
+ "prohibited_actions",
387
+ "evidence_lanes",
388
+ ]) {
389
+ validateLoopStringList(frontmatter, key, filePath);
390
+ }
391
+ validateLoopActionSets(frontmatter, filePath);
392
+ for (const key of [
393
+ "scope_refs",
394
+ "template_refs",
395
+ "child_refs",
396
+ "lane_waiver_refs",
397
+ "run_refs",
398
+ "decision_refs",
399
+ "output_refs",
400
+ "approval_refs",
401
+ "evaluation_refs",
402
+ ]) {
403
+ validateLoopRefList(frontmatter, key, filePath);
404
+ }
405
+ validateLoopBindingList(frontmatter, "question_answer_refs", "pre_run_questions", ["decision_refs"], filePath);
406
+ validateLoopBindingList(frontmatter, "action_approval_refs", "approval_gated_actions", ["approval_refs"], filePath);
407
+ validateLoopBindingList(frontmatter, "evidence_lane_refs", "evidence_lanes", ["run_refs", "output_refs", "evaluation_refs", "evidence_refs"], filePath);
408
+ validateLoopBindingList(frontmatter, "lane_waiver_decision_refs", "evidence_lanes", ["lane_waiver_refs", "decision_refs"], filePath, true);
409
+ validateLoopBindingList(frontmatter, "lane_waiver_approval_refs", "evidence_lanes", ["approval_refs"], filePath);
410
+ }
218
411
  function extractGoalAttributes(type, frontmatter) {
219
412
  if (type !== "goal") {
220
413
  return {};
@@ -228,6 +421,19 @@ function extractGoalAttributes(type, frontmatter) {
228
421
  }
229
422
  return attributes;
230
423
  }
424
+ function extractLoopAttributes(type, frontmatter) {
425
+ if (type !== "loop") {
426
+ return {};
427
+ }
428
+ const attributes = {};
429
+ for (const key of LOOP_ATTRIBUTE_KEYS) {
430
+ const value = frontmatter[key];
431
+ if (value !== undefined) {
432
+ attributes[key] = value;
433
+ }
434
+ }
435
+ return attributes;
436
+ }
231
437
  function requireTemplateSchema(type, templateSchemas, filePath) {
232
438
  const schema = templateSchemas[type];
233
439
  if (!schema) {
@@ -306,6 +512,7 @@ function parseNode(content, filePath, options) {
306
512
  (0, agent_file_types_1.validateAgentFrontmatter)(type, frontmatter, filePath);
307
513
  (0, archive_file_1.validateArchiveFrontmatter)(type, frontmatter, filePath);
308
514
  validateGoalFrontmatter(type, frontmatter, filePath);
515
+ validateLoopFrontmatter(type, frontmatter, filePath);
309
516
  const idValue = requireLowercase(expectString(frontmatter, "id", filePath), "id", filePath);
310
517
  const id = isPortableType
311
518
  ? requirePortableIdFormat(idValue, "id", filePath)
@@ -376,6 +583,7 @@ function parseNode(content, filePath, options) {
376
583
  });
377
584
  const attributes = {
378
585
  ...extractGoalAttributes(type, frontmatter),
586
+ ...extractLoopAttributes(type, frontmatter),
379
587
  ...(0, agent_file_types_1.extractAgentAttributes)(type, frontmatter),
380
588
  ...(0, archive_file_1.extractArchiveAttributes)(type, frontmatter),
381
589
  };