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
@@ -12,6 +12,7 @@ exports.scaffoldMirrorRoots = scaffoldMirrorRoots;
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 errors_1 = require("../util/errors");
17
18
  const MANIFEST_FILE = ".mdkg-managed.json";
@@ -29,7 +30,9 @@ function resolveMirrorTargets(root, config) {
29
30
  return configuredSkillMirrorTargets(config).map((configuredPath) => {
30
31
  const skillsRoot = path_1.default.join(root, configuredPath);
31
32
  return {
33
+ boundaryRoot: root,
32
34
  configuredPath,
35
+ relativeSkillsRoot: configuredPath.split(/[\\/]/).join("/"),
33
36
  rootDir: path_1.default.dirname(skillsRoot),
34
37
  skillsRoot,
35
38
  manifestPath: path_1.default.join(skillsRoot, MANIFEST_FILE),
@@ -37,17 +40,20 @@ function resolveMirrorTargets(root, config) {
37
40
  });
38
41
  }
39
42
  function readManifest(target) {
40
- if (!fs_1.default.existsSync(target.manifestPath)) {
43
+ const relativeManifest = `${target.relativeSkillsRoot}/${MANIFEST_FILE}`;
44
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: target.boundaryRoot, relativePath: relativeManifest })) {
41
45
  return new Set();
42
46
  }
43
47
  try {
44
- const parsed = JSON.parse(fs_1.default.readFileSync(target.manifestPath, "utf8"));
48
+ const parsed = JSON.parse((0, filesystem_authority_1.readContainedFile)({ root: target.boundaryRoot, relativePath: relativeManifest }));
45
49
  if (!Array.isArray(parsed.managed_slugs)) {
46
50
  return new Set();
47
51
  }
48
- return new Set(parsed.managed_slugs
49
- .map((value) => String(value).trim().toLowerCase())
50
- .filter(Boolean));
52
+ const slugs = parsed.managed_slugs.map((value) => String(value).trim().toLowerCase());
53
+ if (slugs.some((slug) => !skills_indexer_1.SKILL_SLUG_RE.test(slug))) {
54
+ return new Set();
55
+ }
56
+ return new Set(slugs);
51
57
  }
52
58
  catch {
53
59
  return new Set();
@@ -57,8 +63,8 @@ function writeManifest(target, managed) {
57
63
  const payload = {
58
64
  managed_slugs: Array.from(new Set(Array.from(managed).map((value) => value.toLowerCase()))).sort(),
59
65
  };
60
- fs_1.default.mkdirSync(target.skillsRoot, { recursive: true });
61
- fs_1.default.writeFileSync(target.manifestPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
66
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root: target.boundaryRoot, relativePath: target.relativeSkillsRoot });
67
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: target.boundaryRoot, relativePath: `${target.relativeSkillsRoot}/${MANIFEST_FILE}` }, `${JSON.stringify(payload, null, 2)}\n`);
62
68
  }
