mdkg 0.3.5 → 0.3.7

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 (49) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/CLI_COMMAND_MATRIX.md +90 -10
  3. package/README.md +54 -7
  4. package/dist/cli.js +222 -13
  5. package/dist/command-contract.json +587 -19
  6. package/dist/commands/bundle.js +2 -0
  7. package/dist/commands/checkpoint.js +139 -1
  8. package/dist/commands/db.js +8 -0
  9. package/dist/commands/format.js +116 -0
  10. package/dist/commands/goal.js +60 -0
  11. package/dist/commands/graph.js +280 -10
  12. package/dist/commands/handoff.js +295 -0
  13. package/dist/commands/mcp.js +656 -0
  14. package/dist/commands/pack.js +3 -1
  15. package/dist/commands/query_output.js +2 -0
  16. package/dist/commands/show.js +8 -0
  17. package/dist/commands/status.js +1 -0
  18. package/dist/commands/task.js +2 -0
  19. package/dist/commands/upgrade.js +61 -0
  20. package/dist/commands/validate.js +162 -3
  21. package/dist/commands/work.js +164 -0
  22. package/dist/core/project_db_queue_contract.js +101 -0
  23. package/dist/graph/edges.js +15 -0
  24. package/dist/graph/frontmatter.js +4 -1
  25. package/dist/graph/indexer.js +8 -0
  26. package/dist/graph/node.js +12 -1
  27. package/dist/graph/sqlite_index.js +2 -0
  28. package/dist/graph/subgraphs.js +2 -0
  29. package/dist/graph/validate_graph.js +5 -0
  30. package/dist/graph/visibility.js +6 -0
  31. package/dist/init/AGENT_START.md +4 -1
  32. package/dist/init/CLI_COMMAND_MATRIX.md +36 -4
  33. package/dist/init/README.md +26 -3
  34. package/dist/init/init-manifest.json +12 -12
  35. package/dist/init/templates/default/bug.md +2 -0
  36. package/dist/init/templates/default/chk.md +3 -0
  37. package/dist/init/templates/default/epic.md +2 -0
  38. package/dist/init/templates/default/feat.md +2 -0
  39. package/dist/init/templates/default/goal.md +3 -0
  40. package/dist/init/templates/default/spike.md +2 -0
  41. package/dist/init/templates/default/task.md +2 -0
  42. package/dist/init/templates/default/test.md +2 -0
  43. package/dist/pack/export_json.js +20 -8
  44. package/dist/pack/export_md.js +15 -4
  45. package/dist/pack/export_xml.js +9 -4
  46. package/dist/pack/metrics.js +12 -4
  47. package/dist/pack/pack.js +9 -1
  48. package/dist/util/argparse.js +1 -0
  49. package/package.json +7 -2
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runGraphCloneCommand = runGraphCloneCommand;
7
7
  exports.runGraphForkCommand = runGraphForkCommand;
8
8
  exports.runGraphImportTemplateCommand = runGraphImportTemplateCommand;
9
+ exports.runGraphRefsCommand = runGraphRefsCommand;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const bundle_1 = require("./bundle");
@@ -14,15 +15,124 @@ const validate_1 = require("./validate");
14
15
  const config_1 = require("../core/config");
15
16
  const workspace_path_1 = require("../core/workspace_path");
16
17
  const indexer_1 = require("../graph/indexer");
18
+ const index_cache_1 = require("../graph/index_cache");
17
19
  const frontmatter_1 = require("../graph/frontmatter");
18
20
  const errors_1 = require("../util/errors");
19
21
  const qid_1 = require("../util/qid");
20
22
  const atomic_1 = require("../util/atomic");
21
23
  const zip_1 = require("../util/zip");
22
24
  const lock_1 = require("../util/lock");
25
+ const date_1 = require("../util/date");
26
+ const refs_1 = require("../util/refs");
23
27
  function writeJson(value) {
24
28
  console.log(JSON.stringify(value, null, 2));
25
29
  }
