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
@@ -14,10 +14,10 @@ const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const child_process_1 = require("child_process");
16
16
  const config_1 = require("../core/config");
17
+ const filesystem_authority_1 = require("../core/filesystem_authority");
17
18
  const project_db_1 = require("../core/project_db");
18
19
  const project_db_snapshot_1 = require("../core/project_db_snapshot");
19
20
  const validate_1 = require("./validate");
20
- const atomic_1 = require("../util/atomic");
21
21
  const lock_1 = require("../util/lock");
22
22
  const errors_1 = require("../util/errors");
23
23
  function toPosix(value) {
@@ -75,6 +75,22 @@ function assertNoEmbeddedCredentials(label, value) {
75
75
  throw new errors_1.UsageError(`${label} must not contain embedded credentials; use SSH, credential helpers, gh auth, CI env, or shell-managed Git auth`);
76
76
  }
77
77
  }
78
+ function assertGitOperand(label, value) {
79
+ if (value.length === 0 || value !== value.trim() || value.startsWith("-") || /[\0\r\n\t ]/.test(value)) {
80
+ throw new errors_1.UsageError(`${label} must be a non-option Git operand without whitespace or control characters`);
81
+ }
82
+ }
83
+ function assertGitBranch(root, branch) {
84
+ assertGitOperand("--branch", branch);
85
+ const result = (0, child_process_1.spawnSync)("git", ["check-ref-format", "--branch", branch], {
86
+ cwd: root,
87
+ encoding: "utf8",
88
+ stdio: "pipe",
89
+ });
90
+ if (result.status !== 0) {
91
+ throw new errors_1.UsageError(`--branch is not a valid Git branch name: ${branch}`);
92
+ }
93
+ }
78
94
  function sanitizeOutput(value) {
79
95
  return value
80
96
  .split(/\r?\n/)
@@ -234,15 +250,22 @@ function runGitInspectCommand(options) {
234
250
  }
235
251
  function runGitCloneCommand(options) {
236
252
  assertNoEmbeddedCredentials("repository", options.repository);
237
- const target = containedPath(options.root, options.target, "--target");
238
- if (fs_1.default.existsSync(target) && fs_1.default.readdirSync(target).length > 0) {
239
- throw new errors_1.UsageError("--target must be empty or absent");
253
+ assertGitOperand("repository", options.repository);
254
+ if (options.branch) {
255
+ assertGitBranch(options.root, options.branch);
240
256
  }
257
+ const target = containedPath(options.root, options.target, "--target");
258
+ const targetRelative = rel(options.root, target);
259
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: targetRelative, operation: "create" }, ({ absolutePath }) => {
260
+ if (fs_1.default.existsSync(absolutePath) && fs_1.default.readdirSync(absolutePath).length > 0) {
261
+ throw new errors_1.UsageError("--target must be empty or absent");
262
+ }
263
+ });
241
264
  const args = ["clone"];
242
265
  if (options.branch) {
243
266
  args.push("--branch", options.branch);
244
267
  }
245
- args.push(options.repository, target);
268
+ args.push("--", options.repository, target);
246
269
  git(options.root, args);
247
270
  const inspect = collectInspectReceipt(target);
248
271
  const receipt = {
@@ -273,12 +296,16 @@ function runGitCloneCommand(options) {
273
296
  function runGitFetchCommand(options) {
274
297
  requireGitWorkTree(options.root);
275
298
  const remote = options.remote ?? "origin";
299
+ assertGitOperand("--remote", remote);
300
+ if (options.branch) {
301
+ assertGitBranch(options.root, options.branch);
302
+ }
276
303
  assertNoEmbeddedCredentials("--remote", remote);
277
304
  const configuredRemoteUrl = rawRemoteUrl(options.root, remote);
278
305
  if (configuredRemoteUrl && hasEmbeddedUrlCredentials(configuredRemoteUrl)) {
279
306
  throw new errors_1.UsageError(`remote ${remote} must not contain embedded credentials; use external Git auth`);
280
307
  }
281
- const args = ["fetch", remote];
308
+ const args = ["fetch", "--", remote];
282
309
  if (options.branch) {
283
310
  args.push(options.branch);
284
311
  }
@@ -382,7 +409,7 @@ function collectCloseoutReceipt(options) {
382
409
  else {
383
410
  warnings.push("project DB did not participate in this closeout; no DB snapshot was sealed");
384
411
  }
385
- fs_1.default.mkdirSync(outputDir, { recursive: true });
412
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root: options.root, relativePath: outputRel });
386
413
  const baseReceipt = {
387
414
  action: "git.closeout",
388
415
  ok: true,
@@ -406,8 +433,8 @@ function collectCloseoutReceipt(options) {
406
433
  markdown: rel(options.root, markdownPath),
407
434
  },
408
435
  };
409
- (0, atomic_1.atomicWriteFile)(jsonPath, stableJson(receipt));
410
- (0, atomic_1.atomicWriteFile)(markdownPath, closeoutMarkdown(baseReceipt));
436
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: rel(options.root, jsonPath) }, stableJson(receipt));
437
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: rel(options.root, markdownPath) }, closeoutMarkdown(baseReceipt));
411
438
  return receipt;
