mdkg 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +88 -15
  2. package/CLI_COMMAND_MATRIX.md +74 -5
  3. package/README.md +30 -2
  4. package/dist/cli.js +132 -5
  5. package/dist/command-contract.json +778 -16
  6. package/dist/commands/archive.js +196 -42
  7. package/dist/commands/bundle.js +180 -44
  8. package/dist/commands/capability.js +1 -0
  9. package/dist/commands/checkpoint.js +6 -5
  10. package/dist/commands/event_support.js +16 -7
  11. package/dist/commands/fix.js +11 -8
  12. package/dist/commands/git.js +41 -10
  13. package/dist/commands/goal.js +23 -23
  14. package/dist/commands/graph.js +64 -10
  15. package/dist/commands/handoff.js +4 -2
  16. package/dist/commands/init.js +28 -23
  17. package/dist/commands/loop.js +1668 -0
  18. package/dist/commands/loop_descriptors.js +219 -0
  19. package/dist/commands/mcp.js +101 -11
  20. package/dist/commands/new.js +49 -3
  21. package/dist/commands/pack.js +14 -7
  22. package/dist/commands/search.js +10 -1
  23. package/dist/commands/skill.js +12 -9
  24. package/dist/commands/skill_mirror.js +39 -31
  25. package/dist/commands/subgraph.js +59 -26
  26. package/dist/commands/task.js +77 -4
  27. package/dist/commands/upgrade.js +13 -15
  28. package/dist/commands/validate.js +20 -7
  29. package/dist/commands/work.js +44 -26
  30. package/dist/commands/workspace.js +10 -4
  31. package/dist/core/config.js +45 -17
  32. package/dist/core/filesystem_authority.js +281 -0
  33. package/dist/core/project_db_events.js +4 -0
  34. package/dist/core/project_db_materializer.js +2 -0
  35. package/dist/core/project_db_migrations.js +238 -181
  36. package/dist/core/project_db_snapshot.js +64 -14
  37. package/dist/core/workspace_path.js +7 -0
  38. package/dist/graph/archive_integrity.js +1 -1
  39. package/dist/graph/capabilities_index_cache.js +4 -1
  40. package/dist/graph/frontmatter.js +38 -0
  41. package/dist/graph/goal_scope.js +4 -1
  42. package/dist/graph/index_cache.js +37 -7
  43. package/dist/graph/loop_bindings.js +39 -0
  44. package/dist/graph/node.js +209 -1
  45. package/dist/graph/node_body.js +52 -6
  46. package/dist/graph/skills_index_cache.js +41 -6
  47. package/dist/graph/skills_indexer.js +5 -2
  48. package/dist/graph/sqlite_index.js +118 -105
  49. package/dist/graph/subgraphs.js +142 -5
  50. package/dist/graph/validate_graph.js +109 -13
  51. package/dist/graph/workspace_files.js +39 -9
  52. package/dist/init/AGENT_START.md +16 -0
  53. package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
  54. package/dist/init/config.json +7 -1
  55. package/dist/init/init-manifest.json +49 -4
  56. package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
  57. package/dist/init/templates/default/loop.md +160 -0
  58. package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
  59. package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
  60. package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
  61. package/dist/init/templates/loops/security-audit.loop.md +140 -0
  62. package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
  63. package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
  64. package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
  65. package/dist/pack/order.js +3 -1
  66. package/dist/pack/pack.js +139 -6
  67. package/dist/templates/loader.js +9 -3
  68. package/dist/util/lock.js +10 -9
  69. package/dist/util/zip.js +163 -9
  70. package/package.json +8 -4
@@ -12,9 +12,10 @@ exports.normalizeEventRefList = normalizeEventRefList;
12
12
  exports.normalizeEventStringList = normalizeEventStringList;
13
13
  exports.appendEvent = appendEvent;
14
14
  exports.appendAutomaticEvent = appendAutomaticEvent;
