mdkg 0.4.1 → 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 (71) hide show
  1. package/CHANGELOG.md +103 -15
  2. package/CLI_COMMAND_MATRIX.md +109 -2
  3. package/README.md +40 -2
  4. package/dist/cli.js +262 -2
  5. package/dist/command-contract.json +1902 -524
  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 +624 -0
  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/argparse.js +5 -0
  69. package/dist/util/lock.js +10 -9
  70. package/dist/util/zip.js +163 -9
  71. package/package.json +8 -4
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOOP_COMMAND_DESCRIPTORS = void 0;
4
+ exports.loopDescriptorForSubcommand = loopDescriptorForSubcommand;
5
+ const READ_ONLY_LOOP_SAFETY = {
6
+ json_schema_ref: "mdkg.command_output.v1",
7
+ side_effects: ["none"],
8
+ read_paths: [".mdkg/**"],
9
+ write_paths: [],
10
+ dry_run: { supported: false },
11
+ lock_policy: "none-read-only",
12
+ atomic_write_policy: "none-read-only",
13
+ receipts: ["loop-read-receipt"],
14
+ danger_level: "read-only",
15
+ };
16
+ const LOOP_GRAPH_WRITE_SAFETY = {
17
+ json_schema_ref: "mdkg.command_output.v1",
18
+ side_effects: [
19
+ "create-scoped-loop-and-optional-child-nodes",
20
+ "reserve-sqlite-node-ids-when-configured",
21
+ "rebuild-derived-indexes-when-auto-reindex-is-enabled",
22
+ "append-loop-fork-event-when-event-logging-is-enabled",
23
+ ],
24
+ read_paths: [".mdkg/**"],
25
+ write_paths: [".mdkg/**/*.md", ".mdkg/index/**", ".mdkg/events/*.jsonl"],
26
+ dry_run: {
27
+ supported: true,
28
+ flag: "--dry-run",
29
+ side_effects: ["none"],
30
+ write_paths: [],
31
+ reserves_ids: false,
32
+ },
33
+ lock_policy: "mutation-lock-required",
34
+ atomic_write_policy: "exclusive-create-and-atomic-file-writes",
35
+ receipts: ["loop-fork-receipt"],
36
+ danger_level: "moderate",
37
+ };
38
+ const JSON_FLAG = {
39
+ name: "--json",
40
+ value: null,
41
+ required: false,
42
+ description: "Emit deterministic JSON instead of text.",
43
+ };
44
+ const WS_FLAG = {
45
+ name: "--ws",
46
+ value: "<alias>",
47
+ required: false,
48
+ description: "Resolve the command against one workspace alias.",
49
+ };
50
+ const ROOT_FLAG = {
51
+ name: "--root",
52
+ value: "<path>",
53
+ required: false,
54
+ description: "Run against a specific repository root; -r is the short alias.",
55
+ };
56
+ const NO_CACHE_FLAG = {
57
+ name: "--no-cache",
58
+ value: null,
59
+ required: false,
60
+ description: "Build a non-persisting in-memory index projection instead of reading the cache.",
61
+ };
62
+ const NO_REINDEX_FLAG = {
63
+ name: "--no-reindex",
64
+ value: null,
65
+ required: false,
66
+ description: "Do not rebuild a stale or missing index projection.",
67
+ };
68
+ const RUN_ID_FLAG = {
69
+ name: "--run-id",
70
+ value: "<id>",
71
+ required: false,
72
+ description: "Attach an optional run id to the fork event when event logging is enabled.",
73
+ };
74
+ const READ_FLAGS = [ROOT_FLAG, WS_FLAG, JSON_FLAG, NO_CACHE_FLAG, NO_REINDEX_FLAG];
75
+ exports.LOOP_COMMAND_DESCRIPTORS = [
76
+ {
77
+ key: "loop",
78
+ path: ["loop"],
79
+ summary: "mdkg loop command",
80
+ usage: [
81
+ "mdkg loop list [--ws <alias>] [--json]",
82
+ "mdkg loop show <loop-or-template> [--meta] [--ws <alias>] [--json]",
83
+ "mdkg loop fork <template> --scope <scope> [--title <title>] [--materialization default_children|planning_only|manual] [--planning-only] [--no-children] [--dry-run] [--run-id <id>] [--ws <alias>] [--json]",
84
+ "mdkg loop plan <loop> [--ws <alias>] [--json]",
85
+ "mdkg loop next <loop> [--ws <alias>] [--json]",
86
+ "mdkg loop runs <loop> [--ws <alias>] [--json]",
87
+ ],
88
+ args: [],
89
+ flags: [ROOT_FLAG, WS_FLAG, JSON_FLAG, NO_CACHE_FLAG, NO_REINDEX_FLAG, RUN_ID_FLAG],
90
+ output_formats: ["text", "json"],
91
+ notes: [
92
+ "loop is one first-class node type. Templates, scoped forks, and run-bearing loops are represented through metadata and links.",
93
+ "mdkg defines reusable process state and graph context; runtimes execute agents, tools, sandboxes, traces, and model routing.",
94
+ ],
95
+ safety: {
96
+ ...READ_ONLY_LOOP_SAFETY,
97
+ side_effects: ["read-or-write-loop-graph-state"],
98
+ write_paths: [".mdkg/**/*.md", ".mdkg/index/**", ".mdkg/events/*.jsonl"],
99
+ dry_run: { supported: true, commands: ["fork"] },
100
+ lock_policy: "mutation-lock-required-for-fork",
101
+ atomic_write_policy: "exclusive-create-and-atomic-file-writes",
102
+ receipts: ["loop-receipt"],
103
+ danger_level: "mixed",
104
+ },
105
+ handler: "runLoopSubcommand",
106
+ },
107
+ {
108
+ key: "loop list",
109
+ path: ["loop", "list"],
110
+ summary: "mdkg loop list command",
111
+ usage: ["mdkg loop list [--ws <alias>] [--no-cache] [--no-reindex] [--json]"],
112
+ args: [],
113
+ flags: READ_FLAGS,
114
+ output_formats: ["text", "json"],
115
+ notes: ["Lists indexed loop nodes and reusable seed loop templates."],
116
+ safety: { ...READ_ONLY_LOOP_SAFETY, receipts: ["loop-list-receipt"] },
117
+ handler: "runLoopListCommand",
118
+ },
119
+ {
120
+ key: "loop show",
121
+ path: ["loop", "show"],
122
+ summary: "mdkg loop show command",
123
+ usage: ["mdkg loop show <loop-or-template> [--meta] [--ws <alias>] [--no-cache] [--no-reindex] [--json]"],
124
+ args: [
125
+ {
126
+ name: "loop-or-template",
127
+ required: true,
128
+ source: "<loop-or-template>",
129
+ description: "Loop node id/qid or seed template name/ref.",
130
+ },
131
+ ],
132
+ flags: [
133
+ { name: "--meta", value: null, required: false, description: "Show metadata without the full body." },
134
+ ...READ_FLAGS,
135
+ ],
136
+ output_formats: ["text", "json"],
137
+ notes: ["Shows an indexed loop node or a seed loop template."],
138
+ safety: { ...READ_ONLY_LOOP_SAFETY, receipts: ["loop-show-receipt"] },
139
+ handler: "runLoopShowCommand",
140
+ },
141
+ {
142
+ key: "loop fork",
143
+ path: ["loop", "fork"],
144
+ summary: "mdkg loop fork command",
145
+ usage: [
146
+ "mdkg loop fork <template> --scope <scope> [--title <title>] [--materialization <mode>] [--planning-only] [--no-children] [--dry-run] [--run-id <id>] [--ws <alias>] [--no-cache] [--no-reindex] [--json]",
147
+ ],
148
+ args: [
149
+ { name: "template", required: true, source: "<template>", description: "Loop template node id/qid or seed template name/ref." },
150
+ ],
151
+ flags: [
152
+ { name: "--scope", value: "<scope>", required: true, description: "Scope ref, qid, URI, path, or description for the scoped loop." },
153
+ { name: "--title", value: "<title>", required: false, description: "Override the generated scoped loop title." },
154
+ { name: "--materialization", value: "<mode>", required: false, description: "Child materialization mode: default_children, planning_only, or manual." },
155
+ { name: "--planning-only", value: null, required: false, description: "Create only the scoped loop shell." },
156
+ { name: "--no-children", value: null, required: false, description: "Alias for planning-only materialization." },
157
+ { name: "--dry-run", value: null, required: false, description: "Plan the fork without writing loop or child nodes." },
158
+ RUN_ID_FLAG,
159
+ ROOT_FLAG,
160
+ WS_FLAG,
161
+ JSON_FLAG,
162
+ NO_CACHE_FLAG,
163
+ NO_REINDEX_FLAG,
164
+ ],
165
+ output_formats: ["text", "json"],
166
+ notes: [
167
+ "Seed templates resolve from .mdkg/templates/loops/<name>.loop.md.",
168
+ "Default forks create a scoped loop plus linked spike/task/test child nodes.",
169
+ ],
170
+ safety: LOOP_GRAPH_WRITE_SAFETY,
171
+ handler: "runLoopForkCommand",
172
+ },
173
+ {
174
+ key: "loop plan",
175
+ path: ["loop", "plan"],
176
+ summary: "mdkg loop plan command",
177
+ usage: ["mdkg loop plan <loop> [--ws <alias>] [--no-cache] [--no-reindex] [--json]"],
178
+ args: [
179
+ { name: "loop", required: true, source: "<loop>", description: "Loop node id or qid." },
180
+ ],
181
+ flags: READ_FLAGS,
182
+ output_formats: ["text", "json"],
183
+ notes: ["Read-only readiness cockpit for loop execution planning."],
184
+ safety: { ...READ_ONLY_LOOP_SAFETY, receipts: ["loop-plan-receipt"] },
185
+ handler: "runLoopPlanCommand",
186
+ },
187
+ {
188
+ key: "loop next",
189
+ path: ["loop", "next"],
190
+ summary: "mdkg loop next command",
191
+ usage: ["mdkg loop next <loop> [--ws <alias>] [--no-cache] [--no-reindex] [--json]"],
192
+ args: [
193
+ { name: "loop", required: true, source: "<loop>", description: "Loop node id or qid." },
194
+ ],
195
+ flags: READ_FLAGS,
196
+ output_formats: ["text", "json"],
197
+ notes: ["Read-only routing for the next actionable child, readiness lane, or blocker recovery step."],
198
+ safety: { ...READ_ONLY_LOOP_SAFETY, receipts: ["loop-next-receipt"] },
199
+ handler: "runLoopNextCommand",
200
+ },
201
+ {
202
+ key: "loop runs",
203
+ path: ["loop", "runs"],
204
+ summary: "mdkg loop runs command",
205
+ usage: ["mdkg loop runs <loop> [--ws <alias>] [--no-cache] [--no-reindex] [--json]"],
206
+ args: [
207
+ { name: "loop", required: true, source: "<loop>", description: "Loop node id or qid." },
208
+ ],
209
+ flags: READ_FLAGS,
210
+ output_formats: ["text", "json"],
211
+ notes: ["Lists run refs and evidence refs without executing runtime jobs."],
212
+ safety: { ...READ_ONLY_LOOP_SAFETY, receipts: ["loop-runs-receipt"] },
213
+ handler: "runLoopRunsCommand",
214
+ },
215
+ ];
216
+ function loopDescriptorForSubcommand(subcommand) {
217
+ const key = subcommand ? `loop ${subcommand.toLowerCase()}` : "loop";
218
+ return exports.LOOP_COMMAND_DESCRIPTORS.find((descriptor) => descriptor.key === key) ?? exports.LOOP_COMMAND_DESCRIPTORS[0];
219
+ }
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.handleMcpRequest = handleMcpRequest;
7
7
  exports.handleMcpMessage = handleMcpMessage;