63
69
  function shouldCreateMirrorRoots(root, config) {
64
70
  if (MANAGED_ROOT_MARKERS.some((relPath) => fs_1.default.existsSync(path_1.default.join(root, relPath)))) {
@@ -250,30 +256,32 @@ function syncSkillMirrors(options) {
250
256
  continue;
251
257
  }
252
258
  touchedTargets += 1;
253
- fs_1.default.mkdirSync(target.skillsRoot, { recursive: true });
254
- const managed = readManifest(target);
255
- for (const source of sources) {
256
- const destDir = path_1.default.join(target.skillsRoot, source.slug);
257
- const exists = fs_1.default.existsSync(destDir);
258
- if (exists && !managed.has(source.slug) && !force) {
259
- throw new errors_1.UsageError(`${path_1.default.relative(options.root, destDir)} already exists and is not mdkg-managed; rerun \`mdkg skill sync --force\` to replace it`);
259
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root: options.root, relativePath: target.relativeSkillsRoot });
260
+ (0, filesystem_authority_1.withContainedTreeSink)({ root: options.root, relativePath: target.relativeSkillsRoot, operation: "replace" }, () => {
261
+ const managed = readManifest(target);
262
+ for (const source of sources) {
263
+ const destDir = path_1.default.join(target.skillsRoot, source.slug);
264
+ const exists = fs_1.default.existsSync(destDir);
265
+ if (exists && !managed.has(source.slug) && !force) {
266
+ throw new errors_1.UsageError(`${path_1.default.relative(options.root, destDir)} already exists and is not mdkg-managed; rerun \`mdkg skill sync --force\` to replace it`);
267
+ }
268
+ materializeSkillMirror(source, destDir);
269
+ managed.add(source.slug);
270
+ synced += 1;
260
271
  }
261
- materializeSkillMirror(source, destDir);
262
- managed.add(source.slug);
263
- synced += 1;
264
- }
265
- for (const slug of Array.from(managed)) {
266
- if (sources.some((source) => source.slug === slug)) {
267
- continue;
268
- }
269
- const destDir = path_1.default.join(target.skillsRoot, slug);
270
- if (fs_1.default.existsSync(destDir)) {
271
- fs_1.default.rmSync(destDir, { recursive: true, force: true });
272
- pruned += 1;
272
+ for (const slug of Array.from(managed)) {
273
+ if (sources.some((source) => source.slug === slug)) {
274
+ continue;
275
+ }
276
+ const destDir = path_1.default.join(target.skillsRoot, slug);
277
+ if (fs_1.default.existsSync(destDir)) {
278
+ fs_1.default.rmSync(destDir, { recursive: true, force: true });
279
+ pruned += 1;
280
+ }
281
+ managed.delete(slug);
273
282
  }
274
- managed.delete(slug);
275
- }
276
- writeManifest(target, managed);
283
+ writeManifest(target, managed);
284
+ });
277
285
  }
278
286
  return { synced, pruned, targets: touchedTargets };
279
287
  }
@@ -342,8 +350,8 @@ function auditSkillMirrors(root, config) {
342
350
  }
343
351
  function scaffoldMirrorRoots(root, config) {
344
352
  for (const target of resolveMirrorTargets(root, config)) {
345
- fs_1.default.mkdirSync(target.skillsRoot, { recursive: true });
346
- if (!fs_1.default.existsSync(target.manifestPath)) {
353
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: target.relativeSkillsRoot });
354
+ if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: `${target.relativeSkillsRoot}/${MANIFEST_FILE}` })) {
347
355
  writeManifest(target, []);
348
356
  }
349
357
  }
@@ -19,6 +19,7 @@ const fs_1 = __importDefault(require("fs"));
19
19
  const path_1 = __importDefault(require("path"));
20
20
  const child_process_1 = require("child_process");
21
21
  const config_1 = require("../core/config");
22
+ const filesystem_authority_1 = require("../core/filesystem_authority");
22
23
  const migrate_1 = require("../core/migrate");
23
24
  const workspace_path_1 = require("../core/workspace_path");
24
25
  const subgraphs_1 = require("../graph/subgraphs");
@@ -35,6 +36,9 @@ function normalizeAlias(alias) {
35
36
  if (alias === "all") {
36
37
  throw new errors_1.UsageError("subgraph alias cannot be 'all'");
37
38
  }
39
+ if (alias === "__proto__" || alias === "constructor" || alias === "prototype") {
40
+ throw new errors_1.UsageError(`subgraph alias is reserved: ${alias}`);
41
+ }
38
42
  if (alias !== alias.toLowerCase() || !ALIAS_RE.test(alias)) {
39
43
  throw new errors_1.UsageError("subgraph alias must be lowercase and use [a-z0-9_]");
40
44
  }
@@ -674,10 +678,12 @@ function inspectSourcePath(root, alias, subgraph, allowDirty) {
674
678
  throw new errors_1.UsageError(`subgraph ${alias} is missing source_path`);
675
679
  }
676
680
  const sourcePath = normalizeContained(subgraph.source_path, `subgraphs.${alias}.source_path`);
677
- const sourceRoot = path_1.default.resolve(root, sourcePath);
678
- if (!fs_1.default.existsSync(sourceRoot) || !fs_1.default.statSync(sourceRoot).isDirectory()) {
679
- throw new errors_1.NotFoundError(`subgraph ${alias} source_path does not exist: ${sourcePath}`);
680
- }
681
+ const sourceRoot = (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: sourcePath, operation: "read" }, ({ absolutePath }) => {
682
+ if (!fs_1.default.existsSync(absolutePath) || !fs_1.default.lstatSync(absolutePath).isDirectory()) {
683
+ throw new errors_1.NotFoundError(`subgraph ${alias} source_path does not exist: ${sourcePath}`);
684
+ }
685
+ return absolutePath;
686
+ });
681
687
  const gitTop = gitRun(sourceRoot, ["rev-parse", "--show-toplevel"]);