30
+ function toStringList(value) {
31
+ if (!Array.isArray(value)) {
32
+ return [];
33
+ }
34
+ return value.filter((item) => typeof item === "string");
35
+ }
36
+ function summarizeNode(node) {
37
+ return {
38
+ qid: node.qid,
39
+ id: node.id,
40
+ workspace: node.ws,
41
+ type: node.type,
42
+ title: node.title,
43
+ status: node.status,
44
+ path: node.path,
45
+ read_only: Boolean(node.source?.read_only ?? node.source?.imported),
46
+ source: node.source,
47
+ };
48
+ }
49
+ function summarizeRef(index, value, ws) {
50
+ if ((0, refs_1.isUriRef)(value)) {
51
+ return {
52
+ ref: value,
53
+ kind: "uri",
54
+ exists: true,
55
+ };
56
+ }
57
+ const resolved = (0, qid_1.resolveQid)(index, value, ws);
58
+ if (resolved.status !== "ok") {
59
+ return {
60
+ ref: value,
61
+ kind: "missing",
62
+ exists: false,
63
+ };
64
+ }
65
+ const node = index.nodes[resolved.qid];
66
+ if (!node) {
67
+ return {
68
+ ref: value,
69
+ kind: "missing",
70
+ exists: false,
71
+ qid: resolved.qid,
72
+ };
73
+ }
74
+ return {
75
+ ref: value,
76
+ kind: "node",
77
+ exists: true,
78
+ qid: node.qid,
79
+ node: summarizeNode(node),
80
+ };
81
+ }
82
+ function summarizeRefs(index, values, ws) {
83
+ return [...new Set(values)].sort().map((value) => summarizeRef(index, value, ws));
84
+ }
85
+ function compactOutgoing(node) {
86
+ return {
87
+ scope_refs: toStringList(node.attributes.scope_refs),
88
+ epic: node.edges.epic ? [node.edges.epic] : [],
89
+ parent: node.edges.parent ? [node.edges.parent] : [],
90
+ prev: node.edges.prev ? [node.edges.prev] : [],
91
+ next: node.edges.next ? [node.edges.next] : [],
92
+ relates: node.edges.relates,
93
+ blocked_by: node.edges.blocked_by,
94
+ blocks: node.edges.blocks,
95
+ context_refs: node.edges.context_refs ?? [],
96
+ evidence_refs: node.edges.evidence_refs ?? [],
97
+ };
98
+ }
99
+ function incomingScopeRefs(index, targetQid) {
100
+ const sources = [];
101
+ for (const node of Object.values(index.nodes)) {
102
+ for (const ref of toStringList(node.attributes.scope_refs)) {
103
+ const resolved = (0, qid_1.resolveQid)(index, ref, node.ws);
104
+ if (resolved.status === "ok" && resolved.qid === targetQid) {
105
+ sources.push(node.qid);
106
+ }
107
+ }
108
+ }
109
+ return sources.sort();
110
+ }
111
+ function buildGraphRefsReceipt(index, node, warnings) {
112
+ const outgoingRaw = compactOutgoing(node);
113
+ const incomingRaw = {
114
+ scope_refs: incomingScopeRefs(index, node.qid),
115
+ epic: index.reverse_edges.epic?.[node.qid] ?? [],
116
+ parent: index.reverse_edges.parent?.[node.qid] ?? [],
117
+ prev: index.reverse_edges.prev?.[node.qid] ?? [],
118
+ next: index.reverse_edges.next?.[node.qid] ?? [],
119
+ relates: index.reverse_edges.relates?.[node.qid] ?? [],
120
+ blocked_by: index.reverse_edges.blocked_by?.[node.qid] ?? [],
121
+ blocks: index.reverse_edges.blocks?.[node.qid] ?? [],
122
+ context_refs: index.reverse_edges.context_refs?.[node.qid] ?? [],
123
+ evidence_refs: index.reverse_edges.evidence_refs?.[node.qid] ?? [],
124
+ };
125
+ const outgoing = Object.fromEntries(Object.entries(outgoingRaw).map(([key, values]) => [key, summarizeRefs(index, values, node.ws)]));
126
+ const incoming = Object.fromEntries(Object.entries(incomingRaw).map(([key, values]) => [key, summarizeRefs(index, values, node.ws)]));
127
+ return {
128
+ action: "graph.refs",
129
+ ok: true,
130
+ target: summarizeNode(node),
131
+ outgoing,
132
+ incoming,
133
+ warnings,
134
+ };
135
+ }
26
136
  function toPosixPath(value) {
27
137
  return value.split(path_1.default.sep).join("/");
28
138
  }
@@ -176,6 +286,85 @@ function writeSelectedGoal(targetRoot, qid, id, ws) {
176
286
  (0, atomic_1.atomicWriteFile)(statePath, `${JSON.stringify(state, null, 2)}\n`);
177
287
  return rel(targetRoot, statePath);
178
288
  }