15
- const fs_1 = __importDefault(require("fs"));
16
15
  const path_1 = __importDefault(require("path"));
17
16
  const config_1 = require("../core/config");
17
+ const filesystem_authority_1 = require("../core/filesystem_authority");
18
+ const workspace_path_1 = require("../core/workspace_path");
18
19
  const errors_1 = require("../util/errors");
19
20
  function normalizeWorkspaceForEvents(config, raw) {
20
21
  const normalized = (raw ?? "root").toLowerCase();
@@ -33,6 +34,13 @@ function resolveEventsPath(root, config, ws = "root") {
33
34
  }
34
35
  return path_1.default.resolve(root, entry.path, entry.mdkg_dir, "work", "events", "events.jsonl");
35
36
  }
37
+ function resolveEventsRelativePath(config, ws) {
38
+ const entry = config.workspaces[ws];
39
+ if (!entry) {
40
+ throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
41
+ }
42
+ return (0, workspace_path_1.workspaceDocumentRelativePath)(entry.path, entry.mdkg_dir, "work", "events", "events.jsonl");
43
+ }
36
44
  function formatRunIdTimestamp(now) {
37
45
  const iso = now.toISOString().replace(/[-:]/g, "").replace(/\./g, "").replace("Z", "Z");
38
46
  return iso;
@@ -43,16 +51,16 @@ function createLocalRunId(kind, now = new Date()) {
43
51
  }
44
52
  function isEventLoggingEnabled(root, config, ws) {
45
53
  const normalizedWs = normalizeWorkspaceForEvents(config, ws);
46
- return fs_1.default.existsSync(resolveEventsPath(root, config, normalizedWs));
54
+ return (0, filesystem_authority_1.containedPathExists)({ root, relativePath: resolveEventsRelativePath(config, normalizedWs) });
47
55
  }
48
56
  function ensureEventsEnabled(options) {
49
57
  const config = (0, config_1.loadConfig)(options.root);
50
58
  const ws = normalizeWorkspaceForEvents(config, options.ws);
51
59
  const eventsPath = resolveEventsPath(options.root, config, ws);
60
+ const relativePath = resolveEventsRelativePath(config, ws);
52
61
  let created = false;
53
- if (!fs_1.default.existsSync(eventsPath)) {
54
- fs_1.default.mkdirSync(path_1.default.dirname(eventsPath), { recursive: true });
55
- fs_1.default.writeFileSync(eventsPath, "", "utf8");
62
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath })) {
63
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath }, "");
56
64
  created = true;
57
65
  }
58
66
  return { ws, eventsPath, created };
@@ -102,10 +110,11 @@ function appendEvent(options) {
102
110
  throw new errors_1.UsageError("--refs requires at least one id or qid");
103
111
  }
104
112
  const eventsPath = resolveEventsPath(options.root, config, record.workspace);
105
- if (!fs_1.default.existsSync(eventsPath)) {
113
+ const relativePath = resolveEventsRelativePath(config, record.workspace);
114
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath })) {
106
115
  throw new errors_1.NotFoundError(`events.jsonl is missing for workspace ${record.workspace}; run \`mdkg event enable --ws ${record.workspace}\``);
107
116
  }
108
- fs_1.default.appendFileSync(eventsPath, `${JSON.stringify(record)}\n`, "utf8");
117
+ (0, filesystem_authority_1.appendContainedFile)({ root: options.root, relativePath }, `${JSON.stringify(record)}\n`);
109
118
  return record;
110
119
  }