412
439
  }
413
440
  function runGitCloseoutCommand(options) {
@@ -443,6 +470,8 @@ function collectPushReadyReceipt(options) {
443
470
  if (!branch) {
444
471
  throw new errors_1.UsageError("mdkg git push-ready requires --branch <name>");
445
472
  }
473
+ assertGitOperand("--remote", remote);
474
+ assertGitBranch(options.root, branch);
446
475
  assertNoEmbeddedCredentials("--remote", remote);
447
476
  requireGitWorkTree(options.root);
448
477
  const config = (0, config_1.loadConfig)(options.root);
@@ -529,6 +558,8 @@ function collectPushReceipt(options) {
529
558
  if (!branch) {
530
559
  throw new errors_1.UsageError("mdkg git push requires --branch <name>");
531
560
  }
561
+ assertGitOperand("--remote", remote);
562
+ assertGitBranch(options.root, branch);
532
563
  assertNoEmbeddedCredentials("--remote", remote);
533
564
  requireGitWorkTree(options.root);
534
565
  let closeout = null;
@@ -556,7 +587,7 @@ function collectPushReceipt(options) {
556
587
  if (!pushReady.ok) {
557
588
  throw new errors_1.ValidationError(`git push-ready failed with ${pushReady.failure_count} issue(s)`);
558
589
  }
559
- const push = git(options.root, ["push", remote, `HEAD:refs/heads/${branch}`]);
590
+ const push = git(options.root, ["push", "--", remote, `HEAD:refs/heads/${branch}`]);
560
591
  const inspect = collectInspectReceipt(options.root);
561
592
  return {
562
593
  action: "git.push",
@@ -15,15 +15,14 @@ exports.runGoalPauseCommand = runGoalPauseCommand;
15
15
  exports.runGoalResumeCommand = runGoalResumeCommand;
16
16
  exports.runGoalDoneCommand = runGoalDoneCommand;
17
17
  exports.runGoalArchiveCommand = runGoalArchiveCommand;
18
- const fs_1 = __importDefault(require("fs"));
19
18
  const path_1 = __importDefault(require("path"));
20
19
  const config_1 = require("../core/config");
20
+ const filesystem_authority_1 = require("../core/filesystem_authority");
21
21
  const goal_scope_1 = require("../graph/goal_scope");
22
22
  const index_cache_1 = require("../graph/index_cache");
23
23
  const frontmatter_1 = require("../graph/frontmatter");
24
24
  const indexer_1 = require("../graph/indexer");
25
25
  const reindex_1 = require("../graph/reindex");
26
- const atomic_1 = require("../util/atomic");
27
26
  const date_1 = require("../util/date");
28
27
  const errors_1 = require("../util/errors");
29
28
  const lock_1 = require("../util/lock");
@@ -66,11 +65,11 @@ function selectedGoalPath(root) {
66
65
  }
67
66
  function readSelectedGoalState(root, warnings) {
68
67
  const filePath = selectedGoalPath(root);
69
- if (!fs_1.default.existsSync(filePath)) {
68
+ if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: ".mdkg/state/selected-goal.json" })) {
70
69
  return undefined;
71
70
  }
72
71
  try {
73
- const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
72
+ const parsed = JSON.parse((0, filesystem_authority_1.readContainedFile)({ root, relativePath: ".mdkg/state/selected-goal.json" }));
74
73
  if (typeof parsed.qid === "string" &&
75
74
  typeof parsed.id === "string" &&
76
75
  typeof parsed.ws === "string" &&
@@ -97,30 +96,31 @@ function writeSelectedGoalState(root, node, now) {
97
96
  ws: node.ws,
98
97
  selected_at: now.toISOString(),
99
98
  };
100
- (0, atomic_1.atomicWriteFile)(selectedGoalPath(root), `${JSON.stringify(state, null, 2)}\n`);
99
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: ".mdkg/state/selected-goal.json" }, `${JSON.stringify(state, null, 2)}\n`);
101
100
  }
102
101
  function readNodeFrontmatter(root, node) {
103
102
  const filePath = path_1.default.resolve(root, node.path);
104
- const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(filePath, "utf8"), filePath);
103
+ const relativePath = path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
104
+ const parsed = (0, frontmatter_1.parseFrontmatter)((0, filesystem_authority_1.readContainedFile)({ root, relativePath }), filePath);
105
105
  return {
106
106
  filePath,
107
107
  frontmatter: { ...parsed.frontmatter },
108
108
  body: parsed.body,
109
109
  };
110
110
  }
111
- function writeNodeFrontmatterFile(filePath, frontmatter, body, now) {
111
+ function writeNodeFrontmatterFile(root, filePath, frontmatter, body, now) {
112
112
  frontmatter.updated = (0, date_1.formatDate)(now);
113
113
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
114
114
  const frontmatterBlock = ["---", ...lines, "---"].join("\n");
115
115
  const content = body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
116
- (0, atomic_1.atomicWriteFile)(filePath, content);
116
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: path_1.default.relative(root, filePath).split(path_1.default.sep).join("/") }, content);
117
117
  }
118
118
  function removeSelectedGoalState(root) {
119
119
  const filePath = selectedGoalPath(root);
120
- if (!fs_1.default.existsSync(filePath)) {
120
+ if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: ".mdkg/state/selected-goal.json" })) {
121
121
  return false;
122
122
  }