682
688
  const gitTopPath = gitTop.stdout.trim();
683
689
  if (!gitTop.ok || !gitTopPath) {
@@ -774,13 +780,34 @@ function syncOneAlias(options) {
774
780
  const planned = [];
775
781
  for (const source of activeSources) {
776
782
  const outputPath = path_1.default.resolve(options.root, source.path);
783
+ let oldBundleHash;
784
+ let oldZipSha256;
785
+ try {
786
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: source.path, operation: "replace", createParents: true }, ({ absolutePath }) => {
787
+ oldBundleHash = existingBundleHash(absolutePath);
788
+ oldZipSha256 = fs_1.default.existsSync(absolutePath) ? (0, bundle_1.sha256Buffer)(fs_1.default.readFileSync(absolutePath)) : undefined;
789
+ });
790
+ }
791
+ catch (error) {
792
+ const message = error instanceof Error ? error.message : String(error);
793
+ sources.push({
794
+ label: source.label,
795
+ path: source.path,
796
+ enabled: source.enabled,
797
+ expected_profile: source.expected_profile,
798
+ warnings: [],
799
+ errors: [message],
800
+ });
801
+ errors.push(`source ${source.path}: ${message}`);
802
+ continue;
803
+ }
777
804
  const sourceReceipt = {
778
805
  label: source.label,
779
806
  path: source.path,
780
807
  enabled: source.enabled,
781
808
  expected_profile: source.expected_profile,
782
- old_bundle_hash: existingBundleHash(outputPath),
783
- old_zip_sha256: fs_1.default.existsSync(outputPath) ? (0, bundle_1.sha256Buffer)(fs_1.default.readFileSync(outputPath)) : undefined,
809
+ old_bundle_hash: oldBundleHash,
810
+ old_zip_sha256: oldZipSha256,
784
811
  warnings: [],
785
812
  errors: [],
786
813
  };
@@ -821,7 +848,7 @@ function syncOneAlias(options) {
821
848
  for (const item of planned) {
822
849
  const sourceReceipt = sources.find((source) => source.path === item.source.path);
823
850
  try {
824
- (0, atomic_1.atomicWriteFile)(item.outputPath, item.zip);
851
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: relativeToRoot(options.root, item.outputPath) }, item.zip);
825
852
  const verify = (0, bundle_1.verifyBundle)(gitState.sourceRoot, item.outputPath);
826
853
  sourceReceipt.verified = verify.ok;
827
854
  if (!verify.ok) {
@@ -902,11 +929,11 @@ function safeZipEntryPath(entryName) {
902
929
  }
903
930
  return normalized;
904
931
  }
905
- function writeMaterializeGitignore(targetRoot) {
906
- const gitignorePath = path_1.default.join(targetRoot, ".gitignore");
932
+ function writeMaterializeGitignore(root, targetRoot) {
933
+ const gitignoreRelative = relativeToRoot(root, path_1.default.join(targetRoot, ".gitignore"));
907
934
  const required = ["*", "!.gitignore"];
908
- const existing = fs_1.default.existsSync(gitignorePath)
909
- ? fs_1.default.readFileSync(gitignorePath, "utf8").split(/\r?\n/)
935
+ const existing = (0, filesystem_authority_1.containedPathExists)({ root, relativePath: gitignoreRelative })
936
+ ? (0, filesystem_authority_1.readContainedFile)({ root, relativePath: gitignoreRelative }, "utf8").split(/\r?\n/)
910
937
  : [];
911
938
  const lines = existing.filter((line) => line.length > 0);
912
939
  for (const line of required) {
@@ -914,13 +941,21 @@ function writeMaterializeGitignore(targetRoot) {
914
941
  lines.push(line);
915
942
  }
916
943
  }
917
- (0, atomic_1.atomicWriteFile)(gitignorePath, `${lines.join("\n")}\n`);
944
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: gitignoreRelative }, `${lines.join("\n")}\n`);
918
945
  }
919
946
  function materializeOneAlias(options) {
920
947
  const enabled = enabledSources(options.subgraph);
921
948
  const errors = [];
922
949
  const warnings = [];
923
950
  const outputDir = path_1.default.join(options.targetRoot, options.alias);
951
+ const outputRelative = relativeToRoot(options.root, outputDir);
952
+ try {
953
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: outputRelative, operation: "replace", createParents: true }, () => undefined);
954
+ }
955
+ catch (error) {
956
+ errors.push(error instanceof Error ? error.message : String(error));
957
+ return { alias: options.alias, ok: false, output_path: outputRelative, warnings, errors };
958
+ }
924
959
  if (enabled.length !== 1) {
925
960
  errors.push(`materialize requires exactly one enabled source for ${options.alias}; found ${enabled.length}`);
926
961
  return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
@@ -943,15 +978,14 @@ function materializeOneAlias(options) {
943
978
  }
944
979
  }