289
+ function qidForRoot(id) {
290
+ return `root:${id}`;
291
+ }
292
+ function idFromRootQid(qid) {
293
+ const [workspace, id] = qid.split(":");
294
+ if (workspace !== "root" || !id) {
295
+ throw new errors_1.UsageError(`invalid root qid: ${qid}`);
296
+ }
297
+ return id;
298
+ }
299
+ function ensureStatusAllowed(config, status) {
300
+ const normalized = status.toLowerCase();
301
+ const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
302
+ if (!allowed.has(normalized)) {
303
+ throw new errors_1.UsageError(`goal status ${normalized} is not allowed by work.status_enum`);
304
+ }
305
+ return normalized;
306
+ }
307
+ function isActiveGoalStatus(status, goalState) {
308
+ return status === "progress" && goalState === "active";
309
+ }
310
+ function isClosedGoalStatus(status, goalState) {
311
+ return status === "done" || status === "archived" || goalState === "achieved" || goalState === "archived";
312
+ }
313
+ function activeLocalRootGoals(root) {
314
+ const config = (0, config_1.loadConfig)(root);
315
+ const index = (0, indexer_1.buildIndex)(root, config);
316
+ return Object.values(index.nodes)
317
+ .filter((node) => !node.source?.imported)
318
+ .filter((node) => node.ws === "root" && node.type === "goal")
319
+ .filter((node) => isActiveGoalStatus(node.status, String(node.attributes.goal_state ?? "")))
320
+ .sort((a, b) => a.qid.localeCompare(b.qid));
321
+ }
322
+ function localGoalLifecycleReceipt(node, status, goalState, planned) {
323
+ return {
324
+ workspace: node.ws,
325
+ id: node.id,
326
+ qid: node.qid,
327
+ path: node.path,
328
+ previous_status: node.status ?? "",
329
+ previous_goal_state: String(node.attributes.goal_state ?? ""),
330
+ status,
331
+ goal_state: goalState,
332
+ source: "local",
333
+ planned,
334
+ };
335
+ }
336
+ function importedGoalLifecycleReceipt(plan, status, goalState, planned) {
337
+ return {
338
+ workspace: "root",
339
+ id: plan.to_id,
340
+ qid: qidForRoot(plan.to_id),
341
+ path: plan.target_path,
342
+ previous_status: plan.status ?? "",
343
+ previous_goal_state: plan.goal_state ?? "",
344
+ status,
345
+ goal_state: goalState,
346
+ source: "imported",
347
+ planned,
348
+ };
349
+ }
350
+ function readNodeFile(root, nodePath) {
351
+ const filePath = path_1.default.join(root, nodePath);
352
+ const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(filePath, "utf8"), nodePath);
353
+ return { filePath, frontmatter: { ...parsed.frontmatter }, body: parsed.body };
354
+ }
355
+ function writeRenderedNodeFile(filePath, frontmatter, body) {
356
+ (0, atomic_1.atomicWriteFile)(filePath, renderNode(frontmatter, body));
357
+ }
358
+ function pauseLocalGoals(root, goals, config) {
359
+ const today = (0, date_1.formatDate)(new Date());
360
+ for (const goal of goals.filter((item) => item.source === "local")) {
361
+ const loaded = readNodeFile(root, goal.path);
362
+ loaded.frontmatter.status = ensureStatusAllowed(config, "blocked");
363
+ loaded.frontmatter.goal_state = "paused";
364
+ loaded.frontmatter.updated = today;
365
+ writeRenderedNodeFile(loaded.filePath, loaded.frontmatter, loaded.body);
366
+ }
367
+ }
179
368
  function isWorkMarkdownPath(value) {
180
369
  const normalized = value.replace(/\\/g, "/");
181
370
  return normalized.startsWith(".mdkg/work/") && normalized.endsWith(".md");
@@ -340,14 +529,41 @@ function planImportTemplate(options) {
340
529
  from_id: node.id,
341
530
  to_id: toId,
342
531
  type: node.type,
532
+ status: typeof frontmatter.status === "string" ? frontmatter.status : undefined,
533
+ goal_state: typeof frontmatter.goal_state === "string" ? frontmatter.goal_state : undefined,
343
534
  title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
344
535
  content: renderNode(frontmatter, body),
345
536
  };
346
537
  });
347
538
  const startGoalToId = options.startGoal ? (idMap.get(options.startGoal) ?? options.startGoal) : undefined;