123
- fs_1.default.rmSync(filePath, { force: true });
123
+ (0, filesystem_authority_1.removeContainedPath)({ root, relativePath: ".mdkg/state/selected-goal.json", force: true });
124
124
  return true;
125
125
  }
126
126
  function optionalString(value) {
@@ -184,9 +184,9 @@ function resolveGoalSelection(root, index, idOrQid, wsHint) {
184
184
  }
185
185
  throw new errors_1.NotFoundError("no selected goal or unique active goal found; run `mdkg goal activate <goal-id>`");
186
186
  }
187
- function loadGoal(root, idOrQid, wsHint) {
187
+ function loadGoal(root, idOrQid, wsHint, persistReindex = true) {
188
188
  const config = (0, config_1.loadConfig)(root);
189
- const { index } = (0, index_cache_1.loadIndex)({ root, config });
189
+ const { index } = (0, index_cache_1.loadIndex)({ root, config, persistReindex });
190
190
  const ws = normalizeWorkspace(wsHint);
191
191
  const selection = resolveGoalSelection(root, index, idOrQid, ws);
192
192
  const node = selection.node;
@@ -197,7 +197,7 @@ function loadGoal(root, idOrQid, wsHint) {
197
197
  throw new errors_1.UsageError(`mdkg goal requires a goal node; got ${node.type}:${node.id}`);
198
198
  }
199
199
  const filePath = path_1.default.resolve(root, node.path);
200
- const content = fs_1.default.readFileSync(filePath, "utf8");
200
+ const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: node.path });
201
201
  const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
202
202
  return {
203
203
  config,
@@ -234,8 +234,8 @@ function goalReceipt(root, loaded) {
234
234
  blocked_after_attempts: optionalString(fm.blocked_after_attempts),
235
235
  };
236
236
  }