945
980
  const tempDir = path_1.default.join(options.targetRoot, `.${options.alias}.${process.pid}.${Date.now()}.tmp`);
946
- fs_1.default.rmSync(tempDir, { recursive: true, force: true });
981
+ const tempRelative = relativeToRoot(options.root, tempDir);
982
+ (0, filesystem_authority_1.removeContainedPath)({ root: options.root, relativePath: tempRelative, recursive: true, force: true });
947
983
  try {
948
- const parsed = (0, bundle_1.parseBundle)(bundlePath);
949
- fs_1.default.mkdirSync(tempDir, { recursive: true });
984
+ const parsed = (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: source.path, operation: "read" }, ({ absolutePath }) => (0, bundle_1.parseBundle)(absolutePath));
985
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root: options.root, relativePath: tempRelative });
950
986
  for (const [entryName, data] of parsed.entries.entries()) {
951
987
  const safeName = safeZipEntryPath(entryName);
952
- const target = path_1.default.join(tempDir, safeName);
953
- fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
954
- fs_1.default.writeFileSync(target, data);
988
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: `${tempRelative}/${safeName}` }, data);
955
989
  }
956
990
  const marker = {
957
991
  tool: "mdkg",
@@ -959,19 +993,18 @@ function materializeOneAlias(options) {
959
993
  alias: options.alias,
960
994
  bundle_path: source.path,
961
995
  bundle_hash: parsed.manifest.bundle_hash,
962
- zip_sha256: (0, bundle_1.sha256Buffer)(fs_1.default.readFileSync(bundlePath)),
996
+ zip_sha256: (0, bundle_1.sha256Buffer)((0, filesystem_authority_1.readContainedFile)({ root: options.root, relativePath: source.path }, null)),
963
997
  profile: parsed.manifest.profile,
964
998
  source_repo: options.subgraph.source_repo ?? parsed.manifest.source.repo,
965
999
  source_git_head: parsed.manifest.source.git_head,
966
1000
  generated_at: new Date().toISOString(),
967
1001
  mdkg_version: readPackageVersion(),
968
1002
  };
969
- fs_1.default.writeFileSync(path_1.default.join(tempDir, ".mdkg-materialized.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
1003
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: `${tempRelative}/.mdkg-materialized.json` }, `${JSON.stringify(marker, null, 2)}\n`);
970
1004
  if (fs_1.default.existsSync(outputDir)) {
971
- fs_1.default.rmSync(outputDir, { recursive: true, force: true });
1005
+ (0, filesystem_authority_1.removeContainedPath)({ root: options.root, relativePath: outputRelative, recursive: true, force: true });
972
1006
  }
973
- fs_1.default.mkdirSync(path_1.default.dirname(outputDir), { recursive: true });
974
- fs_1.default.renameSync(tempDir, outputDir);
1007
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: outputRelative, operation: "replace", createParents: true }, ({ absolutePath: safeOutput }) => (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: tempRelative, operation: "read" }, ({ absolutePath: safeTemp }) => fs_1.default.renameSync(safeTemp, safeOutput)));
975
1008
  return {
976
1009
  alias: options.alias,
977
1010
  ok: true,
@@ -984,7 +1017,7 @@ function materializeOneAlias(options) {
984
1017
  };
985
1018
  }