8
8
  exports.runMcpServeCommand = runMcpServeCommand;
9
- const readline_1 = __importDefault(require("readline"));
10
9
  const fs_1 = __importDefault(require("fs"));
11
10
  const path_1 = __importDefault(require("path"));
11
+ const string_decoder_1 = require("string_decoder");
12
12
  const status_1 = require("./status");
13
13
  const validate_1 = require("./validate");
14
14
  const config_1 = require("../core/config");
@@ -33,6 +33,12 @@ const DEFAULT_SEARCH_LIMIT = 20;
33
33
  const MAX_PACK_NODES = 100;
34
34
  const DEFAULT_PACK_NODES = 20;
35
35
  const DEFAULT_PACK_MAX_CHARS = 120000;
36
+ const MAX_MCP_LINE_BYTES = 1024 * 1024;
37
+ const MAX_MCP_JSON_DEPTH = 64;
38
+ const MAX_MCP_BATCH_ITEMS = 50;
39
+ const MAX_MCP_SHOW_BODY_BYTES = 256 * 1024;
40
+ const MAX_MCP_TOOL_PAYLOAD_BYTES = 512 * 1024;
41
+ const MAX_MCP_RESPONSE_BYTES = 1024 * 1024;
36
42
  function objectSchema(properties, required = []) {
37
43
  return {
38
44
  type: "object",
@@ -312,6 +318,10 @@ function isConcreteGoalCandidate(node, statusRanks) {
312
318
  return node.status !== "done" && node.status !== "archived";
313
319
  }
314
320
  function toolResult(payload, isError = false) {
321
+ const compact = JSON.stringify(payload);
322
+ if (Buffer.byteLength(compact, "utf8") > MAX_MCP_TOOL_PAYLOAD_BYTES) {
323
+ throw new errors_1.UsageError(`MCP tool payload exceeds byte limit: ${MAX_MCP_TOOL_PAYLOAD_BYTES}`);
324
+ }
315
325
  return {
316
326
  content: [
317
327
  {
@@ -388,7 +398,7 @@ function showTool(root, args) {
388
398
  }
389
399
  return {
390
400
  command: "mcp.show",
391
- item: (0, query_output_1.toNodeDetailJson)(node, metaOnly ? undefined : (0, node_body_1.readNodeBody)(root, node)),
401
+ item: (0, query_output_1.toNodeDetailJson)(node, metaOnly ? undefined : (0, node_body_1.readNodeBody)(root, node, MAX_MCP_SHOW_BODY_BYTES)),
392
402
  };
393
403
  }
394
404
  function packTool(root, args) {
@@ -414,6 +424,8 @@ function packTool(root, args) {
414
424
  verboseCoreListPath: path_1.default.resolve(root, config.pack.verbose_core_list_path),
415
425
  wsHint: ws,
416
426
  includeLatestCheckpoint: true,
427
+ maxTraversalNodes: MAX_PACK_NODES * 10,
428
+ maxBodyBytes: maxChars,
417
429
  });
418
430
  const headingMap = resolvedProfile.bodyMode === "summary"
419
431
  ? (0, headings_1.loadTemplateHeadingMap)(root, config, Array.from(node_1.ALLOWED_TYPES))
@@ -617,6 +629,9 @@ function handleMcpRequest(context, raw) {
617
629
  }
618
630
  function handleMcpMessage(context, message) {
619
631
  if (Array.isArray(message)) {
632
+ if (message.length === 0 || message.length > MAX_MCP_BATCH_ITEMS) {
633
+ return [jsonRpcError(null, -32600, `batch item count must be between 1 and ${MAX_MCP_BATCH_ITEMS}`)];
634
+ }
620
635
  return message
621
636
  .map((item) => handleMcpRequest(context, item))
622
637
  .filter((item) => Boolean(item));
@@ -624,33 +639,108 @@ function handleMcpMessage(context, message) {
624
639
  const response = handleMcpRequest(context, message);
625
640
  return response ? [response] : [];
626
641
  }
642
+ function jsonNestingWithinLimit(input) {
643
+ let depth = 0;
644
+ let inString = false;
645
+ let escaped = false;
646
+ for (const character of input) {
647
+ if (inString) {
648
+ if (escaped) {
649
+ escaped = false;
650
+ }
651
+ else if (character === "\\") {
652
+ escaped = true;
653
+ }
654
+ else if (character === '"') {
655
+ inString = false;
656
+ }
657
+ continue;
658
+ }
659
+ if (character === '"') {
660
+ inString = true;
661
+ }
662
+ else if (character === "{" || character === "[") {
663
+ depth += 1;
664
+ if (depth > MAX_MCP_JSON_DEPTH) {
665
+ return false;
666
+ }
667
+ }
668
+ else if (character === "}" || character === "]") {
669
+ depth -= 1;
670
+ }
671
+ }
672
+ return true;
673
+ }
674
+ function writeBoundedMcpResponse(output, response) {
675
+ let serialized = JSON.stringify(response);
676
+ if (Buffer.byteLength(serialized, "utf8") > MAX_MCP_RESPONSE_BYTES) {
677
+ serialized = JSON.stringify(jsonRpcError(response.id, -32000, `MCP response exceeds byte limit: ${MAX_MCP_RESPONSE_BYTES}`));
678
+ }
679
+ output.write(`${serialized}\n`);
680
+ }
627
681
  async function runMcpServeCommand(options) {
628
682
  if (!options.stdio) {
629
683
  throw new errors_1.UsageError("mdkg mcp serve requires --stdio");
630
684
  }
631
685
  const input = options.input ?? process.stdin;
632
686
  const output = options.output ?? process.stdout;
633
- const lines = readline_1.default.createInterface({
634
- input,
635
- crlfDelay: Infinity,
636
- });
637
687
  const context = { root: options.root };
638
- for await (const line of lines) {
688
+ const decoder = new string_decoder_1.StringDecoder("utf8");
689
+ let pending = "";
690
+ let discardingOversizedLine = false;
691
+ const processLine = (line) => {
639
692
  const trimmed = line.trim();
640
693
  if (!trimmed) {
641
- continue;
694
+ return;
695
+ }
696
+ if (!jsonNestingWithinLimit(trimmed)) {
697
+ writeBoundedMcpResponse(output, jsonRpcError(null, -32600, `JSON nesting exceeds limit: ${MAX_MCP_JSON_DEPTH}`));
698
+ return;
642
699
  }
643
700
  let parsed;
644
701
  try {
645
702
  parsed = JSON.parse(trimmed);
646
703
  }
647
704
  catch {
648
- output.write(`${JSON.stringify(jsonRpcError(null, -32700, "Parse error"))}\n`);
649
- continue;
705
+ writeBoundedMcpResponse(output, jsonRpcError(null, -32700, "Parse error"));
706
+ return;
650
707
  }
651
708
  for (const response of handleMcpMessage(context, parsed)) {
652
- output.write(`${JSON.stringify(response)}\n`);
709
+ writeBoundedMcpResponse(output, response);
710
+ }
711
+ };
712
+ for await (const chunk of input) {
713
+ const text = decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
714
+ let start = 0;
715
+ for (let index = 0; index < text.length; index += 1) {
716
+ if (text[index] !== "\n") {
717
+ continue;
718
+ }
719
+ const segment = text.slice(start, index).replace(/\r$/, "");
720
+ if (discardingOversizedLine) {
721
+ discardingOversizedLine = false;
722
+ }
723
+ else if (Buffer.byteLength(pending, "utf8") + Buffer.byteLength(segment, "utf8") > MAX_MCP_LINE_BYTES) {
724
+ writeBoundedMcpResponse(output, jsonRpcError(null, -32600, `request line exceeds byte limit: ${MAX_MCP_LINE_BYTES}`));
725
+ }
726
+ else {
727
+ processLine(pending + segment);
728
+ }
729
+ pending = "";
730
+ start = index + 1;
653
731
  }
732
+ if (start < text.length && !discardingOversizedLine) {
733
+ pending += text.slice(start);
734
+ if (Buffer.byteLength(pending, "utf8") > MAX_MCP_LINE_BYTES) {
735
+ pending = "";
736
+ discardingOversizedLine = true;
737
+ writeBoundedMcpResponse(output, jsonRpcError(null, -32600, `request line exceeds byte limit: ${MAX_MCP_LINE_BYTES}`));
738
+ }
739
+ }
740
+ }
741
+ pending += decoder.end();
742
+ if (!discardingOversizedLine && pending.trim()) {
743
+ processLine(pending);
654
744
  }
655
745
  return 0;
656
746
  }
@@ -18,7 +18,8 @@ const errors_1 = require("../util/errors");
18
18
  const qid_1 = require("../util/qid");
19
19
  const id_1 = require("../util/id");
20
20
  const refs_1 = require("../util/refs");
21
- const atomic_1 = require("../util/atomic");
21
+ const filesystem_authority_1 = require("../core/filesystem_authority");
22
+ const workspace_path_1 = require("../core/workspace_path");
22
23
  const lock_1 = require("../util/lock");
23
24
  const sqlite_index_1 = require("../graph/sqlite_index");
24
25
  const reindex_1 = require("../graph/reindex");
@@ -30,6 +31,38 @@ const CONTRACT_METADATA_RE = /^[a-z][a-z0-9_]*(?:-[a-z0-9_]+)*$/;
30
31
  const CORE_TYPES = new Set(["rule"]);
31
32
  const DESIGN_TYPES = new Set(["prd", "edd", "dec", "prop"]);
32
33
  const LEGACY_NEW_SPEC_WARNING = "warning: mdkg new spec is legacy; use mdkg new manifest. This alias creates MANIFEST.md with type: manifest during the compatibility release.";
34
+ const NEW_LOOP_NEXT_ACTIONS = [
35
+ "Review reusable loop templates with mdkg loop list",
36
+ "Fork a reusable template with mdkg loop fork <template> --scope <scope>",
37
+ "Inspect raw loop readiness with mdkg loop plan <loop-id>",
38
+ ];
39
+ function toPosix(value) {
40
+ return value.split(path_1.default.sep).join("/");
41
+ }
42
+ function loopTemplateSuggestions(root, config) {
43
+ const dir = path_1.default.resolve(root, config.templates.root_path, "loops");
44
+ if (!fs_1.default.existsSync(dir)) {
45
+ return [];
46
+ }
47
+ return fs_1.default
48
+ .readdirSync(dir)
49
+ .filter((entry) => entry.endsWith(".loop.md"))
50
+ .sort()
51
+ .map((entry) => {
52
+ const filePath = path_1.default.join(dir, entry);
53
+ const content = fs_1.default.readFileSync(filePath, "utf8");
54
+ const { frontmatter } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
55
+ const slug = entry.replace(/\.loop\.md$/, "");
56
+ const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0
57
+ ? frontmatter.title
58
+ : slug;
59
+ return {
60
+ ref: `template://loops/${slug}`,
61
+ title,
62
+ path: toPosix(path_1.default.relative(root, filePath)),
63
+ };
64
+ });
65
+ }
33
66
  function parseCsvList(raw) {
34
67
  if (!raw) {
35
68
  return [];
@@ -249,9 +282,10 @@ function runNewCommandLocked(options) {
249
282
  const fileName = fileNameForType(type, id, slug);
250
283
  const wsEntry = config.workspaces[ws];
251
284
  const folder = folderForType(type);
285
+ const relativeFilePath = (0, workspace_path_1.workspaceDocumentRelativePath)(wsEntry.path, wsEntry.mdkg_dir, folder, fileName);
252
286
  const targetDir = path_1.default.resolve(options.root, wsEntry.path, wsEntry.mdkg_dir, folder);
253
287
  const filePath = path_1.default.join(targetDir, fileName);
254
- if (fs_1.default.existsSync(filePath)) {
288
+ if ((0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: relativeFilePath })) {
255
289
  throw new errors_1.UsageError(`node already exists: ${path_1.default.relative(options.root, filePath)}`);
256
290
  }
257
291
  const statusInput = options.status?.toLowerCase();
@@ -410,7 +444,7 @@ function runNewCommandLocked(options) {
410
444
  redaction_class: redactionClass,
411
445
  });
412
446
  try {
413
- (0, atomic_1.writeFileExclusive)(filePath, content);
447
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: relativeFilePath }, content);
414
448
  }
415
449
  catch (err) {
416
450
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
@@ -447,14 +481,26 @@ function runNewCommandLocked(options) {
447
481
  if (legacySpecAlias) {
448
482
  console.error(LEGACY_NEW_SPEC_WARNING);
449
483
  }
484
+ const loopGuidance = type === "loop"
485
+ ? {
486
+ next_actions: NEW_LOOP_NEXT_ACTIONS,
487
+ suggested_templates: loopTemplateSuggestions(options.root, config),
488
+ }
489
+ : undefined;
450
490
  if (options.json) {
451
491
  console.log(JSON.stringify({
452
492
  action: "created",
453
493
  node: receipt,
494
+ ...(loopGuidance ? loopGuidance : {}),
454
495
  }, null, 2));
455
496
  return;
456
497
  }
457
498
  console.log(`node created: ${receipt.qid} (${receipt.path})`);
499
+ if (loopGuidance) {
500
+ console.log("next: review reusable loop templates with `mdkg loop list`");
501
+ console.log("next: fork a reusable template with `mdkg loop fork <template> --scope <scope>`");
502
+ console.log(`next: inspect raw loop readiness with \`mdkg loop plan ${receipt.id}\``);
503
+ }
458
504
  }
459
505
  function runNewCommand(options) {
460
506
  const config = (0, config_1.loadConfig)(options.root);
@@ -7,6 +7,7 @@ exports.runPackCommand = runPackCommand;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const config_1 = require("../core/config");
10
+ const filesystem_authority_1 = require("../core/filesystem_authority");
10
11
  const index_cache_1 = require("../graph/index_cache");
11
12
  const frontmatter_1 = require("../graph/frontmatter");
12
13
  const skills_index_cache_1 = require("../graph/skills_index_cache");
@@ -138,8 +139,12 @@ function loadSkillFullBody(root, skill) {
138
139
  if (!fs_1.default.existsSync(skillPath)) {
139
140
  return renderSkillMetaBody(skill);
140
141
  }
141
- const content = fs_1.default.readFileSync(skillPath, "utf8");
142
- return (0, frontmatter_1.parseFrontmatter)(content, skillPath).body.trimEnd();
142
+ const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: skill.path }, "utf8");
143
+ const parsed = (0, frontmatter_1.parseFrontmatter)(content, skillPath);
144
+ if (parsed.frontmatter.name !== skill.name) {
145
+ throw new Error(`cached skill identity mismatch for ${skill.qid}`);
146
+ }
147
+ return parsed.body.trimEnd();
143
148
  }
144
149
  function uniqueSkillOrder(slugs) {
145
150
  const seen = new Set();
@@ -323,10 +328,13 @@ function runPackCommand(options) {
323
328
  if (ws && !config.workspaces[ws] && !config.subgraphs[ws]) {
324
329
  throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
325
330
  }
331
+ const visibility = options.visibility
332
+ ? (0, visibility_1.normalizeVisibility)(options.visibility)
333
+ : undefined;
326
334
  const { index, rebuilt, stale, warnings } = (0, index_cache_1.loadIndex)({
327
335
  root: options.root,
328
336
  config,
329
- useCache: !options.noCache,
337
+ useCache: !options.noCache && !visibility,
330
338
  allowReindex: !options.noReindex,
331
339
  });
332
340
  if (stale && !rebuilt && !options.noCache) {
@@ -347,9 +355,6 @@ function runPackCommand(options) {
347
355
  const edges = normalizeEdges([...config.pack.default_edges, ...extraEdges]);
348
356
  const skillsPolicy = resolveSkillsPolicy(options.skills);
349
357
  const skillsDepth = resolveSkillsDepth(options.skillsDepth);
350
- const visibility = options.visibility
351
- ? (0, visibility_1.normalizeVisibility)(options.visibility)
352
- : undefined;
353
358
  let resolvedProfile;
354
359
  try {
355
360
  resolvedProfile = (0, profile_1.resolvePackProfile)({
@@ -374,6 +379,8 @@ function runPackCommand(options) {
374
379
  edges,
375
380
  verbose: Boolean(options.verbose),
376
381
  maxNodes: config.pack.limits.max_nodes,
382
+ maxTraversalNodes: Math.min(config.index.limits.max_files, Math.max(1000, config.pack.limits.max_nodes * 10)),
383
+ maxBodyBytes: config.pack.limits.max_bytes,
377
384
  verboseCoreListPath: path_1.default.resolve(options.root, config.pack.verbose_core_list_path),
378
385
  wsHint: ws,
379
386
  includeLatestCheckpoint: true,
@@ -386,7 +393,7 @@ function runPackCommand(options) {
386
393
  const skillsLoad = (0, skills_index_cache_1.loadSkillsIndex)({
387
394
  root: options.root,
388
395
  config,
389
- useCache: !options.noCache,
396
+ useCache: !options.noCache && !visibility,
390
397
  allowReindex: !options.noReindex,
391
398
  });
392
399
  if (skillsLoad.stale && !skillsLoad.rebuilt && !options.noCache) {
@@ -8,6 +8,7 @@ const errors_1 = require("../util/errors");
8
8
  const sort_1 = require("../util/sort");
9
9
  const node_card_1 = require("./node_card");
10
10
  const query_output_1 = require("./query_output");
11
+ const DEFAULT_STRUCTURED_SEARCH_LIMIT = 50;
11
12
  function normalizeWorkspace(value) {
12
13
  if (!value || value === "all") {
13
14
  return undefined;
@@ -84,11 +85,19 @@ function runSearchCommand(options) {
84
85
  const sorted = (0, sort_1.sortNodesByQid)(nodeResults);
85
86
  const format = options.format ?? (options.json ? "json" : undefined);
86
87
  if (format) {
88
+ const limit = options.limit ?? DEFAULT_STRUCTURED_SEARCH_LIMIT;
89
+ if (!Number.isInteger(limit) || limit < 1) {
90
+ throw new errors_1.UsageError("--limit must be a positive integer");
91
+ }
92
+ const items = sorted.slice(0, limit);
87
93
  (0, query_output_1.writeStructuredOutput)({
88
94
  command: "search",
89
95
  kind: "node",
90
96
  count: sorted.length,
91
- items: sorted.map(query_output_1.toNodeSummaryJson),
97
+ returned_count: items.length,
98
+ limit,
99
+ truncated: items.length < sorted.length,
100
+ items: items.map(query_output_1.toNodeSummaryJson),
92
101
  }, format);
93
102
  return;
94
103
  }
@@ -12,6 +12,7 @@ exports.runSkillSyncCommand = runSkillSyncCommand;
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const config_1 = require("../core/config");
15
+ const filesystem_authority_1 = require("../core/filesystem_authority");
15
16
  const skills_indexer_1 = require("../graph/skills_indexer");
16
17
  const skills_index_cache_1 = require("../graph/skills_index_cache");
17
18
  const skills_index_cache_2 = require("../graph/skills_index_cache");
@@ -20,7 +21,6 @@ const skill_support_1 = require("./skill_support");
20
21
  const query_output_1 = require("./query_output");
21
22
  const event_support_1 = require("./event_support");
22
23
  const skill_mirror_1 = require("./skill_mirror");
23
- const atomic_1 = require("../util/atomic");
24
24
  const lock_1 = require("../util/lock");
25
25
  function parseCsvList(raw) {
26
26
  if (!raw) {
@@ -170,11 +170,12 @@ function runSkillNewCommandLocked(options) {
170
170
  if (fs_1.default.existsSync(canonicalPath) && !force) {
171
171
  throw new errors_1.UsageError(`skill already exists: ${path_1.default.relative(root, canonicalPath)} (use --force to overwrite)`);
172
172
  }
173
- fs_1.default.mkdirSync(skillDir, { recursive: true });
174
- fs_1.default.mkdirSync(path_1.default.join(skillDir, "references"), { recursive: true });
175
- fs_1.default.mkdirSync(path_1.default.join(skillDir, "assets"), { recursive: true });
173
+ const relativeSkillDir = path_1.default.relative(root, skillDir).split(path_1.default.sep).join("/");
174
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: relativeSkillDir });
175
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: `${relativeSkillDir}/references` });
176
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: `${relativeSkillDir}/assets` });
176
177
  if (options.withScripts) {
177
- fs_1.default.mkdirSync(path_1.default.join(skillDir, "scripts"), { recursive: true });
178
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: `${relativeSkillDir}/scripts` });
178
179
  }
179
180
  const content = (0, skill_support_1.renderSkillTemplate)({
180
181
  name,
@@ -183,7 +184,7 @@ function runSkillNewCommandLocked(options) {
183
184
  authors,
184
185
  links,
185
186
  });
186
- (0, atomic_1.atomicWriteFile)(canonicalPath, content);
187
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: `${relativeSkillDir}/SKILL.md` }, content);
187
188
  (0, skill_support_1.ensureSkillsRegistry)(root, config);
188
189
  (0, skill_support_1.refreshSkillsRegistry)(root, config);
189
190
  if ((0, skill_mirror_1.shouldMaintainSkillMirrors)(root, config)) {
@@ -233,6 +234,7 @@ function runSkillListCommand(options) {
233
234
  config,
234
235
  useCache: !options.noCache,
235
236
  allowReindex: !options.noReindex,
237
+ persistReindex: false,
236
238
  });
237
239
  if (stale && !rebuilt && !options.noCache) {
238
240
  console.error("warning: skills index is stale; run mdkg index to refresh");
@@ -261,6 +263,7 @@ function runSkillShowCommand(options) {
261
263
  config,
262
264
  useCache: !options.noCache,
263
265
  allowReindex: !options.noReindex,
266
+ persistReindex: false,
264
267
  });
265
268
  if (stale && !rebuilt && !options.noCache) {
266
269
  console.error("warning: skills index is stale; run mdkg index to refresh");
@@ -269,13 +272,12 @@ function runSkillShowCommand(options) {
269
272
  if (!skill) {
270
273
  throw new errors_1.NotFoundError(`skill not found: ${slug}`);
271
274
  }
272
- const skillPath = path_1.default.resolve(options.root, skill.path);
273
275
  let body = "";
274
276
  if (!options.metaOnly) {
275
- if (!fs_1.default.existsSync(skillPath)) {
277
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: skill.path })) {
276
278
  throw new errors_1.NotFoundError(`file not found for ${skill.id}: ${skill.path}`);
277
279
  }
278
- body = fs_1.default.readFileSync(skillPath, "utf8").trimEnd();
280
+ body = (0, filesystem_authority_1.readContainedFile)({ root: options.root, relativePath: skill.path }).trimEnd();
279
281
  }
280
282
  const format = options.format ?? (options.json ? "json" : undefined);
281
283
  if (format) {
@@ -347,6 +349,7 @@ function runSkillSearchCommand(options) {
347
349
  config,
348
350
  useCache: !options.noCache,
349
351
  allowReindex: !options.noReindex,
352
+ persistReindex: false,
350
353
  });
351
354
  if (stale && !rebuilt && !options.noCache) {
352
355
  console.error("warning: skills index is stale; run mdkg index to refresh");