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.
- package/CHANGELOG.md +71 -15
- package/CLI_COMMAND_MATRIX.md +59 -2
- package/README.md +20 -2
- package/dist/cli.js +124 -2
- package/dist/command-contract.json +756 -2
- package/dist/commands/archive.js +31 -14
- package/dist/commands/bundle.js +180 -44
- package/dist/commands/capability.js +1 -0
- package/dist/commands/checkpoint.js +6 -5
- package/dist/commands/event_support.js +16 -7
- package/dist/commands/fix.js +11 -8
- package/dist/commands/git.js +41 -10
- package/dist/commands/goal.js +23 -23
- package/dist/commands/graph.js +64 -10
- package/dist/commands/handoff.js +4 -2
- package/dist/commands/init.js +28 -23
- package/dist/commands/loop.js +1668 -0
- package/dist/commands/loop_descriptors.js +219 -0
- package/dist/commands/mcp.js +101 -11
- package/dist/commands/new.js +49 -3
- package/dist/commands/pack.js +14 -7
- package/dist/commands/search.js +10 -1
- package/dist/commands/skill.js +12 -9
- package/dist/commands/skill_mirror.js +39 -31
- package/dist/commands/subgraph.js +59 -26
- package/dist/commands/task.js +77 -4
- package/dist/commands/upgrade.js +13 -15
- package/dist/commands/validate.js +20 -7
- package/dist/commands/work.js +44 -26
- package/dist/commands/workspace.js +10 -4
- package/dist/core/config.js +45 -17
- package/dist/core/filesystem_authority.js +281 -0
- package/dist/core/project_db_events.js +4 -0
- package/dist/core/project_db_materializer.js +2 -0
- package/dist/core/project_db_migrations.js +238 -181
- package/dist/core/project_db_snapshot.js +64 -14
- package/dist/core/workspace_path.js +7 -0
- package/dist/graph/archive_integrity.js +1 -1
- package/dist/graph/capabilities_index_cache.js +4 -1
- package/dist/graph/frontmatter.js +38 -0
- package/dist/graph/goal_scope.js +4 -1
- package/dist/graph/index_cache.js +37 -7
- package/dist/graph/loop_bindings.js +39 -0
- package/dist/graph/node.js +209 -1
- package/dist/graph/node_body.js +52 -6
- package/dist/graph/skills_index_cache.js +41 -6
- package/dist/graph/skills_indexer.js +5 -2
- package/dist/graph/sqlite_index.js +118 -105
- package/dist/graph/subgraphs.js +142 -5
- package/dist/graph/validate_graph.js +109 -13
- package/dist/graph/workspace_files.js +39 -9
- package/dist/init/AGENT_START.md +16 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
- package/dist/init/config.json +7 -1
- package/dist/init/init-manifest.json +49 -4
- package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
- package/dist/init/templates/default/loop.md +160 -0
- package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
- package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
- package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
- package/dist/init/templates/loops/security-audit.loop.md +140 -0
- package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
- package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
- package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
- package/dist/pack/order.js +3 -1
- package/dist/pack/pack.js +139 -6
- package/dist/templates/loader.js +9 -3
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
package/dist/commands/git.js
CHANGED
|
@@ -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
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
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
|
-
|
|
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,
|
|
410
|
-
(0,
|
|
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",
|
package/dist/commands/goal.js
CHANGED
|
@@ -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 (!
|
|
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(
|
|
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,
|
|
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
|
|
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,
|
|
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 (!
|
|
120
|
+
if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: ".mdkg/state/selected-goal.json" })) {
|
|
121
121
|
return false;
|
|
122
122
|
}
|
|
123
|
-
|
|
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 =
|
|
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,
|
package/dist/commands/graph.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 });
|
package/dist/commands/handoff.js
CHANGED
|
@@ -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,
|
|
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",
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
}
|