mdkg 0.4.2 → 0.5.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +71 -15
  2. package/CLI_COMMAND_MATRIX.md +59 -2
  3. package/README.md +20 -2
  4. package/dist/cli.js +124 -2
  5. package/dist/command-contract.json +756 -2
  6. package/dist/commands/archive.js +31 -14
  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
@@ -22,6 +22,131 @@ const MANIFEST_ENTRY = "manifest.json";
22
22
  const GLOBAL_INDEX_ENTRY = ".mdkg/index/global.json";
23
23
  const SKILLS_INDEX_ENTRY = ".mdkg/index/skills.json";
24
24
  const CAPABILITIES_INDEX_ENTRY = ".mdkg/index/capabilities.json";
25
+ const REQUIRED_INDEX_ENTRIES = [GLOBAL_INDEX_ENTRY, SKILLS_INDEX_ENTRY, CAPABILITIES_INDEX_ENTRY];
26
+ const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
27
+ function isRecord(value) {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+ function requireString(value, field) {
31
+ if (typeof value !== "string" || value.length === 0)
32
+ throw new Error(`bundle manifest invalid: ${field} must be a non-empty string`);
33
+ return value;
34
+ }
35
+ function requireHash(value, field) {
36
+ const result = requireString(value, field);
37
+ if (!SHA256_PATTERN.test(result))
38
+ throw new Error(`bundle manifest invalid: ${field} must be a sha256 digest`);
39
+ return result;
40
+ }
41
+ function requireSafePath(value, field) {
42
+ const result = requireString(value, field).replace(/\\/g, "/");
43
+ if (path_1.default.posix.isAbsolute(result) || result.split("/").some((part) => part === "" || part === "." || part === "..")) {
44
+ throw new Error(`bundle manifest invalid: ${field} must be a safe relative path`);
45
+ }
46
+ return result;
47
+ }
48
+ function manifestFilesHash(files) {
49
+ const data = `${JSON.stringify(files.map((file) => ({
50
+ path: file.path,
51
+ kind: file.kind,
52
+ workspace: file.workspace,
53
+ visibility: file.visibility,
54
+ size: file.size,
55
+ sha256: file.sha256,
56
+ })), null, 2)}\n`;
57
+ return sha256Buffer(Buffer.from(data, "utf8"));
58
+ }
59
+ function validateForeignManifest(value) {
60
+ if (!isRecord(value))
61
+ throw new Error("bundle manifest invalid: root must be an object");
62
+ if (value.manifest_version !== 1 || value.tool !== "mdkg")
63
+ throw new Error("bundle manifest invalid: unsupported version or tool");
64
+ const mdkgVersion = requireString(value.mdkg_version, "mdkg_version");
65
+ if (value.profile !== "private" && value.profile !== "public")
66
+ throw new Error("bundle manifest invalid: profile must be private or public");
67
+ if (!Array.isArray(value.selected_workspaces) || value.selected_workspaces.length === 0) {
68
+ throw new Error("bundle manifest invalid: selected_workspaces must be a non-empty array");
69
+ }
70
+ const selectedWorkspaces = value.selected_workspaces.map((item, index) => requireString(item, `selected_workspaces[${index}]`));
71
+ if (new Set(selectedWorkspaces).size !== selectedWorkspaces.length)
72
+ throw new Error("bundle manifest invalid: duplicate selected workspace");
73
+ if (!isRecord(value.source))
74
+ throw new Error("bundle manifest invalid: source must be an object");
75
+ const repo = requireString(value.source.repo, "source.repo");
76
+ const gitHead = value.source.git_head;
77
+ if (gitHead !== null && (typeof gitHead !== "string" || !/^[a-f0-9]{40}$/.test(gitHead))) {
78
+ throw new Error("bundle manifest invalid: source.git_head must be null or a Git SHA");
79
+ }
80
+ if (typeof value.source.dirty !== "boolean")
81
+ throw new Error("bundle manifest invalid: source.dirty must be boolean");
82
+ if (!Number.isSafeInteger(value.file_count) || Number(value.file_count) < 0)
83
+ throw new Error("bundle manifest invalid: file_count");
84
+ if (!isRecord(value.index_hashes) || !Array.isArray(value.files))
85
+ throw new Error("bundle manifest invalid: indexes and files are required");
86
+ const indexHashes = {};
87
+ for (const [entryPath, hash] of Object.entries(value.index_hashes))
88
+ indexHashes[requireSafePath(entryPath, "index hash path")] = requireHash(hash, `index_hashes.${entryPath}`);
89
+ const seen = new Set();
90
+ const files = value.files.map((item, index) => {
91
+ if (!isRecord(item))
92
+ throw new Error(`bundle manifest invalid: files[${index}] must be an object`);
93
+ const filePath = requireSafePath(item.path, `files[${index}].path`);
94
+ if (seen.has(filePath))
95
+ throw new Error(`bundle manifest invalid: duplicate file ${filePath}`);
96
+ seen.add(filePath);
97
+ if (item.kind !== "authored" && item.kind !== "archive_cache" && item.kind !== "generated_index")
98
+ throw new Error(`bundle manifest invalid: files[${index}].kind`);
99
+ if (!Number.isSafeInteger(item.size) || Number(item.size) < 0)
100
+ throw new Error(`bundle manifest invalid: files[${index}].size`);
101
+ const workspace = item.workspace === undefined ? undefined : requireString(item.workspace, `files[${index}].workspace`);
102
+ if (item.visibility !== undefined && item.visibility !== "private" && item.visibility !== "internal" && item.visibility !== "public")
103
+ throw new Error(`bundle manifest invalid: files[${index}].visibility`);
104
+ return { path: filePath, kind: item.kind, size: Number(item.size), sha256: requireHash(item.sha256, `files[${index}].sha256`), ...(workspace ? { workspace } : {}), ...(item.visibility ? { visibility: item.visibility } : {}) };
105
+ });
106
+ if (Number(value.file_count) !== files.length)
107
+ throw new Error("bundle manifest invalid: file_count mismatch");
108
+ for (const required of REQUIRED_INDEX_ENTRIES) {
109
+ const row = files.find((file) => file.path === required);
110
+ if (!row || row.kind !== "generated_index" || row.sha256 !== indexHashes[required])
111
+ throw new Error(`bundle manifest invalid: required index contract mismatch for ${required}`);
112
+ }
113
+ return {
114
+ manifest_version: 1,
115
+ tool: "mdkg",
116
+ mdkg_version: mdkgVersion,
117
+ profile: value.profile,
118
+ selected_workspaces: selectedWorkspaces,
119
+ source: { repo, git_head: gitHead, dirty: value.source.dirty },
120
+ source_tree_hash: requireHash(value.source_tree_hash, "source_tree_hash"),
121
+ bundle_hash: requireHash(value.bundle_hash, "bundle_hash"),
122
+ file_count: files.length,
123
+ index_hashes: indexHashes,
124
+ files,
125
+ };
126
+ }
127
+ function validateIndexShape(value) {
128
+ if (!isRecord(value) || !isRecord(value.meta) || !isRecord(value.nodes) || !isRecord(value.reverse_edges) || !isRecord(value.workspaces)) {
129
+ throw new Error("generated global index has an invalid shape");
130
+ }
131
+ return value;
132
+ }
133
+ function publicProjectionErrors(index, manifest) {
134
+ if (manifest.profile !== "public")
135
+ return [];
136
+ const selected = new Set(manifest.selected_workspaces);
137
+ const rows = new Map(manifest.files.map((file) => [file.path, file]));
138
+ const errors = [];
139
+ for (const alias of Object.keys(index.workspaces)) {
140
+ if (!selected.has(alias))
141
+ errors.push(`public bundle contains unselected workspace ${alias}`);
142
+ }
143
+ for (const node of Object.values(index.nodes)) {
144
+ const row = rows.get(toPosixPath(node.path));
145
+ if (!selected.has(node.ws) || !row || row.visibility !== "public")
146
+ errors.push(`public bundle contains non-public node ${node.qid}`);
147
+ }
148
+ return errors;
149
+ }
25
150
  function toPosixPath(value) {
26
151
  return value.split(path_1.default.sep).join("/");
27
152
  }
@@ -36,7 +161,7 @@ function readJsonEntry(entries, entryPath) {
36
161
  return JSON.parse(data.toString("utf8"));
37
162
  }
38
163
  function readBundleEntries(bundlePath) {
39
- return new Map((0, zip_1.readZipEntries)(fs_1.default.readFileSync(bundlePath)).map((entry) => [entry.name, entry.data]));
164
+ return new Map((0, zip_1.readZipFileEntries)(bundlePath).map((entry) => [entry.name, entry.data]));
40
165
  }
41
166
  function resolveBundlePath(root, source) {
42
167
  return path_1.default.resolve(root, source.path);
@@ -126,6 +251,15 @@ function entryHashErrors(entries, manifest) {
126
251
  errors.push(`size mismatch for ${file.path}`);
127
252
  }
128
253
  }
254
+ for (const required of REQUIRED_INDEX_ENTRIES) {
255
+ const data = entries.get(required);
256
+ if (data && sha256Buffer(data) !== manifest.index_hashes[required])
257
+ errors.push(`generated index hash mismatch: ${required}`);
258
+ }
259
+ if (manifestFilesHash(manifest.files.filter((file) => file.kind !== "generated_index")) !== manifest.source_tree_hash)
260
+ errors.push("source_tree_hash mismatch");
261
+ if (manifestFilesHash(manifest.files) !== manifest.bundle_hash)
262
+ errors.push("bundle_hash mismatch");
129
263
  return errors;
130
264
  }