986
1019
  catch (err) {
987
- fs_1.default.rmSync(tempDir, { recursive: true, force: true });
1020
+ (0, filesystem_authority_1.removeContainedPath)({ root: options.root, relativePath: tempRelative, recursive: true, force: true });
988
1021
  errors.push(err instanceof Error ? err.message : String(err));
989
1022
  return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
990
1023
  }
@@ -1002,8 +1035,8 @@ function runSubgraphMaterializeCommand(options) {
1002
1035
  clean: Boolean(options.clean),
1003
1036
  }));
1004
1037
  if (options.gitignore) {
1005
- fs_1.default.mkdirSync(targetRoot, { recursive: true });
1006
- writeMaterializeGitignore(targetRoot);
1038
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root: options.root, relativePath: relativeToRoot(options.root, targetRoot) });
1039
+ writeMaterializeGitignore(options.root, targetRoot);
1007
1040
  }
1008
1041
  const ok = results.every((item) => item.ok === true);
1009
1042
  const receipt = {
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runTaskStartCommand = runTaskStartCommand;
7
7
  exports.runTaskUpdateCommand = runTaskUpdateCommand;
8
8
  exports.runTaskDoneCommand = runTaskDoneCommand;
9
- const fs_1 = __importDefault(require("fs"));
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const config_1 = require("../core/config");
11
+ const filesystem_authority_1 = require("../core/filesystem_authority");
12
12
  const indexer_1 = require("../graph/indexer");
13
13
  const index_cache_1 = require("../graph/index_cache");
14
14
  const reindex_1 = require("../graph/reindex");
@@ -18,7 +18,6 @@ const date_1 = require("../util/date");
18
18
  const errors_1 = require("../util/errors");
19
19
  const qid_1 = require("../util/qid");
20
20
  const id_1 = require("../util/id");
21
- const atomic_1 = require("../util/atomic");
22
21
  const lock_1 = require("../util/lock");
23
22
  const event_support_1 = require("./event_support");
24
23
  const checkpoint_1 = require("./checkpoint");
@@ -66,6 +65,75 @@ function normalizeIdList(raw, flag) {
66
65
  function normalizeIdRefList(raw, flag) {
67
66
  return parseCsvList(raw).map((value) => normalizeIdRef(value, flag));
68
67
  }
68
+ function taskCompletionCheckpointBody(loaded, artifacts, note) {
69
+ const title = typeof loaded.frontmatter.title === "string" ? loaded.frontmatter.title : loaded.id;
70
+ const artifactLines = artifacts.length > 0
71
+ ? artifacts.map((artifact) => `- ${artifact}`)
72
+ : ["- No artifacts were attached by the completion command."];
73
+ return [
74
+ "# Summary",
75
+ "",
76
+ note?.trim() || `${loaded.id} was marked done through the mdkg task lifecycle.`,
77
+ "",
78
+ "# Scope Covered",
79
+ "",
80
+ `- Completed node: ${loaded.id} (${title})`,
81
+ `- Node type: ${loaded.type}`,
82
+ "- Checkpoint source: `mdkg task done --checkpoint`",
83
+ "",
84
+ "## Changed Surfaces",
85
+ "",
86
+ `- Completed work node: ${loaded.id}`,
87
+ "- Attached artifacts are listed in checkpoint frontmatter and below.",
88
+ "",
89
+ "## Boundaries",
90
+ "",
91
+ "- in scope: structured task completion and checkpoint evidence",
92
+ "- out of scope: unrelated graph, runtime, provider, or release mutations",
93
+ "- raw secrets, raw prompts, raw payloads, and bulky execution traces excluded",
94
+ "",
95
+ "# Decisions Captured",
96
+ "",
97
+ "- See decision and approval refs on the completed node and checkpoint frontmatter.",
98
+ "",
99
+ "# Implementation Summary",
100
+ "",
101
+ `- Completion of ${loaded.id} was recorded through the structured task lifecycle.`,
102
+ "- Detailed implementation or test evidence remains on the completed node and linked refs.",
103
+ "",
104
+ "# Verification / Testing",
105
+ "",
106
+ "## Command Evidence",
107
+ "",
108
+ "- command: `mdkg task done --checkpoint`",
109
+ "- result: completed node and evidence checkpoint written",
110
+ "",
111
+ "## Pass / Fail Status",
112
+ "",
113
+ "- status: done",
114
+ "",
115
+ "## Known Warnings",
116
+ "",
117
+ "- warning: none recorded by the completion command",
118
+ "",
119
+ "# Known Issues / Follow-ups",
120
+ "",
121
+ "- Inspect the completed node and linked refs for any explicitly recorded residual work.",
122
+ "",
123
+ "## Follow-up Refs",
124
+ "",
125
+ "- task/test/goal refs: inspect the completed node and checkpoint frontmatter",
126
+ "",
127
+ "# Links / Artifacts",
128
+ "",
129
+ ...artifactLines,
130
+ "",
131
+ "# Raw Content Safety",
132
+ "",
133
+ "- This checkpoint stores the completion summary and artifact refs, not raw prompts, secrets, payloads, or bulky traces.",
134
+ "",
135
+ ].join("\n");
136
+ }
69
137
  function normalizeSkillList(raw) {
70
138
  return parseCsvList(raw).map((value) => {
71
139
  const normalized = value.toLowerCase();
@@ -116,7 +184,7 @@ function loadMutableTaskNode(root, idOrQid, wsHint) {
116
184
  throw new errors_1.UsageError(`mdkg task only supports feat, task, bug, test, and spike nodes; use markdown editing for ${node.type}:${node.id}`);
117
185
  }
118
186
  const filePath = path_1.default.resolve(root, node.path);
119
- const content = fs_1.default.readFileSync(filePath, "utf8");
187
+ const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: node.path });
120
188
  const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
121
189
  return {
122
190
  config,
@@ -134,7 +202,7 @@ function writeNodeFile(root, filePath, frontmatter, body) {
134
202
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
135
203
  const frontmatterBlock = ["---", ...lines, "---"].join("\n");
136
204
  const content = body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
137
- (0, atomic_1.atomicWriteFile)(filePath, content);
205
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: path_1.default.relative(root, filePath).split(path_1.default.sep).join("/") }, content);
138
206
  }
139
207
  function ensureStatusAllowed(config, statusRaw, flag = "--status") {
140
208
  const normalized = statusRaw.trim().toLowerCase();
@@ -290,6 +358,7 @@ function runTaskDoneCommandLocked(options) {
290
358
  now,
291
359
  });
292
360
  let checkpoint;
361
+ const checkpointBody = taskCompletionCheckpointBody(loaded, nextArtifacts, options.note);
293
362
  if (options.checkpoint && options.json) {
294
363
  checkpoint = (0, checkpoint_1.createCheckpoint)({
295
364
  root: options.root,
@@ -298,6 +367,8 @@ function runTaskDoneCommandLocked(options) {
298
367
  relates: loaded.id,
299
368
  scope: loaded.id,
300
369
  kind: options.checkpointKind,
370
+ status: "done",
371
+ body: checkpointBody,
301
372
  runId: options.runId,
302
373
  note: `checkpoint created from mdkg task done for ${loaded.id}`,
303
374
  now,
@@ -311,6 +382,8 @@ function runTaskDoneCommandLocked(options) {
311
382
  relates: loaded.id,
312
383
  scope: loaded.id,
313
384
  kind: options.checkpointKind,
385
+ status: "done",
386
+ body: checkpointBody,
314
387
  runId: options.runId,
315
388
  note: `checkpoint created from mdkg task done for ${loaded.id}`,
316
389
  now,
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
9
9
  const child_process_1 = require("child_process");
10
10
  const migrate_1 = require("../core/migrate");
11
11
  const config_1 = require("../core/config");
12
+ const filesystem_authority_1 = require("../core/filesystem_authority");
12
13
  const paths_1 = require("../core/paths");
13
14
  const version_1 = require("../core/version");
14
15
  const project_db_1 = require("../core/project_db");
@@ -16,7 +17,6 @@ const errors_1 = require("../util/errors");
16
17
  const frontmatter_1 = require("../graph/frontmatter");
17
18
  const workspace_files_1 = require("../graph/workspace_files");
18
19
  const agent_file_types_1 = require("../graph/agent_file_types");
19
- const atomic_1 = require("../util/atomic");
20
20
  const init_manifest_1 = require("./init_manifest");
21
21
  const skill_support_1 = require("./skill_support");
22
22
  const skill_mirror_1 = require("./skill_mirror");
@@ -58,13 +58,11 @@ function isAgentWorkspace(root, config) {
58
58
  ...(0, skill_mirror_1.configuredSkillMirrorTargets)(config).map((target) => path_1.default.join(root, target)),
59
59
  ].some((candidate) => fs_1.default.existsSync(candidate));
60
60
  }
61
- function copyFile(src, dest) {
62
- fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
63
- fs_1.default.copyFileSync(src, dest);
61
+ function copyFile(root, src, dest) {
62
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: repoRelativePath(root, dest) }, fs_1.default.readFileSync(src));
64
63
  }
65
- function writeFile(filePath, content) {
66
- fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
67
- fs_1.default.writeFileSync(filePath, content, "utf8");
64
+ function writeFile(root, filePath, content) {
65
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: repoRelativePath(root, filePath) }, content);
68
66
  }
69
67
  function repoRelativePath(root, filePath) {
70
68
  return path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
@@ -142,7 +140,7 @@ function planSeedFile(options) {
142
140
  reason: "missing managed init asset",
143
141
  });
144
142
  if (!options.dryRun) {
145
- copyFile(srcPath, destPath);
143
+ copyFile(options.root, srcPath, destPath);
146
144
  }
147
145
  options.managedCurrentFiles.push(options.file);
148
146
  return true;
@@ -165,7 +163,7 @@ function planSeedFile(options) {
165
163
  reason: "matches a managed seed hash",
166
164
  });
167
165
  if (!options.dryRun) {
168
- copyFile(srcPath, destPath);
166
+ copyFile(options.root, srcPath, destPath);
169
167
  }
170
168
  options.managedCurrentFiles.push(options.file);
171
169
  return true;
@@ -295,7 +293,7 @@ function migrateConfigIfNeeded(root, dryRun, summary, changes) {
295
293
  reason: reasons.join(" and "),
296
294
  });
297
295
  if (!dryRun) {
298
- writeFile(cfgPath, `${JSON.stringify(customizationConfig.config, null, 2)}\n`);
296
+ writeFile(root, cfgPath, `${JSON.stringify(customizationConfig.config, null, 2)}\n`);
299
297
  }
300
298
  }
301
299
  function sameStringArray(left, right) {
@@ -369,7 +367,7 @@ function migrateClosedGoalActiveNodes(root, dryRun, summary, changes) {
369
367
  frontmatter.last_active_node = activeNode;
370
368
  }
371
369
  delete frontmatter.active_node;
372
- writeFile(filePath, renderFrontmatterDocument(frontmatter, parsed.body.length > 0 ? parsed.body : ""));
370
+ writeFile(root, filePath, renderFrontmatterDocument(frontmatter, parsed.body.length > 0 ? parsed.body : ""));
373
371
  }
374
372
  }