237
- function writeGoalFile(loaded, now) {
238
- writeNodeFrontmatterFile(loaded.filePath, loaded.frontmatter, loaded.body, now);
237
+ function writeGoalFile(root, loaded, now) {
238
+ writeNodeFrontmatterFile(root, loaded.filePath, loaded.frontmatter, loaded.body, now);
239
239
  }
240
240
  function maybeReindex(root, config) {
241
241
  if (!config.index.auto_reindex) {
@@ -313,7 +313,7 @@ function readOnlySubgraphBlockerWarnings(index, qids) {
313
313
  return warnings;
314
314
  }
315
315
  function runGoalShowCommand(options) {
316
- const loaded = loadGoal(options.root, options.id, options.ws);
316
+ const loaded = loadGoal(options.root, options.id, options.ws, false);
317
317
  const receipt = { action: "showed", goal: goalReceipt(options.root, loaded) };
318
318
  if (options.json) {
319
319
  console.log(JSON.stringify(receipt, null, 2));
@@ -332,7 +332,7 @@ function runGoalShowCommand(options) {
332
332
  console.log(`required_checks: ${checks.length === 0 ? "none" : checks.join(", ")}`);
333
333
  }
334
334
  function runGoalEvaluateCommand(options) {
335
- const loaded = loadGoal(options.root, options.id, options.ws);
335
+ const loaded = loadGoal(options.root, options.id, options.ws, false);
336
336
  const checks = toStringList(loaded.frontmatter.required_checks).map((command) => ({
337
337
  command,
338
338
  status: "report_only",
@@ -364,7 +364,7 @@ function runGoalEvaluateCommand(options) {
364
364
  }
365
365
  }
366
366
  function runGoalNextCommand(options) {
367
- const loaded = loadGoal(options.root, options.id, options.ws);
367
+ const loaded = loadGoal(options.root, options.id, options.ws, false);
368
368
  if (isArchivedGoal(loaded.node)) {
369
369
  const warnings = [...loaded.warnings, `${loaded.node.qid} is archived and has no actionable next work`];
370
370
  if (options.json) {
@@ -501,7 +501,7 @@ function runGoalActivateCommand(options) {
501
501
  const conflictFile = readNodeFrontmatter(options.root, conflict);
502
502
  conflictFile.frontmatter.goal_state = "paused";
503
503
  conflictFile.frontmatter.status = ensureStatusAllowed(config, "blocked");
504
- writeNodeFrontmatterFile(conflictFile.filePath, conflictFile.frontmatter, conflictFile.body, now);
504
+ writeNodeFrontmatterFile(options.root, conflictFile.filePath, conflictFile.frontmatter, conflictFile.body, now);
505
505
  pausedGoals.push({
506
506
  workspace: conflict.ws,
507
507
  id: conflict.id,
@@ -515,7 +515,7 @@ function runGoalActivateCommand(options) {
515
515
  }
516
516
  loaded.frontmatter.goal_state = "active";
517
517
  loaded.frontmatter.status = ensureStatusAllowed(config, "progress");
518
- writeGoalFile(loaded, now);
518
+ writeGoalFile(options.root, loaded, now);
519
519
  writeSelectedGoalState(options.root, loaded.node, now);
520
520
  maybeReindex(options.root, loaded.config);
521
521
  (0, event_support_1.appendAutomaticEvent)({
@@ -552,7 +552,7 @@ function runGoalActivateCommand(options) {
552
552
  }
553
553
  function runGoalCurrentCommand(options) {
554
554
  const config = (0, config_1.loadConfig)(options.root);
555
- const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
555
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, persistReindex: false });
556
556
  const ws = normalizeWorkspace(options.ws);
557
557
  const warnings = [];
558
558
  let source = "none";
@@ -665,7 +665,7 @@ function runGoalClaimCommand(options) {
665
665
  }
666
666
  const now = options.now ?? new Date();
667
667
  loaded.frontmatter.active_node = node.ws === loaded.node.ws ? node.id : node.qid;
668
- writeGoalFile(loaded, now);
668
+ writeGoalFile(options.root, loaded, now);
669
669
  maybeReindex(options.root, loaded.config);
670
670
  (0, event_support_1.appendAutomaticEvent)({
671
671
  root: options.root,
@@ -705,7 +705,7 @@ function runGoalStateMutationLocked(action, options) {
705
705
  }
706
706
  loaded.frontmatter.goal_state = GOAL_STATE_BY_ACTION[action];
707
707
  loaded.frontmatter.status = ensureStatusAllowed(loaded.config, STATUS_BY_ACTION[action]);
708
- writeGoalFile(loaded, now);
708
+ writeGoalFile(options.root, loaded, now);
709
709
  maybeReindex(options.root, loaded.config);
710
710
  (0, event_support_1.appendAutomaticEvent)({
711
711
  root: options.root,
@@ -13,6 +13,7 @@ const bundle_1 = require("./bundle");
13
13
  const index_1 = require("./index");
14
14
  const validate_1 = require("./validate");
15
15
  const config_1 = require("../core/config");
16
+ const filesystem_authority_1 = require("../core/filesystem_authority");
16
17
  const workspace_path_1 = require("../core/workspace_path");
17
18
  const indexer_1 = require("../graph/indexer");
18
19
  const index_cache_1 = require("../graph/index_cache");
@@ -24,6 +25,7 @@ const zip_1 = require("../util/zip");
24
25
  const lock_1 = require("../util/lock");
25
26
  const date_1 = require("../util/date");
26
27
  const refs_1 = require("../util/refs");
28
+ const id_1 = require("../util/id");
27
29
  function writeJson(value) {
28
30
  console.log(JSON.stringify(value, null, 2));
29
31
  }
@@ -146,6 +148,45 @@ function isInside(parent, child) {
146
148
  const relative = path_1.default.relative(parent, child);
147
149
  return relative === "" || (!relative.startsWith("..") && !path_1.default.isAbsolute(relative));
148
150
  }
151
+ function lstatIfPresent(target) {
152
+ try {
153
+ return fs_1.default.lstatSync(target);
154
+ }
155
+ catch (err) {
156
+ if (err.code === "ENOENT") {
157
+ return undefined;
158
+ }
159
+ throw err;
160
+ }
161
+ }
162
+ function assertTargetPathContainment(root, targetRoot) {
163
+ // Preflight existing components before writes. Node does not expose an
164
+ // openat-style atomic directory walk, so concurrent path replacement remains
165
+ // an operating-system race rather than a guarantee this check can eliminate.
166
+ const absoluteRoot = path_1.default.resolve(root);
167
+ const relativeTarget = path_1.default.relative(absoluteRoot, targetRoot);
168
+ const canonicalRoot = fs_1.default.realpathSync(absoluteRoot);
169
+ let nearestExisting = absoluteRoot;
170
+ let current = absoluteRoot;
171
+ for (const segment of relativeTarget.split(path_1.default.sep).filter(Boolean)) {
172
+ current = path_1.default.join(current, segment);
173
+ const stat = lstatIfPresent(current);
174
+ if (!stat) {
175
+ break;
176
+ }
177
+ if (stat.isSymbolicLink()) {
178
+ throw new errors_1.UsageError(`--target must not contain symbolic links: ${rel(absoluteRoot, current)}`);
179
+ }
180
+ if (current !== targetRoot && !stat.isDirectory()) {
181
+ throw new errors_1.UsageError(`--target ancestor must be a directory: ${rel(absoluteRoot, current)}`);
182
+ }
183
+ nearestExisting = current;
184
+ }
185
+ const canonicalExisting = fs_1.default.realpathSync(nearestExisting);
186
+ if (!isInside(canonicalRoot, canonicalExisting)) {
187
+ throw new errors_1.UsageError("--target must resolve inside the current mdkg root");
188
+ }
189
+ }
149
190
  function resolveSourcePath(root, source) {
150
191
  if (source.includes("\0")) {
151
192
  throw new errors_1.UsageError("source cannot contain NUL bytes");
@@ -216,7 +257,12 @@ function resolveTargetRoot(root, target) {
216
257
  if (!isInside(root, targetRoot)) {
217
258
  throw new errors_1.UsageError("--target must stay inside the current mdkg root");
218
259
  }
219
- if (fs_1.default.existsSync(targetRoot)) {
260
+ assertTargetPathContainment(root, targetRoot);
261
+ const targetStat = lstatIfPresent(targetRoot);
262
+ if (targetStat) {
263
+ if (!targetStat.isDirectory()) {
264
+ throw new errors_1.UsageError(`target must be an empty directory or absent: ${rel(root, targetRoot)}`);
265
+ }
220
266
  const entries = fs_1.default.readdirSync(targetRoot);
221
267
  if (entries.length > 0) {
222
268
  throw new errors_1.UsageError(`target must be empty or absent: ${rel(root, targetRoot)}`);
@@ -454,6 +500,17 @@ function targetPathForImport(sourcePath, fromId, toId, usedPaths) {
454
500
  function renderNode(frontmatter, body) {
455
501
  return ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---", body].join("\n");
456
502
  }
503
+ function requireImportedIdentity(sourcePath, frontmatter) {
504
+ const id = typeof frontmatter.id === "string" ? frontmatter.id : undefined;
505
+ const type = typeof frontmatter.type === "string" ? frontmatter.type : undefined;
506
+ if (!id || !type) {
507
+ throw new errors_1.ValidationError(`${sourcePath}: imported node requires string id and type`);
508
+ }
509
+ if (!(0, id_1.isCanonicalId)(id) && !(0, id_1.isPortableId)(id)) {
510
+ throw new errors_1.ValidationError(`${sourcePath}: imported node id is invalid: ${id}`);
511
+ }
512
+ return { id, type };
513
+ }
457
514
  function localIdsAndPaths(root) {
458
515
  const config = (0, config_1.loadConfig)(root);
459
516
  const index = (0, indexer_1.buildIndex)(root, config);
@@ -486,11 +543,7 @@ function planImportTemplate(options) {
486
543
  throw new errors_1.ValidationError(`graph source missing bundled file: ${sourcePath}`);
487
544
  }
488
545
  const parsed = (0, frontmatter_1.parseFrontmatter)(data.toString("utf8"), sourcePath);
489
- const id = typeof parsed.frontmatter.id === "string" ? parsed.frontmatter.id : undefined;
490
- const type = typeof parsed.frontmatter.type === "string" ? parsed.frontmatter.type : undefined;
491
- if (!id || !type) {
492
- throw new errors_1.ValidationError(`${sourcePath}: imported node requires string id and type`);
493
- }
546
+ const { id, type } = requireImportedIdentity(sourcePath, parsed.frontmatter);
494
547
  return { sourcePath, parsed, id, type };
495
548
  });
496
549
  const idMap = new Map();
@@ -629,7 +682,7 @@ function applyImportTemplate(options, receipt) {
629
682
  throw new errors_1.ValidationError(`graph source missing bundled file: ${sourcePath}`);
630
683
  }
631
684
  const parsed = (0, frontmatter_1.parseFrontmatter)(data.toString("utf8"), sourcePath);
632
- const id = String(parsed.frontmatter.id);
685
+ const { id } = requireImportedIdentity(sourcePath, parsed.frontmatter);
633
686
  return { sourcePath, parsed, id };
634
687
  });
635
688
  const idMap = new Map();
@@ -671,14 +724,15 @@ function applyImportTemplate(options, receipt) {
671
724
  const targetPath = targetPathForImport(node.sourcePath, node.id, toId, usedPaths);
672
725
  contentByTarget.set(targetPath, renderNode(frontmatter, body));
673
726
  }
727
+ for (const targetPath of files) {
728
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: targetPath, operation: "create", createParents: true }, () => undefined);
729
+ }
674
730
  for (const targetPath of files) {
675
731
  const content = contentByTarget.get(targetPath);
676
732
  if (!content) {
677
733
  throw new errors_1.UsageError(`import plan content missing for ${targetPath}`);
678
734
  }
679
- const targetAbs = path_1.default.resolve(options.root, targetPath);
680
- fs_1.default.mkdirSync(path_1.default.dirname(targetAbs), { recursive: true });
681
- (0, atomic_1.atomicWriteFile)(targetAbs, content);
735
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: targetPath }, content);
682
736
  }
683
737
  pauseLocalGoals(options.root, applyPlan.paused_goals, config);
684
738
  const indexReceipt = (0, index_1.rebuildDerivedIndexCaches)({ root: options.root });
@@ -7,10 +7,10 @@ exports.runHandoffCreateCommand = runHandoffCreateCommand;
7
7
  const crypto_1 = __importDefault(require("crypto"));
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 pack_1 = require("../pack/pack");
12
13
  const metrics_1 = require("../pack/metrics");
13
- const atomic_1 = require("../util/atomic");
14
14
  const errors_1 = require("../util/errors");
15
15
  const qid_1 = require("../util/qid");
16
16
  const version_1 = require("../core/version");
@@ -231,6 +231,8 @@ function runHandoffCreateCommand(options) {
231
231
  edges: ["parent", "epic", "relates", "blocked_by", "blocks", "context_refs", "evidence_refs"],
232
232
  verbose: false,
233
233
  maxNodes: config.pack.limits.max_nodes,
234
+ maxTraversalNodes: Math.min(config.index.limits.max_files, Math.max(1000, config.pack.limits.max_nodes * 10)),
235
+ maxBodyBytes: config.pack.limits.max_bytes,
234
236
  verboseCoreListPath: path_1.default.resolve(options.root, config.pack.verbose_core_list_path),
235
237
  wsHint: ws,
236
238
  includeLatestCheckpoint: true,
@@ -252,7 +254,7 @@ function runHandoffCreateCommand(options) {
252
254
  const contentHash = sha256(content);
253
255
  const outPath = outputPath(options.root, options.out);
254
256
  if (outPath) {
255
- (0, atomic_1.atomicWriteFile)(outPath, content);
257
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: path_1.default.relative(options.root, outPath).split(path_1.default.sep).join("/") }, content);
256
258
  }
257
259
  const receipt = {
258
260
  action: "handoff-created",
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runInitCommand = runInitCommand;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const filesystem_authority_1 = require("../core/filesystem_authority");
9
10
  const config_1 = require("../core/config");
10
11
  const migrate_1 = require("../core/migrate");
11
12
  const errors_1 = require("../util/errors");
@@ -55,12 +56,12 @@ function recordSkipped(root, dest, stats) {
55
56
  stats.skippedPaths.push(displayPath(root, dest));
56
57
  }
57
58
  function copySeedFile(root, src, dest, force, stats) {
58
- if (fs_1.default.existsSync(dest) && !force) {
59
+ const relativePath = displayPath(root, dest);
60
+ if ((0, filesystem_authority_1.containedPathExists)({ root, relativePath }) && !force) {
59
61
  recordSkipped(root, dest, stats);
60
62
  return;
61
63
  }
62
- fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
63
- fs_1.default.copyFileSync(src, dest);
64
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath }, fs_1.default.readFileSync(src));
64
65
  recordCreated(root, dest, stats);
65
66
  }
66
67
  function copySeedDir(root, srcDir, destDir, force, stats) {
@@ -74,8 +75,11 @@ function copySeedDir(root, srcDir, destDir, force, stats) {
74
75
  copySeedFile(root, filePath, destPath, force, stats);
75
76
  }
76
77
  }
77
- function appendIgnoreEntries(filePath, entries) {
78
- const raw = fs_1.default.existsSync(filePath) ? fs_1.default.readFileSync(filePath, "utf8") : "";
78
+ function appendIgnoreEntries(root, filePath, entries) {
79
+ const relativePath = displayPath(root, filePath);
80
+ const raw = (0, filesystem_authority_1.containedPathExists)({ root, relativePath })
81
+ ? (0, filesystem_authority_1.readContainedFile)({ root, relativePath })
82
+ : "";
79
83
  const lines = raw.split(/\r?\n/);
80
84
  const existing = new Set(lines.map((line) => line.trim()).filter(Boolean));
81
85
  const additions = entries.filter((entry) => !existing.has(entry));
@@ -84,17 +88,16 @@ function appendIgnoreEntries(filePath, entries) {
84
88
  }
85
89
  const suffix = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
86
90
  const updated = `${raw}${suffix}${additions.join("\n")}\n`;
87
- fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
88
- fs_1.default.writeFileSync(filePath, updated, "utf8");
91
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath }, updated);
89
92
  return true;
90
93
  }
91
94
  function writeFileIfMissing(root, filePath, content, force, stats) {
92
- if (fs_1.default.existsSync(filePath) && !force) {
95
+ const relativePath = displayPath(root, filePath);
96
+ if ((0, filesystem_authority_1.containedPathExists)({ root, relativePath }) && !force) {
93
97
  recordSkipped(root, filePath, stats);
94
98
  return;
95
99
  }
96
- fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
97
- fs_1.default.writeFileSync(filePath, content, "utf8");
100
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath }, content);
98
101
  recordCreated(root, filePath, stats);
99
102
  }
100
103
  function soulTemplate(created) {
@@ -300,8 +303,11 @@ function parseCoreList(raw) {
300
303
  }
301
304
  return { header, ids };
302
305
  }
303
- function ensureCorePins(coreListPath, requiredPins) {
304
- const raw = fs_1.default.existsSync(coreListPath) ? fs_1.default.readFileSync(coreListPath, "utf8") : "";
306
+ function ensureCorePins(root, coreListPath, requiredPins) {
307
+ const relativePath = displayPath(root, coreListPath);
308
+ const raw = (0, filesystem_authority_1.containedPathExists)({ root, relativePath })
309
+ ? (0, filesystem_authority_1.readContainedFile)({ root, relativePath })
310
+ : "";
305
311
  const { header, ids } = parseCoreList(raw);
306
312
  const seen = new Set();
307
313
  const dedupedExisting = [];
@@ -321,8 +327,7 @@ function ensureCorePins(coreListPath, requiredPins) {
321
327
  normalizedHeader.pop();
322
328
  }
323
329
  const output = [...normalizedHeader, "", ...finalIds, ""].join("\n");
324
- fs_1.default.mkdirSync(path_1.default.dirname(coreListPath), { recursive: true });
325
- fs_1.default.writeFileSync(coreListPath, output, "utf8");
330
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath }, output);
326
331
  }
327
332
  function refreshManifestHash(root, manifest, relPath) {
328
333
  const entry = manifest.files.find((file) => file.path === relPath);
@@ -419,9 +424,9 @@ function runInitCommand(options) {
419
424
  };
420
425
  const mdkgDir = path_1.default.join(root, ".mdkg");
421
426
  try {
422
- fs_1.default.mkdirSync(mdkgDir, { recursive: true });
423
- fs_1.default.mkdirSync(path_1.default.join(mdkgDir, "work"), { recursive: true });
424
- fs_1.default.mkdirSync(path_1.default.join(mdkgDir, "design"), { recursive: true });
427
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: ".mdkg" });
428
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: ".mdkg/work" });
429
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: ".mdkg/design" });
425
430
  copySeedFile(root, seedConfig, path_1.default.join(mdkgDir, "config.json"), force, stats);
426
431
  copySeedFile(root, seedReadme, path_1.default.join(mdkgDir, "README.md"), force, stats);
427
432
  copySeedDir(root, seedCore, path_1.default.join(mdkgDir, "core"), force, stats);
@@ -446,8 +451,8 @@ function runInitCommand(options) {
446
451
  const registryPath = path_1.default.join(skillsDir, "registry.md");
447
452
  const eventsDir = path_1.default.join(mdkgDir, "work", "events");
448
453
  const eventsPath = path_1.default.join(eventsDir, "events.jsonl");
449
- fs_1.default.mkdirSync(skillsDir, { recursive: true });
450
- fs_1.default.mkdirSync(eventsDir, { recursive: true });
454
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: ".mdkg/skills" });
455
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: ".mdkg/work/events" });
451
456
  copySeedDir(root, seedDefaultSkills, skillsDir, force, stats);
452
457
  if (!fs_1.default.existsSync(seedSoul)) {
453
458
  writeFileIfMissing(root, soulPath, soulTemplate(today), force, stats);
@@ -463,7 +468,7 @@ function runInitCommand(options) {
463
468
  writeFileIfMissing(root, eventsPath, seededInitEvent(new Date().toISOString()), force, stats);
464
469
  }
465
470
  const coreListPath = path_1.default.join(mdkgDir, "core", "core.md");
466
- ensureCorePins(coreListPath, [SOUL_PIN_ID, COLLABORATION_PIN_ID, HUMAN_PIN_ID]);
471
+ ensureCorePins(root, coreListPath, [SOUL_PIN_ID, COLLABORATION_PIN_ID, HUMAN_PIN_ID]);
467
472
  refreshManifestHash(root, seedManifest, ".mdkg/core/core.md");
468
473
  ensureCreatedCoreManifestEntry(root, seedManifest, ".mdkg/core/SOUL.md", stats);
469
474
  ensureCreatedCoreManifestEntry(root, seedManifest, ".mdkg/core/COLLABORATION.md", stats);
@@ -490,7 +495,7 @@ function runInitCommand(options) {
490
495
  const shouldUpdateNpmignore = Boolean(options.updateNpmignore || !noUpdateIgnores);
491
496
  try {
492
497
  if (shouldUpdateGitignore) {
493
- if (appendIgnoreEntries(path_1.default.join(root, ".gitignore"), [
498
+ if (appendIgnoreEntries(root, path_1.default.join(root, ".gitignore"), [
494
499
  ".mdkg/index/*.json",
495
500
  ".mdkg/index/*.tmp",
496
501
  ".mdkg/index/*.lock",
@@ -508,12 +513,12 @@ function runInitCommand(options) {
508
513
  }
509
514
  }
510
515
  if (shouldUpdateNpmignore) {
511
- if (appendIgnoreEntries(path_1.default.join(root, ".npmignore"), [".mdkg/", ".mdkg/index/", ".mdkg/pack/"])) {
516
+ if (appendIgnoreEntries(root, path_1.default.join(root, ".npmignore"), [".mdkg/", ".mdkg/index/", ".mdkg/pack/"])) {
512
517
  stats.ignoreFilesUpdated.push(".npmignore");
513
518
  }
514
519
  }
515
520
  if (options.updateDockerignore) {
516
- if (appendIgnoreEntries(path_1.default.join(root, ".dockerignore"), [".mdkg/"])) {
521
+ if (appendIgnoreEntries(root, path_1.default.join(root, ".dockerignore"), [".mdkg/"])) {
517
522
  stats.ignoreFilesUpdated.push(".dockerignore");
518
523
  }
519
524
  }