mdkg 0.4.1 → 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 (71) hide show
  1. package/CHANGELOG.md +103 -15
  2. package/CLI_COMMAND_MATRIX.md +109 -2
  3. package/README.md +40 -2
  4. package/dist/cli.js +262 -2
  5. package/dist/command-contract.json +1902 -524
  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 +624 -0
  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/argparse.js +5 -0
  69. package/dist/util/lock.js +10 -9
  70. package/dist/util/zip.js +163 -9
  71. package/package.json +8 -4
@@ -0,0 +1,624 @@
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 filesystem_authority_1 = require("../core/filesystem_authority");
18
+ const project_db_1 = require("../core/project_db");
19
+ const project_db_snapshot_1 = require("../core/project_db_snapshot");
20
+ const validate_1 = require("./validate");
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 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
+ }
94
+ function sanitizeOutput(value) {
95
+ return value
96
+ .split(/\r?\n/)
97
+ .map((line) => redactRemoteRef(line).trimEnd())
98
+ .filter((line) => line.length > 0)
99
+ .join("\n");
100
+ }
101
+ function git(cwd, args, allowFailure = false) {
102
+ const result = (0, child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: "pipe" });
103
+ const payload = {
104
+ status: result.status,
105
+ stdout: sanitizeOutput(result.stdout ?? ""),
106
+ stderr: sanitizeOutput(result.stderr ?? ""),
107
+ };
108
+ if (!allowFailure && result.status !== 0) {
109
+ const detail = payload.stderr || payload.stdout || `git ${args[0] ?? ""} failed`;
110
+ throw new errors_1.ValidationError(detail);
111
+ }
112
+ return payload;
113
+ }
114
+ function gitOptional(cwd, args) {
115
+ const result = git(cwd, args, true);
116
+ if (result.status !== 0) {
117
+ return null;
118
+ }
119
+ const output = result.stdout.trim();
120
+ return output.length > 0 ? output : null;
121
+ }
122
+ function requireGitWorkTree(root) {
123
+ const inside = gitOptional(root, ["rev-parse", "--is-inside-work-tree"]);
124
+ if (inside !== "true") {
125
+ throw new errors_1.ValidationError("mdkg git requires a Git work tree");
126
+ }
127
+ }
128
+ function parseStatusEntries(raw) {
129
+ if (!raw) {
130
+ return [];
131
+ }
132
+ return raw
133
+ .split(/\r?\n/)
134
+ .filter(Boolean)
135
+ .map((line) => ({
136
+ index: line.slice(0, 1),
137
+ worktree: line.slice(1, 2),
138
+ path: line.slice(3).replace(/^"|"$/g, ""),
139
+ }));
140
+ }
141
+ function currentBranch(root) {
142
+ return gitOptional(root, ["branch", "--show-current"]);
143
+ }
144
+ function currentHead(root) {
145
+ return gitOptional(root, ["rev-parse", "HEAD"]);
146
+ }
147
+ function currentTree(root) {
148
+ return gitOptional(root, ["rev-parse", "HEAD^{tree}"]);
149
+ }
150
+ function remoteUrl(root, remote) {
151
+ const value = gitOptional(root, ["remote", "get-url", remote]);
152
+ return value ? redactRemoteRef(value) : null;
153
+ }
154
+ function rawRemoteUrl(root, remote) {
155
+ return gitOptional(root, ["remote", "get-url", remote]);
156
+ }
157
+ function listRemotes(root) {
158
+ const output = gitOptional(root, ["remote"]);
159
+ if (!output) {
160
+ return [];
161
+ }
162
+ return output
163
+ .split(/\r?\n/)
164
+ .filter(Boolean)
165
+ .sort()
166
+ .map((name) => ({
167
+ name,
168
+ fetch_url: remoteUrl(root, name) ?? "",
169
+ push_url: redactRemoteRef(gitOptional(root, ["remote", "get-url", "--push", name]) ?? ""),
170
+ }));
171
+ }
172
+ function defaultRepositoryRef(inspect) {
173
+ const origin = inspect.remotes.find((remote) => remote.name === "origin");
174
+ return origin?.fetch_url ?? inspect.remotes[0]?.fetch_url ?? null;
175
+ }
176
+ function buildSourceDescriptor(inspect, remoteName = "origin") {
177
+ return {
178
+ kind: "git",
179
+ repository_ref: defaultRepositoryRef(inspect),
180
+ remote: remoteName,
181
+ branch: inspect.branch,
182
+ access_ref: "external-git-auth",
183
+ };
184
+ }
185
+ function buildAcceptedRevision(inspect) {
186
+ return {
187
+ commit_sha: inspect.head_sha,
188
+ tree_hash: inspect.tree_hash,
189
+ branch: inspect.branch,
190
+ };
191
+ }
192
+ function collectInspectReceipt(root) {
193
+ const inside = gitOptional(root, ["rev-parse", "--is-inside-work-tree"]) === "true";
194
+ const statusEntries = inside ? parseStatusEntries(gitOptional(root, ["status", "--porcelain=v1"])) : [];
195
+ const receipt = {
196
+ action: "git.inspect",
197
+ ok: true,
198
+ root,
199
+ inside_work_tree: inside,
200
+ branch: inside ? currentBranch(root) : null,
201
+ head_sha: inside ? currentHead(root) : null,
202
+ tree_hash: inside ? currentTree(root) : null,
203
+ remotes: inside ? listRemotes(root) : [],
204
+ status: {
205
+ clean: statusEntries.length === 0,
206
+ entry_count: statusEntries.length,
207
+ entries: statusEntries,
208
+ },
209
+ source_descriptor: {
210
+ kind: "git",
211
+ repository_ref: null,
212
+ remote: "origin",
213
+ branch: null,
214
+ access_ref: "external-git-auth",
215
+ },
216
+ accepted_revision: {
217
+ commit_sha: null,
218
+ tree_hash: null,
219
+ branch: null,
220
+ },
221
+ warnings: [],
222
+ };
223
+ receipt.source_descriptor = buildSourceDescriptor(receipt);
224
+ receipt.accepted_revision = buildAcceptedRevision(receipt);
225
+ if (!inside) {
226
+ receipt.warnings.push("not inside a Git work tree");
227
+ }
228
+ return receipt;
229
+ }
230
+ function printJson(value) {
231
+ console.log(JSON.stringify(value, null, 2));
232
+ }
233
+ function printInspect(receipt) {
234
+ console.log(`git work tree: ${receipt.inside_work_tree ? "yes" : "no"}`);
235
+ console.log(`branch: ${receipt.branch ?? "(detached or unknown)"}`);
236
+ console.log(`head: ${receipt.head_sha ?? "(none)"}`);
237
+ console.log(`tree: ${receipt.tree_hash ?? "(none)"}`);
238
+ console.log(`status: ${receipt.status.clean ? "clean" : `${receipt.status.entry_count} change(s)`}`);
239
+ for (const remote of receipt.remotes) {
240
+ console.log(`remote ${remote.name}: ${remote.fetch_url}`);
241
+ }
242
+ }
243
+ function runGitInspectCommand(options) {
244
+ const receipt = collectInspectReceipt(options.root);
245
+ if (options.json) {
246
+ printJson(receipt);
247
+ return;
248
+ }
249
+ printInspect(receipt);
250
+ }
251
+ function runGitCloneCommand(options) {
252
+ assertNoEmbeddedCredentials("repository", options.repository);
253
+ assertGitOperand("repository", options.repository);
254
+ if (options.branch) {
255
+ assertGitBranch(options.root, options.branch);
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
+ });
264
+ const args = ["clone"];
265
+ if (options.branch) {
266
+ args.push("--branch", options.branch);
267
+ }
268
+ args.push("--", options.repository, target);
269
+ git(options.root, args);
270
+ const inspect = collectInspectReceipt(target);
271
+ const receipt = {
272
+ action: "git.clone",
273
+ ok: true,
274
+ repository_ref: redactRemoteRef(options.repository),
275
+ target: rel(options.root, target),
276
+ branch: options.branch ?? inspect.branch,
277
+ source_descriptor: {
278
+ kind: "git",
279
+ repository_ref: redactRemoteRef(options.repository),
280
+ remote: "origin",
281
+ branch: options.branch ?? inspect.branch,
282
+ access_ref: "external-git-auth",
283
+ },
284
+ accepted_revision: buildAcceptedRevision(inspect),
285
+ inspect,
286
+ warnings: [],
287
+ };
288
+ if (options.json) {
289
+ printJson(receipt);
290
+ return;
291
+ }
292
+ console.log("git clone complete");
293
+ console.log(`target: ${receipt.target}`);
294
+ console.log(`head: ${receipt.accepted_revision.commit_sha ?? "(none)"}`);
295
+ }
296
+ function runGitFetchCommand(options) {
297
+ requireGitWorkTree(options.root);
298
+ const remote = options.remote ?? "origin";
299
+ assertGitOperand("--remote", remote);
300
+ if (options.branch) {
301
+ assertGitBranch(options.root, options.branch);
302
+ }
303
+ assertNoEmbeddedCredentials("--remote", remote);
304
+ const configuredRemoteUrl = rawRemoteUrl(options.root, remote);
305
+ if (configuredRemoteUrl && hasEmbeddedUrlCredentials(configuredRemoteUrl)) {
306
+ throw new errors_1.UsageError(`remote ${remote} must not contain embedded credentials; use external Git auth`);
307
+ }
308
+ const args = ["fetch", "--", remote];
309
+ if (options.branch) {
310
+ args.push(options.branch);
311
+ }
312
+ const result = git(options.root, args);
313
+ const inspect = collectInspectReceipt(options.root);
314
+ const receipt = {
315
+ action: "git.fetch",
316
+ ok: true,
317
+ remote,
318
+ branch: options.branch ?? null,
319
+ fetch_output: [...result.stdout.split(/\r?\n/), ...result.stderr.split(/\r?\n/)].filter(Boolean),
320
+ inspect,
321
+ warnings: [],
322
+ };
323
+ if (options.json) {
324
+ printJson(receipt);
325
+ return;
326
+ }
327
+ console.log("git fetch complete");
328
+ console.log(`remote: ${remote}`);
329
+ console.log(`head: ${inspect.head_sha ?? "(none)"}`);
330
+ }
331
+ function validateForCloseout(root) {
332
+ const validation = (0, validate_1.collectValidateReceipt)({ root, json: true });
333
+ if (!validation.ok) {
334
+ throw new errors_1.ValidationError(`closeout requires mdkg validation to pass; found ${validation.error_count} error(s)`);
335
+ }
336
+ return validation;
337
+ }
338
+ function dbRuntimeParticipated(root) {
339
+ const config = (0, config_1.loadConfig)(root);
340
+ if (!config.db.enabled) {
341
+ return false;
342
+ }
343
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
344
+ return fs_1.default.existsSync(layout.runtimeFile);
345
+ }
346
+ function closeoutOutputDir(root, output) {
347
+ if (output) {
348
+ return containedPath(root, output, "--output");
349
+ }
350
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
351
+ return path_1.default.join(root, ".mdkg", "git", "closeouts", stamp);
352
+ }
353
+ function closeoutMarkdown(receipt) {
354
+ const lines = [
355
+ "# mdkg git closeout",
356
+ "",
357
+ `generated_at: ${receipt.generated_at}`,
358
+ `root: ${receipt.root}`,
359
+ `git_head: ${receipt.git.head_sha ?? "(none)"}`,
360
+ `git_tree: ${receipt.git.tree_hash ?? "(none)"}`,
361
+ `git_branch: ${receipt.git.branch ?? "(detached or unknown)"}`,
362
+ `validation_ok: ${receipt.validation.ok}`,
363
+ `validation_errors: ${receipt.validation.error_count}`,
364
+ `validation_warnings: ${receipt.validation.warning_count}`,
365
+ `db_participated: ${receipt.db_participated}`,
366
+ ];
367
+ if (receipt.db_snapshot_seal) {
368
+ lines.push(`db_snapshot: ${receipt.db_snapshot_seal.snapshot}`);
369
+ lines.push(`db_snapshot_sha256: ${receipt.db_snapshot_seal.new_snapshot_sha256}`);
370
+ }
371
+ if (receipt.db_snapshot_dump) {
372
+ lines.push(`db_dump: ${receipt.db_snapshot_dump.output ?? "(stdout)"}`);
373
+ lines.push(`db_dump_sha256: ${receipt.db_snapshot_dump.sha256}`);
374
+ }
375
+ if (receipt.warnings.length > 0) {
376
+ lines.push("", "## Warnings", "");
377
+ for (const warning of receipt.warnings) {
378
+ lines.push(`- ${warning}`);
379
+ }
380
+ }
381
+ lines.push("", "## Boundary", "");
382
+ lines.push("- Git authentication is external; no credentials are recorded.");
383
+ lines.push("- Remote push still requires an explicit mdkg git push command with remote and branch.");
384
+ return `${lines.join("\n")}\n`;
385
+ }
386
+ function collectCloseoutReceipt(options) {
387
+ requireGitWorkTree(options.root);
388
+ const config = (0, config_1.loadConfig)(options.root);
389
+ const validation = validateForCloseout(options.root);
390
+ const inspect = collectInspectReceipt(options.root);
391
+ const outputDir = closeoutOutputDir(options.root, options.output);
392
+ const outputRel = rel(options.root, outputDir);
393
+ const participated = dbRuntimeParticipated(options.root);
394
+ const warnings = [];
395
+ let dbSnapshotStatus = null;
396
+ let dbSnapshotSeal = null;
397
+ let dbSnapshotDump = null;
398
+ if (participated) {
399
+ dbSnapshotSeal = (0, project_db_snapshot_1.sealProjectDbSnapshot)(options.root, config, options.queuePolicy ?? "drain");
400
+ const dumpPath = toPosix(path_1.default.join(outputRel, "project-db.dump.md"));
401
+ const dumpResult = (0, project_db_snapshot_1.dumpProjectDbSnapshot)(options.root, config, undefined, dumpPath);
402
+ const { dump, ...dumpReceipt } = dumpResult;
403
+ dbSnapshotDump = { ...dumpReceipt, dump_sha256: sha256Buffer(dump) };
404
+ dbSnapshotStatus = (0, project_db_snapshot_1.projectDbSnapshotStatus)(options.root, config);
405
+ if (!dbSnapshotStatus.ok || dbSnapshotStatus.status !== "valid") {
406
+ throw new errors_1.ValidationError(`db snapshot status must be valid after closeout; got ${dbSnapshotStatus.status}`);
407
+ }
408
+ }
409
+ else {
410
+ warnings.push("project DB did not participate in this closeout; no DB snapshot was sealed");
411
+ }
412
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root: options.root, relativePath: outputRel });
413
+ const baseReceipt = {
414
+ action: "git.closeout",
415
+ ok: true,
416
+ root: options.root,
417
+ output_dir: outputRel,
418
+ generated_at: new Date().toISOString(),
419
+ git: inspect,
420
+ validation,
421
+ db_participated: participated,
422
+ db_snapshot_status: dbSnapshotStatus,
423
+ db_snapshot_seal: dbSnapshotSeal,
424
+ db_snapshot_dump: dbSnapshotDump,
425
+ warnings,
426
+ };
427
+ const jsonPath = path_1.default.join(outputDir, "closeout.json");
428
+ const markdownPath = path_1.default.join(outputDir, "closeout.md");
429
+ const receipt = {
430
+ ...baseReceipt,
431
+ static_receipts: {
432
+ json: rel(options.root, jsonPath),
433
+ markdown: rel(options.root, markdownPath),
434
+ },
435
+ };
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));
438
+ return receipt;
439
+ }
440
+ function runGitCloseoutCommand(options) {
441
+ const config = (0, config_1.loadConfig)(options.root);
442
+ const receipt = (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => collectCloseoutReceipt(options));
443
+ if (options.json) {
444
+ printJson(receipt);
445
+ return;
446
+ }
447
+ console.log("git closeout written");
448
+ console.log(`json: ${receipt.static_receipts.json}`);
449
+ console.log(`markdown: ${receipt.static_receipts.markdown}`);
450
+ if (receipt.db_snapshot_seal) {
451
+ console.log(`db snapshot: ${receipt.db_snapshot_seal.snapshot}`);
452
+ }
453
+ }
454
+ function check(name, ok, detail, error = null, warning = null) {
455
+ return {
456
+ name,
457
+ ok,
458
+ level: ok ? (warning ? "warn" : "ok") : "fail",
459
+ detail,
460
+ errors: ok || !error ? [] : [error],
461
+ warnings: warning ? [warning] : [],
462
+ };
463
+ }
464
+ function collectPushReadyReceipt(options) {
465
+ const remote = options.remote;
466
+ const branch = options.branch;
467
+ if (!remote) {
468
+ throw new errors_1.UsageError("mdkg git push-ready requires --remote <name>");
469
+ }
470
+ if (!branch) {
471
+ throw new errors_1.UsageError("mdkg git push-ready requires --branch <name>");
472
+ }
473
+ assertGitOperand("--remote", remote);
474
+ assertGitBranch(options.root, branch);
475
+ assertNoEmbeddedCredentials("--remote", remote);
476
+ requireGitWorkTree(options.root);
477
+ const config = (0, config_1.loadConfig)(options.root);
478
+ const inspect = collectInspectReceipt(options.root);
479
+ const validation = (0, validate_1.collectValidateReceipt)({ root: options.root, json: true });
480
+ const rawUrl = rawRemoteUrl(options.root, remote);
481
+ const redactedRemoteUrl = rawUrl ? redactRemoteRef(rawUrl) : null;
482
+ const checks = [];
483
+ checks.push(check("remote-explicit", true, `remote target is ${remote}`));
484
+ checks.push(check("branch-explicit", true, `branch target is ${branch}`));
485
+ checks.push(check("remote-exists", rawUrl !== null, rawUrl ? `remote ${remote} exists` : `remote ${remote} missing`, `remote ${remote} is not configured`));
486
+ if (rawUrl) {
487
+ checks.push(check("remote-credential-boundary", !hasEmbeddedUrlCredentials(rawUrl), "remote URL does not embed credentials", "remote URL contains embedded credentials; use external Git auth"));
488
+ }
489
+ 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"));
490
+ 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"));
491
+ let snapshotStatus = null;
492
+ if (config.db.enabled) {
493
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(options.root, config.db);
494
+ if (fs_1.default.existsSync(layout.runtimeFile)) {
495
+ snapshotStatus = (0, project_db_snapshot_1.projectDbSnapshotStatus)(options.root, config);
496
+ 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"));
497
+ }
498
+ else {
499
+ checks.push(check("db-snapshot", true, "project DB is enabled but runtime DB is absent", null, "no runtime DB participated in this push"));
500
+ }
501
+ }
502
+ else {
503
+ checks.push(check("db-snapshot", true, "project DB disabled; no DB closeout required"));
504
+ }
505
+ const errors = checks.flatMap((item) => item.errors.map((error) => `${item.name}: ${error}`));
506
+ const warnings = checks.flatMap((item) => item.warnings.map((warning) => `${item.name}: ${warning}`));
507
+ return {
508
+ action: "git.push_ready",
509
+ ok: errors.length === 0,
510
+ root: options.root,
511
+ remote,
512
+ branch,
513
+ remote_url: redactedRemoteUrl,
514
+ git: inspect,
515
+ validation,
516
+ db_snapshot_status: snapshotStatus,
517
+ checks,
518
+ warning_count: warnings.length,
519
+ failure_count: errors.length,
520
+ warnings,
521
+ errors,
522
+ };
523
+ }
524
+ function runGitPushReadyCommand(options) {
525
+ const receipt = collectPushReadyReceipt(options);
526
+ if (options.json) {
527
+ printJson(receipt);
528
+ }
529
+ else {
530
+ for (const item of receipt.checks) {
531
+ console.log(`${item.level}: ${item.name} - ${item.detail}`);
532
+ }
533
+ }
534
+ if (!receipt.ok) {
535
+ throw new errors_1.ValidationError(`git push-ready failed with ${receipt.failure_count} issue(s)`);
536
+ }
537
+ if (!options.json) {
538
+ console.log("git push-ready ok");
539
+ }
540
+ }
541
+ function hasStagedChanges(root) {
542
+ const result = git(root, ["diff", "--cached", "--quiet"], true);
543
+ return result.status !== 0;
544
+ }
545
+ function ensureGitIdentity(root) {
546
+ const email = gitOptional(root, ["config", "user.email"]);
547
+ const name = gitOptional(root, ["config", "user.name"]);
548
+ if (!email || !name) {
549
+ throw new errors_1.ValidationError("git commit requires user.name and user.email to be configured");
550
+ }
551
+ }
552
+ function collectPushReceipt(options) {
553
+ const remote = options.remote;
554
+ const branch = options.branch;
555
+ if (!remote) {
556
+ throw new errors_1.UsageError("mdkg git push requires --remote <name>");
557
+ }
558
+ if (!branch) {
559
+ throw new errors_1.UsageError("mdkg git push requires --branch <name>");
560
+ }
561
+ assertGitOperand("--remote", remote);
562
+ assertGitBranch(options.root, branch);
563
+ assertNoEmbeddedCredentials("--remote", remote);
564
+ requireGitWorkTree(options.root);
565
+ let closeout = null;
566
+ let commitCreated = false;
567
+ let commitMessage = null;
568
+ if (options.stageAll) {
569
+ if (!options.message) {
570
+ throw new errors_1.UsageError("mdkg git push --stage-all requires --message <text>");
571
+ }
572
+ ensureGitIdentity(options.root);
573
+ const config = (0, config_1.loadConfig)(options.root);
574
+ closeout = (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => collectCloseoutReceipt({
575
+ root: options.root,
576
+ queuePolicy: options.queuePolicy,
577
+ json: true,
578
+ }));
579
+ git(options.root, ["add", "-A"]);
580
+ if (hasStagedChanges(options.root)) {
581
+ git(options.root, ["commit", "-m", options.message]);
582
+ commitCreated = true;
583
+ commitMessage = options.message;
584
+ }
585
+ }
586
+ const pushReady = collectPushReadyReceipt({ root: options.root, remote, branch, json: true });
587
+ if (!pushReady.ok) {
588
+ throw new errors_1.ValidationError(`git push-ready failed with ${pushReady.failure_count} issue(s)`);
589
+ }
590
+ const push = git(options.root, ["push", "--", remote, `HEAD:refs/heads/${branch}`]);
591
+ const inspect = collectInspectReceipt(options.root);
592
+ return {
593
+ action: "git.push",
594
+ ok: true,
595
+ remote,
596
+ branch,
597
+ head_sha: inspect.head_sha,
598
+ pushed_ref: `refs/heads/${branch}`,
599
+ stage_all: Boolean(options.stageAll),
600
+ closeout,
601
+ commit: {
602
+ created: commitCreated,
603
+ message: commitMessage,
604
+ sha: commitCreated ? inspect.head_sha : null,
605
+ },
606
+ push_ready: pushReady,
607
+ push_output: [...push.stdout.split(/\r?\n/), ...push.stderr.split(/\r?\n/)].filter(Boolean),
608
+ warnings: [],
609
+ };
610
+ }
611
+ function runGitPushCommand(options) {
612
+ const receipt = collectPushReceipt(options);
613
+ if (options.json) {
614
+ printJson(receipt);
615
+ return;
616
+ }
617
+ console.log("git push complete");
618
+ console.log(`remote: ${receipt.remote}`);
619
+ console.log(`branch: ${receipt.branch}`);
620
+ console.log(`head: ${receipt.head_sha ?? "(none)"}`);
621
+ if (receipt.commit.created) {
622
+ console.log(`commit: ${receipt.commit.sha}`);
623
+ }
624
+ }