375
373
  }
@@ -420,8 +418,8 @@ function migrateLegacySpecManifests(root, dryRun, summary, changes) {
420
418
  });
421
419
  if (!dryRun) {
422
420
  const frontmatter = { ...parsed.frontmatter, type: "manifest" };
423
- (0, atomic_1.atomicWriteFile)(filePath, renderFrontmatterDocument(frontmatter, parsed.body));
424
- fs_1.default.renameSync(filePath, targetPath);
421
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: repoRelativePath(root, filePath) }, renderFrontmatterDocument(frontmatter, parsed.body));
422
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: repoRelativePath(root, targetPath), operation: "create", createParents: true }, ({ absolutePath: safeTarget }) => (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: repoRelativePath(root, filePath), operation: "delete" }, ({ absolutePath: safeSource }) => fs_1.default.renameSync(safeSource, safeTarget)));
425
423
  }
426
424
  }
427
425
  }
@@ -487,7 +485,7 @@ function ensureAgentRuntimeFiles(root, dryRun, summary, changes) {
487
485
  reason: "agent workspace is missing event log",
488
486
  });
489
487
  if (!dryRun) {
490
- writeFile(eventsPath, seededInitEvent(new Date().toISOString()));
488
+ writeFile(root, eventsPath, seededInitEvent(new Date().toISOString()));
491
489
  }