111
120
  function appendAutomaticEvent(options) {
@@ -13,6 +13,7 @@ const child_process_1 = require("child_process");
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const config_1 = require("../core/config");
16
+ const filesystem_authority_1 = require("../core/filesystem_authority");
16
17
  const capabilities_indexer_1 = require("../graph/capabilities_indexer");
17
18
  const capabilities_index_cache_1 = require("../graph/capabilities_index_cache");
18
19
  const staleness_1 = require("../graph/staleness");
@@ -26,7 +27,6 @@ const template_schema_1 = require("../graph/template_schema");
26
27
  const workspace_files_1 = require("../graph/workspace_files");
27
28
  const errors_1 = require("../util/errors");
28
29
  const refs_1 = require("../util/refs");
29
- const atomic_1 = require("../util/atomic");
30
30
  const lock_1 = require("../util/lock");
31
31
  const index_1 = require("./index");
32
32
  const FAMILY_VALUES = new Set(["index", "refs", "ids", "all"]);
@@ -1211,12 +1211,12 @@ function applyDuplicateIdChange(root, change) {
1211
1211
  if (!fromId || !toId) {
1212
1212
  throw new errors_1.UsageError(`fix apply change ${change.id} is missing duplicate id rewrite details`);
1213
1213
  }
1214
- const current = fs_1.default.readFileSync(absPath, "utf8");
1214
+ const current = (0, filesystem_authority_1.readContainedFile)({ root, relativePath });
1215
1215
  const rewritten = rewriteIdInNodeContent(current, fromId, toId);
1216
1216
  if (rewritten === current) {
1217
1217
  throw new errors_1.UsageError(`fix apply change ${change.id} produced no file changes`);
1218
1218
  }
1219
- (0, atomic_1.atomicWriteFile)(absPath, rewritten);
1219
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath }, rewritten);
1220
1220
  const afterDetails = change.after;
1221
1221
  const safeReferenceRewrites = Array.isArray(afterDetails.safe_reference_rewrites)
1222
1222
  ? afterDetails.safe_reference_rewrites
@@ -1227,13 +1227,13 @@ function applyDuplicateIdChange(root, change) {
1227
1227
  continue;
1228
1228
  }
1229
1229
  const rewriteAbs = path_1.default.resolve(root, rewrite.path);
1230
- if (!isInsideRoot(root, rewriteAbs) || !fs_1.default.existsSync(rewriteAbs)) {
1230
+ if (!isInsideRoot(root, rewriteAbs) || !(0, filesystem_authority_1.containedPathExists)({ root, relativePath: rewrite.path })) {
1231
1231
  continue;
1232
1232
  }
1233
- const rewriteCurrent = fs_1.default.readFileSync(rewriteAbs, "utf8");
1233
+ const rewriteCurrent = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: rewrite.path });
1234
1234
  const rewriteNext = rewriteCurrent.split(rewrite.from).join(rewrite.to);
1235
1235
  if (rewriteNext !== rewriteCurrent) {
1236
- (0, atomic_1.atomicWriteFile)(rewriteAbs, rewriteNext);
1236
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: rewrite.path }, rewriteNext);
1237
1237
  touchedPaths.add(rewrite.path);
1238
1238
  }
1239
1239
  }
@@ -1273,8 +1273,11 @@ function applyGitStageDuplicateIdChange(root, change) {
1273
1273
  throw new errors_1.UsageError(`fix apply refused path outside repo while resolving ${conflictPath}`);
1274
1274
  }
1275
1275
  const rewrittenDuplicate = rewriteIdInNodeContent(duplicateContent, fromId, candidateId);
1276
- (0, atomic_1.atomicWriteFile)(canonicalAbs, canonicalContent);
1277
- (0, atomic_1.atomicWriteFile)(candidateAbs, rewrittenDuplicate);
1276
+ for (const relativePath of [conflictPath, candidatePath]) {
1277
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath, operation: "replace", createParents: true }, () => undefined);
1278
+ }
1279
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: conflictPath }, canonicalContent);
1280
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: candidatePath }, rewrittenDuplicate);
1278
1281
  runGitStrict(root, ["add", "--", conflictPath, candidatePath]);
1279
1282
  return {
1280
1283
  id: change.id,
@@ -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",