348
- if (options.startGoal && !plans.some((plan) => plan.to_id === startGoalToId && plan.type === "goal")) {
539
+ const startGoalPlan = startGoalToId
540
+ ? plans.find((plan) => plan.to_id === startGoalToId && plan.type === "goal")
541
+ : undefined;
542
+ if (options.startGoal && !startGoalPlan) {
349
543
  throw new errors_1.NotFoundError(`start goal not found in imported template graph: ${options.startGoal}`);
350
544
  }
545
+ if (options.selectGoal && startGoalPlan && isClosedGoalStatus(startGoalPlan.status, startGoalPlan.goal_state)) {
546
+ throw new errors_1.UsageError(`cannot select achieved or archived imported start goal: ${options.startGoal}`);
547
+ }
548
+ const localActiveGoals = activeLocalRootGoals(options.root);
549
+ const importedActiveGoals = plans
550
+ .filter((plan) => plan.type === "goal")
551
+ .filter((plan) => isActiveGoalStatus(plan.status, plan.goal_state));
552
+ if (!options.selectGoal && localActiveGoals.length + importedActiveGoals.length > 1) {
553
+ throw new errors_1.UsageError("import-template would create multiple active root goals; use --select-goal --start-goal <goal-id> or pause active goals before importing");
554
+ }
555
+ const activatedGoal = options.selectGoal && startGoalPlan
556
+ ? importedGoalLifecycleReceipt(startGoalPlan, "progress", "active", !options.apply)
557
+ : undefined;
558
+ const pausedGoals = options.selectGoal && startGoalPlan
559
+ ? [
560
+ ...localActiveGoals.map((node) => localGoalLifecycleReceipt(node, "blocked", "paused", !options.apply)),
561
+ ...importedActiveGoals
562
+ .filter((plan) => plan.to_id !== startGoalPlan.to_id)
563
+ .map((plan) => importedGoalLifecycleReceipt(plan, "blocked", "paused", !options.apply)),
564
+ ]
565
+ : [];
566
+ const warnings = pausedGoals.length > 0 ? [`paused ${pausedGoals.length} competing active goal(s)`] : [];
351
567
  const mode = options.apply ? "import_template_applied" : "import_template_dry_run";
352
568
  return {
353
569
  action: "graph.import_template",
@@ -383,7 +599,9 @@ function planImportTemplate(options) {
383
599
  ...(options.selectGoal && startGoalToId
384
600
  ? { selected_goal: { qid: `root:${startGoalToId}`, path: ".mdkg/state/selected-goal.json", planned: !options.apply } }
385
601
  : {}),
386
- warnings: [],
602
+ ...(activatedGoal ? { activated_goal: activatedGoal } : {}),
603
+ paused_goals: pausedGoals,
604
+ warnings,
387
605
  };
388
606
  }
389
607
  function applyImportTemplate(options, receipt) {
@@ -439,6 +657,17 @@ function applyImportTemplate(options, receipt) {
439
657
  frontmatter[field] = rewriteFrontmatterValue(value, idMap, node.sourcePath, field, ignoredRewrites);
440
658
  }
441
659
  const body = rewriteStringValue(node.parsed.body, idMap, node.sourcePath, "body", ignoredRewrites);
660
+ if (frontmatter.type === "goal" && options.selectGoal) {
661
+ if (qidForRoot(toId) === applyPlan.activated_goal?.qid) {
662
+ frontmatter.status = ensureStatusAllowed(config, "progress");
663
+ frontmatter.goal_state = "active";
664
+ }
665
+ else if (isActiveGoalStatus(String(frontmatter.status ?? ""), String(frontmatter.goal_state ?? ""))) {
666
+ frontmatter.status = ensureStatusAllowed(config, "blocked");
667
+ frontmatter.goal_state = "paused";
668
+ }
669
+ frontmatter.updated = (0, date_1.formatDate)(new Date());
670
+ }
442
671
  const targetPath = targetPathForImport(node.sourcePath, node.id, toId, usedPaths);
443
672
  contentByTarget.set(targetPath, renderNode(frontmatter, body));
444
673
  }
@@ -451,22 +680,24 @@ function applyImportTemplate(options, receipt) {
451
680
  fs_1.default.mkdirSync(path_1.default.dirname(targetAbs), { recursive: true });
452
681
  (0, atomic_1.atomicWriteFile)(targetAbs, content);
453
682
  }
683
+ pauseLocalGoals(options.root, applyPlan.paused_goals, config);
454
684
  const indexReceipt = (0, index_1.rebuildDerivedIndexCaches)({ root: options.root });
685
+ const validation = (0, validate_1.collectValidateReceipt)({ root: options.root, quiet: true });
686
+ if (validation.error_count > 0) {
687
+ throw new errors_1.ValidationError(`imported graph validation failed with ${validation.error_count} error(s)`);
688
+ }
455
689
  if (options.selectGoal && options.startGoal) {
456
690
  const selected = applyPlan.selected_goal?.qid;
457
691
  if (!selected) {
458
692
  throw new errors_1.UsageError("--select-goal could not resolve imported start goal");
459
693
  }
460
- const [, id] = selected.split(":");
461
- if (!id) {
462
- throw new errors_1.UsageError(`invalid selected goal qid: ${selected}`);
463
- }
694
+ const id = idFromRootQid(selected);
464
695
  writeSelectedGoal(options.root, selected, id, "root");
465
696
  applyPlan.selected_goal = { qid: selected, path: ".mdkg/state/selected-goal.json", planned: false };
466
- }
467
- const validation = (0, validate_1.collectValidateReceipt)({ root: options.root, quiet: true });
468
- if (validation.error_count > 0) {
469
- throw new errors_1.ValidationError(`imported graph validation failed with ${validation.error_count} error(s)`);
697
+ if (applyPlan.activated_goal) {
698
+ applyPlan.activated_goal.planned = false;
699
+ }
700
+ applyPlan.paused_goals = applyPlan.paused_goals.map((goal) => ({ ...goal, planned: false }));
470
701
  }
471
702
  return {
472
703
  ...applyPlan,
@@ -580,3 +811,42 @@ function runGraphImportTemplateCommand(options) {
580
811
  console.log(`selected_goal: ${receipt.selected_goal.qid}${receipt.selected_goal.planned ? " (planned)" : ""}`);
581
812
  }
582
813
  }
814
+ function runGraphRefsCommand(options) {
815
+ const config = (0, config_1.loadConfig)(options.root);
816
+ const { index, warnings } = (0, index_cache_1.loadIndex)({ root: options.root, config });
817
+ const resolved = (0, qid_1.resolveQid)(index, options.id, options.ws);
818
+ if (resolved.status !== "ok") {
819
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("node", options.id, resolved, options.ws));
820
+ }
821
+ const node = index.nodes[resolved.qid];
822
+ if (!node) {
823
+ throw new errors_1.NotFoundError(`node not found: ${options.id}`);
824
+ }
825
+ const receipt = buildGraphRefsReceipt(index, node, warnings);
826
+ if (options.json) {
827
+ writeJson(receipt);
828
+ return;
829
+ }
830
+ console.log(`graph refs: ${node.qid}`);
831
+ if (node.source?.imported) {
832
+ console.log(`source: subgraph:${node.source.subgraph_alias} read-only`);
833
+ }
834
+ for (const [direction, lanes] of Object.entries({ outgoing: receipt.outgoing, incoming: receipt.incoming })) {
835
+ console.log(`${direction}:`);
836
+ for (const [lane, refs] of Object.entries(lanes)) {
837
+ if (refs.length === 0) {
838
+ continue;
839
+ }
840
+ console.log(` ${lane}:`);
841
+ for (const ref of refs) {
842
+ const target = ref.node
843
+ ? `${ref.node.qid}${ref.node.read_only ? " (read-only)" : ""}`
844
+ : ref.ref;
845
+ console.log(` - ${target}`);
846
+ }
847
+ }
848
+ }
849
+ for (const warning of warnings) {
850
+ console.error(`warning: ${warning}`);
851
+ }
852
+ }
@@ -0,0 +1,295 @@
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.runHandoffCreateCommand = runHandoffCreateCommand;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("../core/config");
10
+ const index_cache_1 = require("../graph/index_cache");
11
+ const pack_1 = require("../pack/pack");
12
+ const metrics_1 = require("../pack/metrics");
13
+ const atomic_1 = require("../util/atomic");
14
+ const errors_1 = require("../util/errors");
15
+ const qid_1 = require("../util/qid");
16
+ const version_1 = require("../core/version");
17
+ const RAW_CONTENT_MARKERS = [
18
+ { id: "raw_prompt", pattern: /\bRAW_PROMPT_MARKER\b/i, description: "raw prompt marker" },
19
+ { id: "raw_payload", pattern: /\bRAW_PAYLOAD_MARKER\b/i, description: "raw payload marker" },
20
+ { id: "raw_secret", pattern: /\bRAW_SECRET_MARKER\b|BEGIN [A-Z ]*PRIVATE KEY|secret\s*=/i, description: "raw secret marker" },
21
+ ];
22
+ function normalizeWorkspace(value) {
23
+ if (!value || value === "all") {
24
+ return undefined;
25
+ }
26
+ return value;
27
+ }
28
+ function outputPath(root, out) {
29
+ if (!out) {
30
+ return undefined;
31
+ }
32
+ const resolved = path_1.default.resolve(root, out);
33
+ const rootWithSep = root.endsWith(path_1.default.sep) ? root : `${root}${path_1.default.sep}`;
34
+ if (resolved !== root && !resolved.startsWith(rootWithSep)) {
35
+ throw new errors_1.UsageError("--out must stay within the repo root");
36
+ }
37
+ return resolved;
38
+ }
39
+ function sha256(value) {
40
+ return `sha256:${crypto_1.default.createHash("sha256").update(value).digest("hex")}`;
41
+ }
42
+ function listAttribute(node, key) {
43
+ const value = "attributes" in node ? node.attributes[key] : undefined;
44
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
45
+ }
46
+ function stringAttribute(node, key) {
47
+ const value = "attributes" in node ? node.attributes[key] : undefined;
48
+ return typeof value === "string" ? value : undefined;
49
+ }
50
+ function collectRawMarkerWarnings(pack) {
51
+ const warnings = [];
52
+ for (const node of pack.nodes) {
53
+ for (const marker of RAW_CONTENT_MARKERS) {
54
+ if (marker.pattern.test(node.body)) {
55
+ warnings.push({
56
+ qid: node.qid,
57
+ path: node.path,
58
+ marker_id: marker.id,
59
+ message: `${marker.description} detected; handoff omitted raw body content for this node`,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return warnings;
65
+ }
66
+ function nodeLine(node) {
67
+ const parts = [`${node.qid}`, node.type, node.title];
68
+ const state = [node.status, node.priority !== undefined ? `p${node.priority}` : undefined].filter(Boolean).join("/");
69
+ if (state) {
70
+ parts.push(state);
71
+ }
72
+ parts.push(node.path);
73
+ return `- ${parts.join(" | ")}`;
74
+ }
75
+ function renderList(values, empty = "- none") {
76
+ if (values.length === 0) {
77
+ return [empty];
78
+ }
79
+ return values.map((value) => `- ${value}`);
80
+ }
81
+ function renderNodeRefs(node) {
82
+ const lines = [];
83
+ if (node.refs.length > 0) {
84
+ lines.push(` refs: ${node.refs.join(", ")}`);
85
+ }
86
+ if (node.context_refs.length > 0) {
87
+ lines.push(` context_refs: ${node.context_refs.join(", ")}`);
88
+ }
89
+ if (node.evidence_refs.length > 0) {
90
+ lines.push(` evidence_refs: ${node.evidence_refs.join(", ")}`);
91
+ }
92
+ return lines;
93
+ }
94
+ function renderHandoff(params) {
95
+ const { rootNode, indexNode, pack, rawWarnings } = params;
96
+ const stats = (0, metrics_1.measurePack)(pack);
97
+ const lines = [];
98
+ const requiredChecks = listAttribute(indexNode, "required_checks");
99
+ const requiredSkills = listAttribute(indexNode, "required_skills");
100
+ const goalCondition = stringAttribute(indexNode, "goal_condition");
101
+ const activeNode = stringAttribute(indexNode, "active_node");
102
+ const lastActiveNode = stringAttribute(indexNode, "last_active_node");
103
+ const latestCheckpoint = pack.meta.latest_checkpoint_qid
104
+ ? pack.nodes.find((node) => node.qid === pack.meta.latest_checkpoint_qid)
105
+ : undefined;
106
+ lines.push("# mdkg Agent Handoff");
107
+ lines.push("");
108
+ lines.push("Use this handoff as a sanitized graph summary. Inspect source files before mutating durable state.");
109
+ lines.push("");
110
+ lines.push("## Target");
111
+ lines.push(`- qid: ${rootNode.qid}`);
112
+ lines.push(`- type: ${rootNode.type}`);
113
+ lines.push(`- title: ${rootNode.title}`);
114
+ if (rootNode.status) {
115
+ lines.push(`- status: ${rootNode.status}`);
116
+ }
117
+ if (rootNode.priority !== undefined) {
118
+ lines.push(`- priority: ${rootNode.priority}`);
119
+ }
120
+ lines.push(`- path: ${rootNode.path}`);
121
+ if (goalCondition) {
122
+ lines.push(`- goal_condition: ${goalCondition}`);
123
+ }
124
+ if (activeNode) {
125
+ lines.push(`- active_node: ${activeNode}`);
126
+ }
127
+ if (lastActiveNode) {
128
+ lines.push(`- last_active_node: ${lastActiveNode}`);
129
+ }
130
+ lines.push("");
131
+ lines.push("## Boundaries");
132
+ lines.push("- mdkg is durable semantic memory and graph state, not raw execution trace storage.");
133
+ lines.push("- Do not include raw secrets, credentials, model prompts, provider payloads, cookies, tokens, or bulky runtime artifacts in mdkg nodes or handoffs.");
134
+ lines.push("- Use refs, hashes, redacted summaries, archive links, artifact links, and checkpoints for evidence.");
135
+ lines.push("- Treat subgraph nodes as read-only planning context unless you are operating in the owning repo.");
136
+ lines.push("- Run validation before closing work or handing execution to another agent.");
137
+ lines.push("");
138
+ lines.push("## Recommended Next Steps");
139
+ if (rootNode.type === "goal") {
140
+ lines.push(`- Run \`mdkg goal current --json\` and \`mdkg goal next ${rootNode.id} --json\` to confirm routing.`);
141
+ lines.push("- Claim one actionable local node before implementation with `mdkg goal claim <goal-id> <work-id> --json`.");
142
+ }
143
+ else {
144
+ lines.push(`- Run \`mdkg show ${rootNode.id} --json\` and \`mdkg pack ${rootNode.id}\` to refresh local context.`);
145
+ lines.push("- Use `mdkg task start|update|done` for lifecycle updates when the node is task-like.");
146
+ }
147
+ lines.push("- Keep detailed implementation notes in Markdown body sections, not CLI flags.");
148
+ lines.push("");
149
+ lines.push("## Required Checks");
150
+ lines.push(...renderList(requiredChecks));
151
+ lines.push("");
152
+ lines.push("## Required Skills");
153
+ lines.push(...renderList(requiredSkills));
154
+ lines.push("");
155
+ lines.push("## Latest Checkpoint");
156
+ if (latestCheckpoint) {
157
+ lines.push(nodeLine(latestCheckpoint));
158
+ }
159
+ else {
160
+ lines.push("- none");
161
+ }
162
+ lines.push("");
163
+ lines.push("## Included Graph Context");
164
+ for (const node of pack.nodes) {
165
+ lines.push(nodeLine(node));
166
+ lines.push(...renderNodeRefs(node));
167
+ }
168
+ lines.push("");
169
+ lines.push("## Raw Content Warnings");
170
+ if (rawWarnings.length === 0) {
171
+ lines.push("- none");
172
+ }
173
+ else {
174
+ for (const warning of rawWarnings) {
175
+ lines.push(`- ${warning.qid} (${warning.path}): ${warning.marker_id} - ${warning.message}`);
176
+ }
177
+ }
178
+ lines.push("");
179
+ lines.push("## Pack Summary");
180
+ lines.push(`- generated_at: ${pack.meta.generated_at}`);
181
+ lines.push(`- node_count: ${pack.nodes.length}`);
182
+ lines.push(`- tokens_estimate: ${stats.totals.tokens_estimate}`);
183
+ lines.push(`- truncated: max_nodes=${pack.meta.truncated.max_nodes} max_bytes=${pack.meta.truncated.max_bytes}`);
184
+ if (pack.meta.truncated.dropped.length > 0) {
185
+ lines.push(`- dropped: ${pack.meta.truncated.dropped.join(", ")}`);
186
+ }
187
+ lines.push("");
188
+ lines.push("## Handoff Prompt");
189
+ lines.push("Continue from the target above. Preserve the boundaries, verify current repo state, use mdkg commands for structured lifecycle changes, and record validation evidence before closeout.");
190
+ lines.push("");
191
+ return lines.join("\n");
192
+ }
193
+ function runHandoffCreateCommand(options) {
194
+ if (!options.id) {
195
+ throw new errors_1.UsageError("mdkg handoff create requires <id-or-qid>");
196
+ }
197
+ const config = (0, config_1.loadConfig)(options.root);
198
+ const ws = normalizeWorkspace(options.ws);
199
+ if (ws && !config.workspaces[ws] && !config.subgraphs[ws]) {
200
+ throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
201
+ }
202
+ const { index, stale, rebuilt, warnings } = (0, index_cache_1.loadIndex)({
203
+ root: options.root,
204
+ config,
205
+ useCache: true,
206
+ allowReindex: true,
207
+ });
208
+ if (stale && !rebuilt) {
209
+ console.error("warning: index is stale; run mdkg index to refresh");
210
+ }
211
+ for (const warning of warnings) {
212
+ console.error(`warning: ${warning}`);
213
+ }
214
+ const resolved = (0, qid_1.resolveQid)(index, options.id, ws);
215
+ if (resolved.status !== "ok") {
216
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("id", options.id, resolved, ws));
217
+ }
218
+ const indexNode = index.nodes[resolved.qid];
219
+ if (!indexNode) {
220
+ throw new errors_1.NotFoundError(`node not found: ${resolved.qid}`);
221
+ }
222
+ const depth = options.depth ?? config.pack.default_depth;
223
+ if (!Number.isInteger(depth) || depth < 0) {
224
+ throw new errors_1.UsageError("--depth must be a non-negative integer");
225
+ }
226
+ const buildResult = (0, pack_1.buildPack)({
227
+ root: options.root,
228
+ index,
229
+ rootQid: resolved.qid,
230
+ depth,
231
+ edges: ["parent", "epic", "relates", "blocked_by", "blocks", "context_refs", "evidence_refs"],
232
+ verbose: false,
233
+ maxNodes: config.pack.limits.max_nodes,
234
+ verboseCoreListPath: path_1.default.resolve(options.root, config.pack.verbose_core_list_path),
235
+ wsHint: ws,
236
+ includeLatestCheckpoint: true,
237
+ });
238
+ for (const warning of buildResult.warnings) {
239
+ console.error(`warning: ${warning}`);
240
+ }
241
+ const rootNode = buildResult.pack.nodes[0];
242
+ if (!rootNode) {
243
+ throw new errors_1.NotFoundError(`node not found: ${resolved.qid}`);
244
+ }
245
+ const rawWarnings = collectRawMarkerWarnings(buildResult.pack);
246
+ const content = renderHandoff({
247
+ rootNode,
248
+ indexNode,
249
+ pack: buildResult.pack,
250
+ rawWarnings,
251
+ });
252
+ const contentHash = sha256(content);
253
+ const outPath = outputPath(options.root, options.out);
254
+ if (outPath) {
255
+ (0, atomic_1.atomicWriteFile)(outPath, content);
256
+ }
257
+ const receipt = {
258
+ action: "handoff-created",
259
+ ok: true,
260
+ mdkg_version: (0, version_1.readPackageVersion)(),
261
+ target: {
262
+ qid: rootNode.qid,
263
+ id: rootNode.id,
264
+ type: rootNode.type,
265
+ title: rootNode.title,
266
+ path: rootNode.path,
267
+ status: rootNode.status,
268
+ },
269
+ output_path: outPath ? path_1.default.relative(options.root, outPath).split(path_1.default.sep).join("/") : null,
270
+ content_sha256: contentHash,
271
+ raw_marker_warning_count: rawWarnings.length,
272
+ raw_marker_warnings: rawWarnings,
273
+ latest_checkpoint_qid: buildResult.pack.meta.latest_checkpoint_qid ?? null,
274
+ included_qids: buildResult.pack.nodes.map((node) => node.qid),
275
+ pack: {
276
+ node_count: buildResult.pack.nodes.length,
277
+ generated_at: buildResult.pack.meta.generated_at,
278
+ truncated: buildResult.pack.meta.truncated,
279
+ },
280
+ content,
281
+ };
282
+ if (options.json) {
283
+ console.log(JSON.stringify(receipt, null, 2));
284
+ return;
285
+ }
286
+ if (outPath) {
287
+ console.log(`handoff written: ${outPath}`);
288
+ console.log(`sha256: ${contentHash}`);
289
+ if (rawWarnings.length > 0) {
290
+ console.log(`raw marker warnings: ${rawWarnings.length}`);
291
+ }
292
+ return;
293
+ }
294
+ console.log(content);
295
+ }