mdkg 0.4.0 → 0.4.2
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 +58 -0
- package/CLI_COMMAND_MATRIX.md +69 -9
- package/README.md +26 -2
- package/dist/cli.js +183 -8
- package/dist/command-contract.json +690 -24
- package/dist/commands/git.js +593 -0
- package/dist/commands/new.js +50 -1
- package/dist/commands/validate.js +84 -0
- package/dist/commands/work.js +45 -2
- package/dist/graph/agent_file_types.js +74 -2
- package/dist/graph/frontmatter.js +5 -0
- package/dist/graph/node.js +28 -0
- package/dist/init/AGENT_START.md +6 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +10 -5
- package/dist/init/README.md +14 -5
- package/dist/init/init-manifest.json +12 -12
- package/dist/init/skills/default/author-mdkg-skill/SKILL.md +10 -5
- package/dist/init/templates/default/manifest.md +3 -0
- package/dist/init/templates/default/receipt.md +5 -0
- package/dist/init/templates/default/spec.md +3 -0
- package/dist/init/templates/default/work.md +1 -0
- package/dist/init/templates/default/work_order.md +3 -0
- package/dist/init/templates/specs/base.MANIFEST.md +3 -0
- package/dist/init/templates/specs/base.SPEC.md +3 -0
- package/dist/util/argparse.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runGitInspectCommand = runGitInspectCommand;
|
|
7
|
+
exports.runGitCloneCommand = runGitCloneCommand;
|
|
8
|
+
exports.runGitFetchCommand = runGitFetchCommand;
|
|
9
|
+
exports.runGitCloseoutCommand = runGitCloseoutCommand;
|
|
10
|
+
exports.runGitPushReadyCommand = runGitPushReadyCommand;
|
|
11
|
+
exports.runGitPushCommand = runGitPushCommand;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
const config_1 = require("../core/config");
|
|
17
|
+
const project_db_1 = require("../core/project_db");
|
|
18
|
+
const project_db_snapshot_1 = require("../core/project_db_snapshot");
|
|
19
|
+
const validate_1 = require("./validate");
|
|
20
|
+
const atomic_1 = require("../util/atomic");
|
|
21
|
+
const lock_1 = require("../util/lock");
|
|
22
|
+
const errors_1 = require("../util/errors");
|
|
23
|
+
function toPosix(value) {
|
|
24
|
+
return value.split(path_1.default.sep).join("/");
|
|
25
|
+
}
|
|
26
|
+
function rel(root, filePath) {
|
|
27
|
+
return toPosix(path_1.default.relative(root, filePath));
|
|
28
|
+
}
|
|
29
|
+
function stableJson(value) {
|
|
30
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
31
|
+
}
|
|
32
|
+
function sha256Buffer(value) {
|
|
33
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(value).digest("hex")}`;
|
|
34
|
+
}
|
|
35
|
+
function isInside(root, target) {
|
|
36
|
+
const relative = path_1.default.relative(root, target);
|
|
37
|
+
return relative === "" || (!relative.startsWith("..") && !path_1.default.isAbsolute(relative));
|
|
38
|
+
}
|
|
39
|
+
function containedPath(root, rawPath, label) {
|
|
40
|
+
if (rawPath.trim().length === 0) {
|
|
41
|
+
throw new errors_1.UsageError(`${label} requires a non-empty path`);
|
|
42
|
+
}
|
|
43
|
+
const resolved = path_1.default.resolve(root, rawPath);
|
|
44
|
+
if (!isInside(root, resolved)) {
|
|
45
|
+
throw new errors_1.UsageError(`${label} must stay inside the repo`);
|
|
46
|
+
}
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
function redactEmbeddedUrlUserinfo(value) {
|
|
50
|
+
return value.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+(?::[^@\s/]*)?@)/gi, "$1<redacted>@");
|
|
51
|
+
}
|
|
52
|
+
function redactRemoteRef(value) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(value);
|
|
55
|
+
if (parsed.username || parsed.password) {
|
|
56
|
+
return `${parsed.protocol}//<redacted>@${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Non-URL Git remotes such as git@github.com:org/repo.git are normal and safe to leave.
|
|
61
|
+
}
|
|
62
|
+
return redactEmbeddedUrlUserinfo(value);
|
|
63
|
+
}
|
|
64
|
+
function hasEmbeddedUrlCredentials(value) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(value);
|
|
67
|
+
return Boolean(parsed.username || parsed.password);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function assertNoEmbeddedCredentials(label, value) {
|
|
74
|
+
if (hasEmbeddedUrlCredentials(value)) {
|
|
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
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function sanitizeOutput(value) {
|
|
79
|
+
return value
|
|
80
|
+
.split(/\r?\n/)
|
|
81
|
+
.map((line) => redactRemoteRef(line).trimEnd())
|
|
82
|
+
.filter((line) => line.length > 0)
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
function git(cwd, args, allowFailure = false) {
|
|
86
|
+
const result = (0, child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: "pipe" });
|
|
87
|
+
const payload = {
|
|
88
|
+
status: result.status,
|
|
89
|
+
stdout: sanitizeOutput(result.stdout ?? ""),
|
|
90
|
+
stderr: sanitizeOutput(result.stderr ?? ""),
|
|
91
|
+
};
|
|
92
|
+
if (!allowFailure && result.status !== 0) {
|
|
93
|
+
const detail = payload.stderr || payload.stdout || `git ${args[0] ?? ""} failed`;
|
|
94
|
+
throw new errors_1.ValidationError(detail);
|
|
95
|
+
}
|
|
96
|
+
return payload;
|
|
97
|
+
}
|
|
98
|
+
function gitOptional(cwd, args) {
|
|
99
|
+
const result = git(cwd, args, true);
|
|
100
|
+
if (result.status !== 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const output = result.stdout.trim();
|
|
104
|
+
return output.length > 0 ? output : null;
|
|
105
|
+
}
|
|
106
|
+
function requireGitWorkTree(root) {
|
|
107
|
+
const inside = gitOptional(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
108
|
+
if (inside !== "true") {
|
|
109
|
+
throw new errors_1.ValidationError("mdkg git requires a Git work tree");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function parseStatusEntries(raw) {
|
|
113
|
+
if (!raw) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
return raw
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.map((line) => ({
|
|
120
|
+
index: line.slice(0, 1),
|
|
121
|
+
worktree: line.slice(1, 2),
|
|
122
|
+
path: line.slice(3).replace(/^"|"$/g, ""),
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
function currentBranch(root) {
|
|
126
|
+
return gitOptional(root, ["branch", "--show-current"]);
|
|
127
|
+
}
|
|
128
|
+
function currentHead(root) {
|
|
129
|
+
return gitOptional(root, ["rev-parse", "HEAD"]);
|
|
130
|
+
}
|
|
131
|
+
function currentTree(root) {
|
|
132
|
+
return gitOptional(root, ["rev-parse", "HEAD^{tree}"]);
|
|
133
|
+
}
|
|
134
|
+
function remoteUrl(root, remote) {
|
|
135
|
+
const value = gitOptional(root, ["remote", "get-url", remote]);
|
|
136
|
+
return value ? redactRemoteRef(value) : null;
|
|
137
|
+
}
|
|
138
|
+
function rawRemoteUrl(root, remote) {
|
|
139
|
+
return gitOptional(root, ["remote", "get-url", remote]);
|
|
140
|
+
}
|
|
141
|
+
function listRemotes(root) {
|
|
142
|
+
const output = gitOptional(root, ["remote"]);
|
|
143
|
+
if (!output) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
return output
|
|
147
|
+
.split(/\r?\n/)
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.sort()
|
|
150
|
+
.map((name) => ({
|
|
151
|
+
name,
|
|
152
|
+
fetch_url: remoteUrl(root, name) ?? "",
|
|
153
|
+
push_url: redactRemoteRef(gitOptional(root, ["remote", "get-url", "--push", name]) ?? ""),
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
function defaultRepositoryRef(inspect) {
|
|
157
|
+
const origin = inspect.remotes.find((remote) => remote.name === "origin");
|
|
158
|
+
return origin?.fetch_url ?? inspect.remotes[0]?.fetch_url ?? null;
|
|
159
|
+
}
|
|
160
|
+
function buildSourceDescriptor(inspect, remoteName = "origin") {
|
|
161
|
+
return {
|
|
162
|
+
kind: "git",
|
|
163
|
+
repository_ref: defaultRepositoryRef(inspect),
|
|
164
|
+
remote: remoteName,
|
|
165
|
+
branch: inspect.branch,
|
|
166
|
+
access_ref: "external-git-auth",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function buildAcceptedRevision(inspect) {
|
|
170
|
+
return {
|
|
171
|
+
commit_sha: inspect.head_sha,
|
|
172
|
+
tree_hash: inspect.tree_hash,
|
|
173
|
+
branch: inspect.branch,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function collectInspectReceipt(root) {
|
|
177
|
+
const inside = gitOptional(root, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
178
|
+
const statusEntries = inside ? parseStatusEntries(gitOptional(root, ["status", "--porcelain=v1"])) : [];
|
|
179
|
+
const receipt = {
|
|
180
|
+
action: "git.inspect",
|
|
181
|
+
ok: true,
|
|
182
|
+
root,
|
|
183
|
+
inside_work_tree: inside,
|
|
184
|
+
branch: inside ? currentBranch(root) : null,
|
|
185
|
+
head_sha: inside ? currentHead(root) : null,
|
|
186
|
+
tree_hash: inside ? currentTree(root) : null,
|
|
187
|
+
remotes: inside ? listRemotes(root) : [],
|
|
188
|
+
status: {
|
|
189
|
+
clean: statusEntries.length === 0,
|
|
190
|
+
entry_count: statusEntries.length,
|
|
191
|
+
entries: statusEntries,
|
|
192
|
+
},
|
|
193
|
+
source_descriptor: {
|
|
194
|
+
kind: "git",
|
|
195
|
+
repository_ref: null,
|
|
196
|
+
remote: "origin",
|
|
197
|
+
branch: null,
|
|
198
|
+
access_ref: "external-git-auth",
|
|
199
|
+
},
|
|
200
|
+
accepted_revision: {
|
|
201
|
+
commit_sha: null,
|
|
202
|
+
tree_hash: null,
|
|
203
|
+
branch: null,
|
|
204
|
+
},
|
|
205
|
+
warnings: [],
|
|
206
|
+
};
|
|
207
|
+
receipt.source_descriptor = buildSourceDescriptor(receipt);
|
|
208
|
+
receipt.accepted_revision = buildAcceptedRevision(receipt);
|
|
209
|
+
if (!inside) {
|
|
210
|
+
receipt.warnings.push("not inside a Git work tree");
|
|
211
|
+
}
|
|
212
|
+
return receipt;
|
|
213
|
+
}
|
|
214
|
+
function printJson(value) {
|
|
215
|
+
console.log(JSON.stringify(value, null, 2));
|
|
216
|
+
}
|
|
217
|
+
function printInspect(receipt) {
|
|
218
|
+
console.log(`git work tree: ${receipt.inside_work_tree ? "yes" : "no"}`);
|
|
219
|
+
console.log(`branch: ${receipt.branch ?? "(detached or unknown)"}`);
|
|
220
|
+
console.log(`head: ${receipt.head_sha ?? "(none)"}`);
|
|
221
|
+
console.log(`tree: ${receipt.tree_hash ?? "(none)"}`);
|
|
222
|
+
console.log(`status: ${receipt.status.clean ? "clean" : `${receipt.status.entry_count} change(s)`}`);
|
|
223
|
+
for (const remote of receipt.remotes) {
|
|
224
|
+
console.log(`remote ${remote.name}: ${remote.fetch_url}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function runGitInspectCommand(options) {
|
|
228
|
+
const receipt = collectInspectReceipt(options.root);
|
|
229
|
+
if (options.json) {
|
|
230
|
+
printJson(receipt);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
printInspect(receipt);
|
|
234
|
+
}
|
|
235
|
+
function runGitCloneCommand(options) {
|
|
236
|
+
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");
|
|
240
|
+
}
|
|
241
|
+
const args = ["clone"];
|
|
242
|
+
if (options.branch) {
|
|
243
|
+
args.push("--branch", options.branch);
|
|
244
|
+
}
|
|
245
|
+
args.push(options.repository, target);
|
|
246
|
+
git(options.root, args);
|
|
247
|
+
const inspect = collectInspectReceipt(target);
|
|
248
|
+
const receipt = {
|
|
249
|
+
action: "git.clone",
|
|
250
|
+
ok: true,
|
|
251
|
+
repository_ref: redactRemoteRef(options.repository),
|
|
252
|
+
target: rel(options.root, target),
|
|
253
|
+
branch: options.branch ?? inspect.branch,
|
|
254
|
+
source_descriptor: {
|
|
255
|
+
kind: "git",
|
|
256
|
+
repository_ref: redactRemoteRef(options.repository),
|
|
257
|
+
remote: "origin",
|
|
258
|
+
branch: options.branch ?? inspect.branch,
|
|
259
|
+
access_ref: "external-git-auth",
|
|
260
|
+
},
|
|
261
|
+
accepted_revision: buildAcceptedRevision(inspect),
|
|
262
|
+
inspect,
|
|
263
|
+
warnings: [],
|
|
264
|
+
};
|
|
265
|
+
if (options.json) {
|
|
266
|
+
printJson(receipt);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log("git clone complete");
|
|
270
|
+
console.log(`target: ${receipt.target}`);
|
|
271
|
+
console.log(`head: ${receipt.accepted_revision.commit_sha ?? "(none)"}`);
|
|
272
|
+
}
|
|
273
|
+
function runGitFetchCommand(options) {
|
|
274
|
+
requireGitWorkTree(options.root);
|
|
275
|
+
const remote = options.remote ?? "origin";
|
|
276
|
+
assertNoEmbeddedCredentials("--remote", remote);
|
|
277
|
+
const configuredRemoteUrl = rawRemoteUrl(options.root, remote);
|
|
278
|
+
if (configuredRemoteUrl && hasEmbeddedUrlCredentials(configuredRemoteUrl)) {
|
|
279
|
+
throw new errors_1.UsageError(`remote ${remote} must not contain embedded credentials; use external Git auth`);
|
|
280
|
+
}
|
|
281
|
+
const args = ["fetch", remote];
|
|
282
|
+
if (options.branch) {
|
|
283
|
+
args.push(options.branch);
|
|
284
|
+
}
|
|
285
|
+
const result = git(options.root, args);
|
|
286
|
+
const inspect = collectInspectReceipt(options.root);
|
|
287
|
+
const receipt = {
|
|
288
|
+
action: "git.fetch",
|
|
289
|
+
ok: true,
|
|
290
|
+
remote,
|
|
291
|
+
branch: options.branch ?? null,
|
|
292
|
+
fetch_output: [...result.stdout.split(/\r?\n/), ...result.stderr.split(/\r?\n/)].filter(Boolean),
|
|
293
|
+
inspect,
|
|
294
|
+
warnings: [],
|
|
295
|
+
};
|
|
296
|
+
if (options.json) {
|
|
297
|
+
printJson(receipt);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
console.log("git fetch complete");
|
|
301
|
+
console.log(`remote: ${remote}`);
|
|
302
|
+
console.log(`head: ${inspect.head_sha ?? "(none)"}`);
|
|
303
|
+
}
|
|
304
|
+
function validateForCloseout(root) {
|
|
305
|
+
const validation = (0, validate_1.collectValidateReceipt)({ root, json: true });
|
|
306
|
+
if (!validation.ok) {
|
|
307
|
+
throw new errors_1.ValidationError(`closeout requires mdkg validation to pass; found ${validation.error_count} error(s)`);
|
|
308
|
+
}
|
|
309
|
+
return validation;
|
|
310
|
+
}
|
|
311
|
+
function dbRuntimeParticipated(root) {
|
|
312
|
+
const config = (0, config_1.loadConfig)(root);
|
|
313
|
+
if (!config.db.enabled) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
|
|
317
|
+
return fs_1.default.existsSync(layout.runtimeFile);
|
|
318
|
+
}
|
|
319
|
+
function closeoutOutputDir(root, output) {
|
|
320
|
+
if (output) {
|
|
321
|
+
return containedPath(root, output, "--output");
|
|
322
|
+
}
|
|
323
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
324
|
+
return path_1.default.join(root, ".mdkg", "git", "closeouts", stamp);
|
|
325
|
+
}
|
|
326
|
+
function closeoutMarkdown(receipt) {
|
|
327
|
+
const lines = [
|
|
328
|
+
"# mdkg git closeout",
|
|
329
|
+
"",
|
|
330
|
+
`generated_at: ${receipt.generated_at}`,
|
|
331
|
+
`root: ${receipt.root}`,
|
|
332
|
+
`git_head: ${receipt.git.head_sha ?? "(none)"}`,
|
|
333
|
+
`git_tree: ${receipt.git.tree_hash ?? "(none)"}`,
|
|
334
|
+
`git_branch: ${receipt.git.branch ?? "(detached or unknown)"}`,
|
|
335
|
+
`validation_ok: ${receipt.validation.ok}`,
|
|
336
|
+
`validation_errors: ${receipt.validation.error_count}`,
|
|
337
|
+
`validation_warnings: ${receipt.validation.warning_count}`,
|
|
338
|
+
`db_participated: ${receipt.db_participated}`,
|
|
339
|
+
];
|
|
340
|
+
if (receipt.db_snapshot_seal) {
|
|
341
|
+
lines.push(`db_snapshot: ${receipt.db_snapshot_seal.snapshot}`);
|
|
342
|
+
lines.push(`db_snapshot_sha256: ${receipt.db_snapshot_seal.new_snapshot_sha256}`);
|
|
343
|
+
}
|
|
344
|
+
if (receipt.db_snapshot_dump) {
|
|
345
|
+
lines.push(`db_dump: ${receipt.db_snapshot_dump.output ?? "(stdout)"}`);
|
|
346
|
+
lines.push(`db_dump_sha256: ${receipt.db_snapshot_dump.sha256}`);
|
|
347
|
+
}
|
|
348
|
+
if (receipt.warnings.length > 0) {
|
|
349
|
+
lines.push("", "## Warnings", "");
|
|
350
|
+
for (const warning of receipt.warnings) {
|
|
351
|
+
lines.push(`- ${warning}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
lines.push("", "## Boundary", "");
|
|
355
|
+
lines.push("- Git authentication is external; no credentials are recorded.");
|
|
356
|
+
lines.push("- Remote push still requires an explicit mdkg git push command with remote and branch.");
|
|
357
|
+
return `${lines.join("\n")}\n`;
|
|
358
|
+
}
|
|
359
|
+
function collectCloseoutReceipt(options) {
|
|
360
|
+
requireGitWorkTree(options.root);
|
|
361
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
362
|
+
const validation = validateForCloseout(options.root);
|
|
363
|
+
const inspect = collectInspectReceipt(options.root);
|
|
364
|
+
const outputDir = closeoutOutputDir(options.root, options.output);
|
|
365
|
+
const outputRel = rel(options.root, outputDir);
|
|
366
|
+
const participated = dbRuntimeParticipated(options.root);
|
|
367
|
+
const warnings = [];
|
|
368
|
+
let dbSnapshotStatus = null;
|
|
369
|
+
let dbSnapshotSeal = null;
|
|
370
|
+
let dbSnapshotDump = null;
|
|
371
|
+
if (participated) {
|
|
372
|
+
dbSnapshotSeal = (0, project_db_snapshot_1.sealProjectDbSnapshot)(options.root, config, options.queuePolicy ?? "drain");
|
|
373
|
+
const dumpPath = toPosix(path_1.default.join(outputRel, "project-db.dump.md"));
|
|
374
|
+
const dumpResult = (0, project_db_snapshot_1.dumpProjectDbSnapshot)(options.root, config, undefined, dumpPath);
|
|
375
|
+
const { dump, ...dumpReceipt } = dumpResult;
|
|
376
|
+
dbSnapshotDump = { ...dumpReceipt, dump_sha256: sha256Buffer(dump) };
|
|
377
|
+
dbSnapshotStatus = (0, project_db_snapshot_1.projectDbSnapshotStatus)(options.root, config);
|
|
378
|
+
if (!dbSnapshotStatus.ok || dbSnapshotStatus.status !== "valid") {
|
|
379
|
+
throw new errors_1.ValidationError(`db snapshot status must be valid after closeout; got ${dbSnapshotStatus.status}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
warnings.push("project DB did not participate in this closeout; no DB snapshot was sealed");
|
|
384
|
+
}
|
|
385
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
386
|
+
const baseReceipt = {
|
|
387
|
+
action: "git.closeout",
|
|
388
|
+
ok: true,
|
|
389
|
+
root: options.root,
|
|
390
|
+
output_dir: outputRel,
|
|
391
|
+
generated_at: new Date().toISOString(),
|
|
392
|
+
git: inspect,
|
|
393
|
+
validation,
|
|
394
|
+
db_participated: participated,
|
|
395
|
+
db_snapshot_status: dbSnapshotStatus,
|
|
396
|
+
db_snapshot_seal: dbSnapshotSeal,
|
|
397
|
+
db_snapshot_dump: dbSnapshotDump,
|
|
398
|
+
warnings,
|
|
399
|
+
};
|
|
400
|
+
const jsonPath = path_1.default.join(outputDir, "closeout.json");
|
|
401
|
+
const markdownPath = path_1.default.join(outputDir, "closeout.md");
|
|
402
|
+
const receipt = {
|
|
403
|
+
...baseReceipt,
|
|
404
|
+
static_receipts: {
|
|
405
|
+
json: rel(options.root, jsonPath),
|
|
406
|
+
markdown: rel(options.root, markdownPath),
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
(0, atomic_1.atomicWriteFile)(jsonPath, stableJson(receipt));
|
|
410
|
+
(0, atomic_1.atomicWriteFile)(markdownPath, closeoutMarkdown(baseReceipt));
|
|
411
|
+
return receipt;
|
|
412
|
+
}
|
|
413
|
+
function runGitCloseoutCommand(options) {
|
|
414
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
415
|
+
const receipt = (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => collectCloseoutReceipt(options));
|
|
416
|
+
if (options.json) {
|
|
417
|
+
printJson(receipt);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
console.log("git closeout written");
|
|
421
|
+
console.log(`json: ${receipt.static_receipts.json}`);
|
|
422
|
+
console.log(`markdown: ${receipt.static_receipts.markdown}`);
|
|
423
|
+
if (receipt.db_snapshot_seal) {
|
|
424
|
+
console.log(`db snapshot: ${receipt.db_snapshot_seal.snapshot}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function check(name, ok, detail, error = null, warning = null) {
|
|
428
|
+
return {
|
|
429
|
+
name,
|
|
430
|
+
ok,
|
|
431
|
+
level: ok ? (warning ? "warn" : "ok") : "fail",
|
|
432
|
+
detail,
|
|
433
|
+
errors: ok || !error ? [] : [error],
|
|
434
|
+
warnings: warning ? [warning] : [],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function collectPushReadyReceipt(options) {
|
|
438
|
+
const remote = options.remote;
|
|
439
|
+
const branch = options.branch;
|
|
440
|
+
if (!remote) {
|
|
441
|
+
throw new errors_1.UsageError("mdkg git push-ready requires --remote <name>");
|
|
442
|
+
}
|
|
443
|
+
if (!branch) {
|
|
444
|
+
throw new errors_1.UsageError("mdkg git push-ready requires --branch <name>");
|
|
445
|
+
}
|
|
446
|
+
assertNoEmbeddedCredentials("--remote", remote);
|
|
447
|
+
requireGitWorkTree(options.root);
|
|
448
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
449
|
+
const inspect = collectInspectReceipt(options.root);
|
|
450
|
+
const validation = (0, validate_1.collectValidateReceipt)({ root: options.root, json: true });
|
|
451
|
+
const rawUrl = rawRemoteUrl(options.root, remote);
|
|
452
|
+
const redactedRemoteUrl = rawUrl ? redactRemoteRef(rawUrl) : null;
|
|
453
|
+
const checks = [];
|
|
454
|
+
checks.push(check("remote-explicit", true, `remote target is ${remote}`));
|
|
455
|
+
checks.push(check("branch-explicit", true, `branch target is ${branch}`));
|
|
456
|
+
checks.push(check("remote-exists", rawUrl !== null, rawUrl ? `remote ${remote} exists` : `remote ${remote} missing`, `remote ${remote} is not configured`));
|
|
457
|
+
if (rawUrl) {
|
|
458
|
+
checks.push(check("remote-credential-boundary", !hasEmbeddedUrlCredentials(rawUrl), "remote URL does not embed credentials", "remote URL contains embedded credentials; use external Git auth"));
|
|
459
|
+
}
|
|
460
|
+
checks.push(check("worktree-clean", inspect.status.clean, inspect.status.clean ? "working tree is clean" : `${inspect.status.entry_count} changed path(s) present`, "working tree must be clean before push-ready"));
|
|
461
|
+
checks.push(check("validation", validation.ok, validation.ok ? "mdkg validate passes" : `mdkg validate has ${validation.error_count} error(s)`, "mdkg validation must pass before push"));
|
|
462
|
+
let snapshotStatus = null;
|
|
463
|
+
if (config.db.enabled) {
|
|
464
|
+
const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(options.root, config.db);
|
|
465
|
+
if (fs_1.default.existsSync(layout.runtimeFile)) {
|
|
466
|
+
snapshotStatus = (0, project_db_snapshot_1.projectDbSnapshotStatus)(options.root, config);
|
|
467
|
+
checks.push(check("db-snapshot", snapshotStatus.ok && snapshotStatus.status === "valid", `db snapshot status is ${snapshotStatus.status}`, "project DB participated; seal a valid snapshot before push"));
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
checks.push(check("db-snapshot", true, "project DB is enabled but runtime DB is absent", null, "no runtime DB participated in this push"));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
checks.push(check("db-snapshot", true, "project DB disabled; no DB closeout required"));
|
|
475
|
+
}
|
|
476
|
+
const errors = checks.flatMap((item) => item.errors.map((error) => `${item.name}: ${error}`));
|
|
477
|
+
const warnings = checks.flatMap((item) => item.warnings.map((warning) => `${item.name}: ${warning}`));
|
|
478
|
+
return {
|
|
479
|
+
action: "git.push_ready",
|
|
480
|
+
ok: errors.length === 0,
|
|
481
|
+
root: options.root,
|
|
482
|
+
remote,
|
|
483
|
+
branch,
|
|
484
|
+
remote_url: redactedRemoteUrl,
|
|
485
|
+
git: inspect,
|
|
486
|
+
validation,
|
|
487
|
+
db_snapshot_status: snapshotStatus,
|
|
488
|
+
checks,
|
|
489
|
+
warning_count: warnings.length,
|
|
490
|
+
failure_count: errors.length,
|
|
491
|
+
warnings,
|
|
492
|
+
errors,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function runGitPushReadyCommand(options) {
|
|
496
|
+
const receipt = collectPushReadyReceipt(options);
|
|
497
|
+
if (options.json) {
|
|
498
|
+
printJson(receipt);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
for (const item of receipt.checks) {
|
|
502
|
+
console.log(`${item.level}: ${item.name} - ${item.detail}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (!receipt.ok) {
|
|
506
|
+
throw new errors_1.ValidationError(`git push-ready failed with ${receipt.failure_count} issue(s)`);
|
|
507
|
+
}
|
|
508
|
+
if (!options.json) {
|
|
509
|
+
console.log("git push-ready ok");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function hasStagedChanges(root) {
|
|
513
|
+
const result = git(root, ["diff", "--cached", "--quiet"], true);
|
|
514
|
+
return result.status !== 0;
|
|
515
|
+
}
|
|
516
|
+
function ensureGitIdentity(root) {
|
|
517
|
+
const email = gitOptional(root, ["config", "user.email"]);
|
|
518
|
+
const name = gitOptional(root, ["config", "user.name"]);
|
|
519
|
+
if (!email || !name) {
|
|
520
|
+
throw new errors_1.ValidationError("git commit requires user.name and user.email to be configured");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function collectPushReceipt(options) {
|
|
524
|
+
const remote = options.remote;
|
|
525
|
+
const branch = options.branch;
|
|
526
|
+
if (!remote) {
|
|
527
|
+
throw new errors_1.UsageError("mdkg git push requires --remote <name>");
|
|
528
|
+
}
|
|
529
|
+
if (!branch) {
|
|
530
|
+
throw new errors_1.UsageError("mdkg git push requires --branch <name>");
|
|
531
|
+
}
|
|
532
|
+
assertNoEmbeddedCredentials("--remote", remote);
|
|
533
|
+
requireGitWorkTree(options.root);
|
|
534
|
+
let closeout = null;
|
|
535
|
+
let commitCreated = false;
|
|
536
|
+
let commitMessage = null;
|
|
537
|
+
if (options.stageAll) {
|
|
538
|
+
if (!options.message) {
|
|
539
|
+
throw new errors_1.UsageError("mdkg git push --stage-all requires --message <text>");
|
|
540
|
+
}
|
|
541
|
+
ensureGitIdentity(options.root);
|
|
542
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
543
|
+
closeout = (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => collectCloseoutReceipt({
|
|
544
|
+
root: options.root,
|
|
545
|
+
queuePolicy: options.queuePolicy,
|
|
546
|
+
json: true,
|
|
547
|
+
}));
|
|
548
|
+
git(options.root, ["add", "-A"]);
|
|
549
|
+
if (hasStagedChanges(options.root)) {
|
|
550
|
+
git(options.root, ["commit", "-m", options.message]);
|
|
551
|
+
commitCreated = true;
|
|
552
|
+
commitMessage = options.message;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const pushReady = collectPushReadyReceipt({ root: options.root, remote, branch, json: true });
|
|
556
|
+
if (!pushReady.ok) {
|
|
557
|
+
throw new errors_1.ValidationError(`git push-ready failed with ${pushReady.failure_count} issue(s)`);
|
|
558
|
+
}
|
|
559
|
+
const push = git(options.root, ["push", remote, `HEAD:refs/heads/${branch}`]);
|
|
560
|
+
const inspect = collectInspectReceipt(options.root);
|
|
561
|
+
return {
|
|
562
|
+
action: "git.push",
|
|
563
|
+
ok: true,
|
|
564
|
+
remote,
|
|
565
|
+
branch,
|
|
566
|
+
head_sha: inspect.head_sha,
|
|
567
|
+
pushed_ref: `refs/heads/${branch}`,
|
|
568
|
+
stage_all: Boolean(options.stageAll),
|
|
569
|
+
closeout,
|
|
570
|
+
commit: {
|
|
571
|
+
created: commitCreated,
|
|
572
|
+
message: commitMessage,
|
|
573
|
+
sha: commitCreated ? inspect.head_sha : null,
|
|
574
|
+
},
|
|
575
|
+
push_ready: pushReady,
|
|
576
|
+
push_output: [...push.stdout.split(/\r?\n/), ...push.stderr.split(/\r?\n/)].filter(Boolean),
|
|
577
|
+
warnings: [],
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function runGitPushCommand(options) {
|
|
581
|
+
const receipt = collectPushReceipt(options);
|
|
582
|
+
if (options.json) {
|
|
583
|
+
printJson(receipt);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
console.log("git push complete");
|
|
587
|
+
console.log(`remote: ${receipt.remote}`);
|
|
588
|
+
console.log(`branch: ${receipt.branch}`);
|
|
589
|
+
console.log(`head: ${receipt.head_sha ?? "(none)"}`);
|
|
590
|
+
if (receipt.commit.created) {
|
|
591
|
+
console.log(`commit: ${receipt.commit.sha}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
package/dist/commands/new.js
CHANGED
|
@@ -12,6 +12,7 @@ const node_1 = require("../graph/node");
|
|
|
12
12
|
const agent_file_types_1 = require("../graph/agent_file_types");
|
|
13
13
|
const indexer_1 = require("../graph/indexer");
|
|
14
14
|
const loader_1 = require("../templates/loader");
|
|
15
|
+
const frontmatter_1 = require("../graph/frontmatter");
|
|
15
16
|
const date_1 = require("../util/date");
|
|
16
17
|
const errors_1 = require("../util/errors");
|
|
17
18
|
const qid_1 = require("../util/qid");
|
|
@@ -25,6 +26,7 @@ const event_support_1 = require("./event_support");
|
|
|
25
26
|
const DEC_ID_RE = /^dec-[0-9]+$/;
|
|
26
27
|
const DEC_STATUS = new Set(["proposed", "accepted", "rejected", "superseded"]);
|
|
27
28
|
const SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
29
|
+
const CONTRACT_METADATA_RE = /^[a-z][a-z0-9_]*(?:-[a-z0-9_]+)*$/;
|
|
28
30
|
const CORE_TYPES = new Set(["rule"]);
|
|
29
31
|
const DESIGN_TYPES = new Set(["prd", "edd", "dec", "prop"]);
|
|
30
32
|
const LEGACY_NEW_SPEC_WARNING = "warning: mdkg new spec is legacy; use mdkg new manifest. This alias creates MANIFEST.md with type: manifest during the compatibility release.";
|
|
@@ -77,6 +79,16 @@ function normalizeRef(value, key) {
|
|
|
77
79
|
function normalizeRefList(raw, key) {
|
|
78
80
|
return parseCsvList(raw).map((value) => normalizeRef(value, key));
|
|
79
81
|
}
|
|
82
|
+
function normalizeContractMetadataToken(raw, key) {
|
|
83
|
+
if (raw === undefined) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const normalized = raw.toLowerCase();
|
|
87
|
+
if (!CONTRACT_METADATA_RE.test(normalized)) {
|
|
88
|
+
throw new errors_1.UsageError(`${key} must be lowercase snake/kebab style`);
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
80
92
|
function normalizeIdRefList(raw, key) {
|
|
81
93
|
return parseCsvList(raw).map((value) => normalizeIdRef(value, key));
|
|
82
94
|
}
|
|
@@ -176,6 +188,18 @@ function ensureExists(index, value, ws, label) {
|
|
|
176
188
|
throw new errors_1.UsageError(`${label} cannot target read-only subgraph node ${node.qid}`);
|
|
177
189
|
}
|
|
178
190
|
}
|
|
191
|
+
function mergeRenderedFrontmatter(content, filePath, additions) {
|
|
192
|
+
const entries = Object.entries(additions).filter((entry) => entry[1] !== undefined);
|
|
193
|
+
if (entries.length === 0) {
|
|
194
|
+
return content;
|
|
195
|
+
}
|
|
196
|
+
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
197
|
+
for (const [key, value] of entries) {
|
|
198
|
+
frontmatter[key] = value;
|
|
199
|
+
}
|
|
200
|
+
const lines = ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter), "---", body.trimStart()];
|
|
201
|
+
return lines.join("\n").endsWith("\n") ? lines.join("\n") : `${lines.join("\n")}\n`;
|
|
202
|
+
}
|
|
179
203
|
function runNewCommandLocked(options) {
|
|
180
204
|
const title = options.title.trim();
|
|
181
205
|
if (!title) {
|
|
@@ -274,6 +298,24 @@ function runNewCommandLocked(options) {
|
|
|
274
298
|
if (options.cases && type !== "test") {
|
|
275
299
|
throw new errors_1.UsageError("--cases is only valid for test nodes");
|
|
276
300
|
}
|
|
301
|
+
const contractProfile = normalizeContractMetadataToken(options.contractProfile, "--contract-profile");
|
|
302
|
+
const validationPolicyRef = options.validationPolicyRef
|
|
303
|
+
? normalizeRef(options.validationPolicyRef, "--validation-policy-ref")
|
|
304
|
+
: undefined;
|
|
305
|
+
const evidencePolicyRef = options.evidencePolicyRef
|
|
306
|
+
? normalizeRef(options.evidencePolicyRef, "--evidence-policy-ref")
|
|
307
|
+
: undefined;
|
|
308
|
+
const receiptKind = normalizeContractMetadataToken(options.receiptKind, "--receipt-kind");
|
|
309
|
+
const redactionClass = normalizeContractMetadataToken(options.redactionClass, "--redaction-class");
|
|
310
|
+
if (contractProfile && !["manifest", "work", "work_order", "receipt"].includes(type)) {
|
|
311
|
+
throw new errors_1.UsageError("--contract-profile is only valid for manifest, work, work_order, and receipt");
|
|
312
|
+
}
|
|
313
|
+
if ((validationPolicyRef || evidencePolicyRef) && !["manifest", "work_order", "receipt"].includes(type)) {
|
|
314
|
+
throw new errors_1.UsageError("--validation-policy-ref and --evidence-policy-ref are only valid for manifest, work_order, and receipt");
|
|
315
|
+
}
|
|
316
|
+
if ((receiptKind || redactionClass) && type !== "receipt") {
|
|
317
|
+
throw new errors_1.UsageError("--receipt-kind and --redaction-class are only valid for receipt");
|
|
318
|
+
}
|
|
277
319
|
const epic = options.epic ? normalizeIdRef(options.epic, "--epic") : undefined;
|
|
278
320
|
const parent = options.parent ? normalizeIdRef(options.parent, "--parent") : undefined;
|
|
279
321
|
const prev = options.prev ? normalizeIdRef(options.prev, "--prev") : undefined;
|
|
@@ -331,7 +373,7 @@ function runNewCommandLocked(options) {
|
|
|
331
373
|
if (template.source === "bundled") {
|
|
332
374
|
console.error(`warning: using bundled template fallback for ${type}; run \`mdkg upgrade --apply\` to vendor missing local templates`);
|
|
333
375
|
}
|
|
334
|
-
const
|
|
376
|
+
const renderedContent = (0, loader_1.renderTemplate)(template, {
|
|
335
377
|
id,
|
|
336
378
|
type,
|
|
337
379
|
title,
|
|
@@ -360,6 +402,13 @@ function runNewCommandLocked(options) {
|
|
|
360
402
|
created: today,
|
|
361
403
|
updated: today,
|
|
362
404
|
});
|
|
405
|
+
const content = mergeRenderedFrontmatter(renderedContent, filePath, {
|
|
406
|
+
contract_profile: contractProfile,
|
|
407
|
+
validation_policy_ref: validationPolicyRef,
|
|
408
|
+
evidence_policy_ref: evidencePolicyRef,
|
|
409
|
+
receipt_kind: receiptKind,
|
|
410
|
+
redaction_class: redactionClass,
|
|
411
|
+
});
|
|
363
412
|
try {
|
|
364
413
|
(0, atomic_1.writeFileExclusive)(filePath, content);
|
|
365
414
|
}
|