131
265
  function projectOneSource(root, subgraph, source) {
@@ -159,11 +293,14 @@ function projectOneSource(root, subgraph, source) {
159
293
  throw new Error(`bundle not found: ${source.path}`);
160
294
  }
161
295
  entries = readBundleEntries(bundlePath);
162
- manifest = readJsonEntry(entries, MANIFEST_ENTRY);
163
- index = readJsonEntry(entries, GLOBAL_INDEX_ENTRY);
164
- readJsonEntry(entries, SKILLS_INDEX_ENTRY);
165
- readJsonEntry(entries, CAPABILITIES_INDEX_ENTRY);
296
+ manifest = validateForeignManifest(readJsonEntry(entries, MANIFEST_ENTRY));
297
+ index = validateIndexShape(readJsonEntry(entries, GLOBAL_INDEX_ENTRY));
298
+ if (!isRecord(readJsonEntry(entries, SKILLS_INDEX_ENTRY)))
299
+ throw new Error("generated skills index has an invalid shape");
300
+ if (!isRecord(readJsonEntry(entries, CAPABILITIES_INDEX_ENTRY)))
301
+ throw new Error("generated capabilities index has an invalid shape");
166
302
  errors.push(...entryHashErrors(entries, manifest));
303
+ errors.push(...publicProjectionErrors(index, manifest));
167
304
  if (manifest.profile !== source.expected_profile) {
168
305
  errors.push(`expected ${source.expected_profile} bundle but found ${manifest.profile}`);
169
306
  }
@@ -6,6 +6,7 @@ const agent_file_types_1 = require("./agent_file_types");
6
6
  const refs_1 = require("../util/refs");
7
7
  const qid_1 = require("../util/qid");
8
8
  const goal_scope_1 = require("./goal_scope");
9
+ const loop_bindings_1 = require("./loop_bindings");
9
10
  function pushError(errors, message) {
10
11
  if (errors) {
11
12
  errors.push(message);
@@ -337,12 +338,11 @@ function validateAgentWorkflowDisputeRefs(index, allowMissing, externalWorkspace
337
338
  }
338
339
  }
339
340
  function buildNodeIdsByWorkspace(index) {
340
- const nodeIdsByWorkspace = {};
341
+ const nodeIdsByWorkspace = new Map();
341
342
  for (const node of Object.values(index.nodes)) {
342
- if (!nodeIdsByWorkspace[node.ws]) {
343
- nodeIdsByWorkspace[node.ws] = new Set();
344
- }
345
- nodeIdsByWorkspace[node.ws].add(node.id);
343
+ const ids = nodeIdsByWorkspace.get(node.ws) ?? new Set();
344
+ ids.add(node.id);
345
+ nodeIdsByWorkspace.set(node.ws, ids);
346
346
  }
347
347
  return nodeIdsByWorkspace;
348
348
  }
@@ -351,10 +351,10 @@ function validateAgentWorkflowNodeIdRef(qid, ws, field, value, nodeIdsByWorkspac
351
351
  if (workspace && value.includes(":") && externalWorkspaces?.has(workspace)) {
352
352
  return;
353
353
  }
354
- if (value.includes(":") && nodeIdsByWorkspace[workspace]?.has(value.split(":").slice(1).join(":"))) {
354
+ if (value.includes(":") && nodeIdsByWorkspace.get(workspace)?.has(value.split(":").slice(1).join(":"))) {
355
355
  return;
356
356
  }
357
- if (nodeIdsByWorkspace[ws]?.has(value)) {
357
+ if (nodeIdsByWorkspace.get(ws)?.has(value)) {
358
358
  return;
359
359
  }
360
360
  if (allowSkillRef &&
@@ -393,15 +393,14 @@ function validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSki
393
393
  }
394
394
  }
395
395
  function buildArchiveIdsByWorkspace(index) {
396
- const archiveIdsByWorkspace = {};
396
+ const archiveIdsByWorkspace = new Map();
397
397
  for (const node of Object.values(index.nodes)) {
398
398
  if (node.type !== "archive") {
399
399
  continue;
400
400
  }
401
- if (!archiveIdsByWorkspace[node.ws]) {
402
- archiveIdsByWorkspace[node.ws] = new Set();
403
- }
404
- archiveIdsByWorkspace[node.ws].add(node.id);
401
+ const ids = archiveIdsByWorkspace.get(node.ws) ?? new Set();
402
+ ids.add(node.id);
403
+ archiveIdsByWorkspace.set(node.ws, ids);
405
404
  }
406
405
  return archiveIdsByWorkspace;
407
406
  }
@@ -414,7 +413,7 @@ function validateArchiveUriValue(qid, ws, field, value, archiveIdsByWorkspace, a
414
413
  pushError(errors, `${qid}: ${field} has malformed archive ref ${value}`);
415
414
  return;
416
415
  }
417
- if (archiveIdsByWorkspace[ws]?.has(archiveId)) {
416
+ if (archiveIdsByWorkspace.get(ws)?.has(archiveId)) {
418
417
  return;
419
418
  }
420
419
  if (allowMissing) {
@@ -496,6 +495,102 @@ function validateGoalRefs(index, allowMissing, errors) {
496
495
  }
497
496
  }
498
497
  }
498
+ function loopAttributeList(node, key) {
499
+ const value = node.attributes[key];
500
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
501
+ }
502
+ function resolveLoopEvidenceNode(index, ws, ref) {
503
+ const resolved = (0, qid_1.resolveQid)(index, ref, ws);
504
+ return resolved.status === "ok" ? index.nodes[resolved.qid] : undefined;
505
+ }
506
+ function isAcceptedLoopDecision(node) {
507
+ return node.type === "dec" && node.status === "accepted";
508
+ }
509
+ function isVerifiedLoopApproval(node) {
510
+ if (node.type === "dec") {
511
+ return node.status === "accepted";
512
+ }
513
+ if (node.type === "checkpoint") {
514
+ return node.status === "done";
515
+ }
516
+ return node.type === "receipt" && node.attributes.receipt_status === "verified";
517
+ }
518
+ function validateLoopTypedRefs(index, allowMissing, errors) {
519
+ for (const [qid, node] of Object.entries(index.nodes)) {
520
+ if (node.type !== "loop") {
521
+ continue;
522
+ }
523
+ for (const ref of loopAttributeList(node, "decision_refs")) {
524
+ if ((0, refs_1.isUriRef)(ref)) {
525
+ pushError(errors, `${qid}: decision_refs requires a local accepted dec ref, got ${ref}`);
526
+ continue;
527
+ }
528
+ const target = resolveLoopEvidenceNode(index, node.ws, ref);
529
+ if (!target) {
530
+ if (!allowMissing) {
531
+ pushError(errors, `${qid}: decision_refs references missing node ${ref}`);
532
+ }
533
+ continue;
534
+ }
535
+ if (!isAcceptedLoopDecision(target)) {
536
+ pushError(errors, `${qid}: decision_refs ${ref} must target an accepted dec, got ${target.type}:${target.status ?? "none"}`);
537
+ }
538
+ }
539
+ for (const ref of loopAttributeList(node, "approval_refs")) {
540
+ if ((0, refs_1.isUriRef)(ref)) {
541
+ continue;
542
+ }
543
+ const target = resolveLoopEvidenceNode(index, node.ws, ref);
544
+ if (!target) {
545
+ if (!allowMissing) {
546
+ pushError(errors, `${qid}: approval_refs references missing node ${ref}`);
547
+ }
548
+ continue;
549
+ }
550
+ if (!isVerifiedLoopApproval(target)) {
551
+ pushError(errors, `${qid}: approval_refs ${ref} must target an accepted dec, done checkpoint, or verified receipt, got ${target.type}:${target.status ?? target.attributes.receipt_status ?? "none"}`);
552
+ }
553
+ }
554
+ const checks = [
555
+ { key: "question_answer_refs", kind: "decision" },
556
+ { key: "action_approval_refs", kind: "approval" },
557
+ { key: "evidence_lane_refs", kind: "evidence" },
558
+ { key: "lane_waiver_decision_refs", kind: "decision" },
559
+ { key: "lane_waiver_approval_refs", kind: "approval" },
560
+ ];
561
+ for (const check of checks) {
562
+ const bindings = (0, loop_bindings_1.parseLoopRefBindings)(loopAttributeList(node, check.key));
563
+ for (const binding of bindings) {
564
+ if ((0, refs_1.isUriRef)(binding.ref)) {
565
+ if (check.kind === "decision") {
566
+ pushError(errors, `${qid}: ${check.key} ${binding.identity} requires a local accepted dec ref, got ${binding.ref}`);
567
+ }
568
+ continue;
569
+ }
570
+ const target = resolveLoopEvidenceNode(index, node.ws, binding.ref);
571
+ if (!target) {
572
+ if (!allowMissing) {
573
+ pushError(errors, `${qid}: ${check.key} ${binding.identity} references missing node ${binding.ref}`);
574
+ }
575
+ continue;
576
+ }
577
+ if (check.kind === "decision" && !isAcceptedLoopDecision(target)) {
578
+ pushError(errors, `${qid}: ${check.key} ${binding.identity} must target an accepted dec`);
579
+ }
580
+ if (check.kind === "approval" && !isVerifiedLoopApproval(target)) {
581
+ pushError(errors, `${qid}: ${check.key} ${binding.identity} must target an accepted dec, done checkpoint, or verified receipt`);
582
+ }
583
+ }
584
+ }
585
+ const waiverDecisionIdentities = new Set((0, loop_bindings_1.parseLoopRefBindings)(loopAttributeList(node, "lane_waiver_decision_refs")).map((binding) => binding.identity));
586
+ const waiverApprovalIdentities = new Set((0, loop_bindings_1.parseLoopRefBindings)(loopAttributeList(node, "lane_waiver_approval_refs")).map((binding) => binding.identity));
587
+ for (const identity of new Set([...waiverDecisionIdentities, ...waiverApprovalIdentities])) {
588
+ if (!waiverDecisionIdentities.has(identity) || !waiverApprovalIdentities.has(identity)) {
589
+ pushError(errors, `${qid}: lane waiver ${identity} requires both decision and approval bindings`);
590
+ }
591
+ }
592
+ }
593
+ }
499
594
  function validateSingleActiveRootGoals(index, errors) {
500
595
  const activeByWorkspace = {};
501
596
  for (const [qid, node] of Object.entries(index.nodes)) {
@@ -563,6 +658,7 @@ function collectGraphErrors(index, options = {}) {
563
658
  validateAgentWorkflowDisputeRefs(index, allowMissing, externalWorkspaces, errors);
564
659
  validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors);
565
660
  validateArchiveUriRefs(index, allowMissing, errors);
661
+ validateLoopTypedRefs(index, allowMissing, errors);
566
662
  validateGoalRefs(index, allowMissing, errors);
567
663
  validateSingleActiveRootGoals(index, errors);
568
664
  detectPrevNextCycles(index, errors);
@@ -8,28 +8,51 @@ exports.listWorkspaceDocFiles = listWorkspaceDocFiles;
8
8
  exports.listWorkspaceDocFilesByAlias = listWorkspaceDocFilesByAlias;
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const filesystem_authority_1 = require("../core/filesystem_authority");
12
+ const workspace_path_1 = require("../core/workspace_path");
11
13
  const DOC_FOLDERS = ["core", "design", "work"];
12
- function listMarkdownFiles(dir) {
14
+ function accountMarkdownFile(filePath, budget) {
15
+ const size = fs_1.default.statSync(filePath).size;
16
+ if (size > budget.limits.max_file_bytes) {
17
+ throw new Error(`graph file exceeds index.limits.max_file_bytes (${budget.limits.max_file_bytes}): ${filePath}`);
18
+ }
19
+ if (budget.files + 1 > budget.limits.max_files) {
20
+ throw new Error(`graph file count exceeds index.limits.max_files (${budget.limits.max_files})`);
21
+ }
22
+ if (budget.bytes + size > budget.limits.max_total_bytes) {
23
+ throw new Error(`graph bytes exceed index.limits.max_total_bytes (${budget.limits.max_total_bytes})`);
24
+ }
25
+ budget.files += 1;
26
+ budget.bytes += size;
27
+ }
28
+ function listMarkdownFiles(dir, budget, depth = 0) {
13
29
  if (!fs_1.default.existsSync(dir)) {
14
30
  return [];
15
31
  }
32
+ if (depth > budget.limits.max_depth) {
33
+ throw new Error(`graph directory depth exceeds index.limits.max_depth (${budget.limits.max_depth}): ${dir}`);
34
+ }
16
35
  const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
17
36
  const files = [];
18
37
  for (const entry of entries) {
19
38
  const fullPath = path_1.default.join(dir, entry.name);
20
39
  if (entry.isDirectory()) {
21
- files.push(...listMarkdownFiles(fullPath));
40
+ files.push(...listMarkdownFiles(fullPath, budget, depth + 1));
22
41
  }
23
42
  else if (entry.isFile() && entry.name.endsWith(".md")) {
43
+ accountMarkdownFile(fullPath, budget);
24
44
  files.push(fullPath);
25
45
  }
26
46
  }
27
47
  return files;
28
48
  }
29
- function listArchiveSidecarFiles(dir) {
49
+ function listArchiveSidecarFiles(dir, budget, depth = 0) {
30
50
  if (!fs_1.default.existsSync(dir)) {
31
51
  return [];
32
52
  }
53
+ if (depth > budget.limits.max_depth) {
54
+ throw new Error(`graph directory depth exceeds index.limits.max_depth (${budget.limits.max_depth}): ${dir}`);
55
+ }
33
56
  const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
34
57
  const files = [];
35
58
  for (const entry of entries) {
@@ -38,9 +61,10 @@ function listArchiveSidecarFiles(dir) {
38
61
  }
39
62
  const fullPath = path_1.default.join(dir, entry.name);
40
63
  if (entry.isDirectory()) {
41
- files.push(...listArchiveSidecarFiles(fullPath));
64
+ files.push(...listArchiveSidecarFiles(fullPath, budget, depth + 1));
42
65
  }
43
66
  else if (entry.isFile() && entry.name.endsWith(".md")) {
67
+ accountMarkdownFile(fullPath, budget);
44
68
  files.push(fullPath);
45
69
  }
46
70
  }
@@ -54,31 +78,37 @@ function getWorkspaceDocRoots(root, config) {
54
78
  if (!entry.enabled) {
55
79
  continue;
56
80
  }
57
- const wsRoot = path_1.default.resolve(root, entry.path, entry.mdkg_dir);
81
+ const wsRoot = (0, filesystem_authority_1.withContainedPathSink)({
82
+ root,
83
+ relativePath: (0, workspace_path_1.workspaceDocumentRelativePath)(entry.path, entry.mdkg_dir),
84
+ operation: "read",
85
+ }, ({ absolutePath }) => absolutePath);
58
86
  roots.push({ alias, root: wsRoot });
59
87
  }
60
88
  return roots;
61
89
  }
62
90
  function listWorkspaceDocFiles(root, config) {
63
91
  const files = [];
92
+ const budget = { files: 0, bytes: 0, limits: config.index.limits };
64
93
  for (const { root: wsRoot } of getWorkspaceDocRoots(root, config)) {
65
94
  for (const folder of DOC_FOLDERS) {
66
95
  const folderPath = path_1.default.join(wsRoot, folder);
67
- files.push(...listMarkdownFiles(folderPath));
96
+ files.push(...listMarkdownFiles(folderPath, budget));
68
97
  }
69
- files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive")));
98
+ files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive"), budget));
70
99
  }
71
100
  return files;
72
101
  }
73
102
  function listWorkspaceDocFilesByAlias(root, config) {
74
103
  const result = {};
104
+ const budget = { files: 0, bytes: 0, limits: config.index.limits };
75
105
  for (const { alias, root: wsRoot } of getWorkspaceDocRoots(root, config)) {
76
106
  const files = [];
77
107
  for (const folder of DOC_FOLDERS) {
78
108
  const folderPath = path_1.default.join(wsRoot, folder);
79
- files.push(...listMarkdownFiles(folderPath));
109
+ files.push(...listMarkdownFiles(folderPath, budget));
80
110
  }
81
- files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive")));
111
+ files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive"), budget));
82
112
  files.sort();
83
113
  result[alias] = files;
84
114
  }
@@ -28,6 +28,11 @@ Agent operating prompt:
28
28
  - Use `mdkg goal activate <goal-id>` to make one local root goal active, then `mdkg goal next` to surface one scoped feature, task, bug, test, or spike at a time; normal `mdkg next` remains for non-goal concrete work.
29
29
  - Use `mdkg goal claim [goal-id] <work-id>` only after accepting the surfaced work item; `mdkg goal next` is read-only.
30
30
  - Treat goal `required_checks` as report-only guidance from mdkg. Run commands yourself, then record evidence in the goal or active work item.
31
+ - Use `mdkg loop show <loop>`, `mdkg loop plan <loop>`, and
32
+ `mdkg pack <loop>` for first-class loop nodes. Use
33
+ `mdkg skill show pursue-mdkg-loop` before executing a loop so the agent works
34
+ every authorized linked lane, records blocker recovery, and closes only when
35
+ the loop definition of done is satisfied or explicitly waived.
31
36
  - Record skill improvement candidates during normal goal execution; edit `SKILL.md` only when the active node is explicit skill-maintenance work.
32
37
  - Use `mdkg skill list`, `mdkg skill search`, and `mdkg skill show <slug>` for skill discovery.
33
38
  - Use `mdkg capability list/search/show` for deterministic skills, `MANIFEST.md` / legacy `SPEC.md`, `WORK.md`, core-doc, and design-doc capability discovery.
@@ -97,6 +102,16 @@ If an active goal is known:
97
102
  - `mdkg goal evaluate <goal-id>`
98
103
  - repeat until the goal condition is achieved, blocked, paused, or budget-limited
99
104
 
105
+ If an active loop is known:
106
+ - `mdkg loop show <loop-id> --json`
107
+ - `mdkg skill show pursue-mdkg-loop`
108
+ - `mdkg loop plan <loop-id> --json`
109
+ - `mdkg pack <loop-id> --pack-profile concise --dry-run --stats`
110
+ - answer or record pre-run questions and approval requirements
111
+ - work every authorized linked lane before marking the loop done or blocked
112
+ - keep the loop open while required lanes are incomplete and not explicitly
113
+ waived
114
+
100
115
  If no task is known:
101
116
  - `mdkg search "..."`
102
117
  - `mdkg show <id>`
@@ -113,6 +128,7 @@ Skill discovery:
113
128
  - `mdkg skill list --tags stage:execute --json`
114
129
  - `mdkg skill list --tags stage:review --json`
115
130
  - `mdkg skill show select-work-and-ground-context`
131
+ - `mdkg skill show pursue-mdkg-loop`
116
132
 
117
133
  Capability discovery:
118
134
  - `mdkg capability list --kind skill --json`
@@ -29,6 +29,7 @@ Primary commands:
29
29
  - `mdkg subgraph`
30
30
  - `mdkg work`
31
31
  - `mdkg goal`
32
+ - `mdkg loop`
32
33
  - `mdkg task`
33
34
  - `mdkg validate`
34
35
  - `mdkg status [--json]`
@@ -130,6 +131,7 @@ Validation commands:
130
131
  Node creation commands:
131
132
  - `mdkg new <type> "<title>" [options] [--json]`
132
133
  - `mdkg new goal "<title>" [options] [--json]`
134
+ - `mdkg new loop "<title>" [options] [--json]`
133
135
  - `mdkg new spike "<research question>" [options] [--json]`
134
136
 
135
137
  Agent workflow file type creation:
@@ -153,10 +155,22 @@ Agent workflow notes:
153
155
  - `manifest` and `work` scaffold as validation-clean standalone docs.
154
156
  - `work_order`, `receipt`, `feedback`, `dispute`, and `proposal` need real refs before strict `mdkg validate` passes.
155
157
  - `goal` nodes capture recursive objective state and required checks, but normal `mdkg next` does not select them.
158
+ - `loop` nodes capture reusable process state, fork lineage, child refs, run/evidence refs, blocker-continuation policy, and a high-bar definition of done; runtime execution remains outside mdkg.
156
159
  - `spike` nodes are actionable research/planning work under `.mdkg/work/`; use `mdkg task start|update|done` for lifecycle state.
157
160
  - Spikes record sources, findings, recommendations, follow-up node ideas, and skill candidates in Markdown body sections; they do not perform web search, execute research, create follow-up nodes, generate `SKILL.md`, or expose a `mdkg spike ...` namespace automatically.
158
161
  - after fresh init, run `mdkg index` before treating `mdkg doctor --strict --json` as a clean health gate; init writes source scaffold files and index writes generated caches.
159
162
 
163
+ Loop commands:
164
+ - `mdkg loop list [--json]`
165
+ - `mdkg loop show <loop-or-template> [--json]`
166
+ - `mdkg loop fork <template> --scope <scope> [--materialization default_children|planning_only|manual] [--planning-only] [--no-children] [--dry-run] [--json]`
167
+ - `mdkg loop plan <loop> [--json]`
168
+ - `mdkg loop next <loop> [--json]`
169
+ - `mdkg loop runs <loop> [--json]`
170
+ - `loop` is one first-class node type; templates, scoped forks, and run-bearing loops are represented through metadata and links
171
+ - seed templates live under `.mdkg/templates/loops/*.loop.md`
172
+ - mdkg defines durable declarative process state and graph context; runtimes execute agents, tools, sandboxes, traces, and model routing
173
+
160
174
  Workspace registry commands:
161
175
  - `mdkg workspace ls [--json]`
162
176
  - `mdkg workspace add <alias> <path> [--mdkg-dir <dir>] [--visibility <level>] [--json]`
@@ -12,7 +12,13 @@
12
12
  "global_index_path": ".mdkg/index/global.json",
13
13
  "sqlite_path": ".mdkg/index/mdkg.sqlite",
14
14
  "sqlite_commit_warning_bytes": 52428800,
15
- "lock_timeout_ms": 10000
15
+ "lock_timeout_ms": 10000,
16
+ "limits": {
17
+ "max_files": 100000,
18
+ "max_file_bytes": 8388608,
19
+ "max_total_bytes": 536870912,
20
+ "max_depth": 64
21
+ }
16
22
  },
17
23
  "capabilities": {
18
24
  "cache_path": ".mdkg/index/capabilities.json"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "tool": "mdkg",
4
- "mdkg_version": "0.4.2",
4
+ "mdkg_version": "0.5.0",
5
5
  "files": [
6
6
  {
7
7
  "path": ".mdkg/config.json",
8
8
  "category": "config",
9
- "sha256": "715e2f55843dc31f66d4b7d333b5f89ea2aeeacc30f5dfaf7d3fd9f2c41e5fb0"
9
+ "sha256": "3d0ed0f8d65a33fe54a3855b3314a6762ad6dd1eaf6efcdce3e551a493433dc3"
10
10
  },
11
11
  {
12
12
  "path": ".mdkg/core/COLLABORATION.md",
@@ -83,6 +83,11 @@
83
83
  "category": "default_skill",
84
84
  "sha256": "0545b011232d6924d84d80ad4b33153e591249d440c55481f125b37923c298d5"
85
85
  },
86
+ {
87
+ "path": ".mdkg/skills/pursue-mdkg-loop/SKILL.md",
88
+ "category": "default_skill",
89
+ "sha256": "65dae2a6e79f2f7e4265b7057287db0308ac6ac59e6ebe9a6b6cd50b5d90176b"
90
+ },
86
91
  {
87
92
  "path": ".mdkg/skills/select-work-and-ground-context/SKILL.md",
88
93
  "category": "default_skill",
@@ -143,6 +148,11 @@
143
148
  "category": "template",
144
149
  "sha256": "8f984580aefd02b34639fa7f5d2a834662656bbf2e12f14a285f5ae31aff74ce"
145
150
  },
151
+ {
152
+ "path": ".mdkg/templates/default/loop.md",
153
+ "category": "template",
154
+ "sha256": "6e8f280c3375b9ec18e09fdb263665b2e3da45ca09b2547378fdd4c9bc015d89"
155
+ },
146
156
  {
147
157
  "path": ".mdkg/templates/default/manifest.md",
148
158
  "category": "template",
@@ -203,6 +213,41 @@
203
213
  "category": "template",
204
214
  "sha256": "1ab3c4c79ecdaff2b76007bfbb9844bf581da592f70ac2a2475594b9c279d7cd"
205
215
  },
216
+ {
217
+ "path": ".mdkg/templates/loops/backend-api-cli-bloat-audit.loop.md",
218
+ "category": "template",
219
+ "sha256": "e61083901ae2dcd315eba81ebf9aa90b9cbe7fb3ba923dd6834feb5502fd03b3"
220
+ },
221
+ {
222
+ "path": ".mdkg/templates/loops/design-frontend-ux-audit.loop.md",
223
+ "category": "template",
224
+ "sha256": "2da3ad70d71e52545006883452b5fc2673761ecd0875a00746a5468c25eee241"
225
+ },
226
+ {
227
+ "path": ".mdkg/templates/loops/duplicate-code-and-linting-audit.loop.md",
228
+ "category": "template",
229
+ "sha256": "36cbe0957883b5d75ebae6705f24eeb77f616202553c428386eeba9821aeaccc"
230
+ },
231
+ {
232
+ "path": ".mdkg/templates/loops/security-audit.loop.md",
233
+ "category": "template",
234
+ "sha256": "2c26211f628cd62474595191fdc106e051a839bf7232a2336852a1b44e1ca9ba"
235
+ },
236
+ {
237
+ "path": ".mdkg/templates/loops/tech-stack-best-practices-audit.loop.md",
238
+ "category": "template",
239
+ "sha256": "866c6aae55fa1e1eba9b77c0c92b678c779d96eb39cf293cf51c7a244ea39e54"
240
+ },
241
+ {
242
+ "path": ".mdkg/templates/loops/test-ci-skill-infrastructure-audit.loop.md",
243
+ "category": "template",
244
+ "sha256": "84d12faf4c33d808eae229f13f169f5b1aedd199e9ef5ef3f1b4b055f0fb3c39"
245
+ },
246
+ {
247
+ "path": ".mdkg/templates/loops/user-story-audit-and-recommendations.loop.md",
248
+ "category": "template",
249
+ "sha256": "2bc4b328ccdfd5c0959b003956a5b8bff1ba83032514ed23025fc8f3587c323f"
250
+ },
206
251
  {
207
252
  "path": ".mdkg/templates/skills/base.SKILL.md",
208
253
  "category": "template",
@@ -311,7 +356,7 @@
311
356
  {
312
357
  "path": "AGENT_START.md",
313
358
  "category": "startup_doc",
314
- "sha256": "c387ce4fee55eafca09bcd30f32e4d7dc8ee7f9cc9811c1adffa0756c9423a35"
359
+ "sha256": "60d5133337d383b0a6a13e108d26a6e034afe59f21a27318cf6f8f0cc3c3c5a3"
315
360
  },
316
361
  {
317
362
  "path": "AGENTS.md",
@@ -326,7 +371,7 @@
326
371
  {
327
372
  "path": "CLI_COMMAND_MATRIX.md",
328
373
  "category": "startup_doc",
329
- "sha256": "7a71fa553e38395caae5f18ecb9650911c56a019b25e793addc20701e5df5a9e"
374
+ "sha256": "b055b6e1a36f633ccb7eb9954f3ba4ec93d57b0829fcfcbdbc1713885d6bc357"
330
375
  },
331
376
  {
332
377
  "path": "llms.txt",