492
490
  }
493
491
  else {
@@ -512,7 +510,7 @@ function ensureArchiveIgnorePolicy(root, dryRun, summary, changes) {
512
510
  });
513
511
  if (!dryRun) {
514
512
  const suffix = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
515
- writeFile(ignorePath, `${raw}${suffix}${additions.join("\n")}\n`);
513
+ writeFile(root, ignorePath, `${raw}${suffix}${additions.join("\n")}\n`);
516
514
  }
517
515
  }
518
516
  function isWritableChange(change) {
@@ -208,20 +208,33 @@ function collectContractProfileErrors(qid, node, profile) {
208
208
  return errors;
209
209
  }
210
210
  function collectChangedPaths(root) {
211
- const result = (0, child_process_1.spawnSync)("git", ["-C", root, "status", "--porcelain", "--", ".mdkg"], {
211
+ const result = (0, child_process_1.spawnSync)("git", ["-C", root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".mdkg"], {
212
212
  encoding: "utf8",
213
213
  });
214
214
  if (result.status !== 0) {
215
- return new Set();
215
+ const detail = result.stderr.trim();
216
+ throw new errors_1.ValidationError(`changed-only validation could not enumerate Git paths${detail ? `: ${detail}` : ""}`);
216
217
  }
217
218
  const changed = new Set();
218
- for (const line of result.stdout.split(/\r?\n/)) {
219
- if (!line.trim()) {
219
+ const records = result.stdout.split("\0");
220
+ for (let index = 0; index < records.length; index += 1) {
221
+ const record = records[index];
222
+ if (!record) {
220
223
  continue;
221
224
  }
222
- const rawPath = line.slice(3).trim();
223
- const filePath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
224
- changed.add(filePath.replace(/\\/g, "/"));
225
+ if (record.length < 4 || record[2] !== " ") {
226
+ throw new errors_1.ValidationError("changed-only validation received malformed Git status output");
227
+ }
228
+ const status = record.slice(0, 2);
229
+ changed.add(record.slice(3).replace(/\\/g, "/"));
230
+ if (status.includes("R") || status.includes("C")) {
231
+ const sourcePath = records[index + 1];
232
+ if (!sourcePath) {
233
+ throw new errors_1.ValidationError("changed-only validation received an incomplete Git rename record");
234
+ }
235
+ changed.add(sourcePath.replace(/\\/g, "/"));
236
+ index += 1;
237
+ }
225
238
  }
226
239
  return changed;
227
240
  }