mdkg 0.0.7 → 0.0.9

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 (45) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/CONTRIBUTING.md +124 -0
  3. package/README.md +33 -10
  4. package/dist/cli.js +80 -32
  5. package/dist/commands/checkpoint.js +19 -2
  6. package/dist/commands/event.js +12 -0
  7. package/dist/commands/new.js +50 -4
  8. package/dist/commands/pack.js +14 -0
  9. package/dist/commands/query_output.js +2 -0
  10. package/dist/commands/search.js +8 -0
  11. package/dist/commands/show.js +7 -0
  12. package/dist/commands/skill.js +80 -12
  13. package/dist/commands/task.js +41 -4
  14. package/dist/commands/validate.js +31 -3
  15. package/dist/commands/workspace.js +105 -13
  16. package/dist/core/config.js +217 -22
  17. package/dist/core/migrate.js +39 -5
  18. package/dist/core/workspace_path.js +41 -0
  19. package/dist/graph/agent_file_types.js +392 -0
  20. package/dist/graph/edges.js +13 -10
  21. package/dist/graph/frontmatter.js +33 -0
  22. package/dist/graph/indexer.js +1 -0
  23. package/dist/graph/node.js +21 -5
  24. package/dist/graph/skills_indexer.js +14 -1
  25. package/dist/graph/validate_graph.js +302 -2
  26. package/dist/init/AGENT_START.md +13 -0
  27. package/dist/init/CLI_COMMAND_MATRIX.md +43 -1
  28. package/dist/init/README.md +7 -0
  29. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +1 -1
  30. package/dist/init/templates/default/dispute.md +31 -0
  31. package/dist/init/templates/default/feedback.md +27 -0
  32. package/dist/init/templates/default/proposal.md +35 -0
  33. package/dist/init/templates/default/receipt.md +31 -0
  34. package/dist/init/templates/default/spec.md +43 -0
  35. package/dist/init/templates/default/work.md +44 -0
  36. package/dist/init/templates/default/work_order.md +32 -0
  37. package/dist/pack/export_json.js +3 -0
  38. package/dist/pack/export_md.js +9 -0
  39. package/dist/pack/export_xml.js +9 -0
  40. package/dist/pack/order.js +7 -0
  41. package/dist/pack/pack.js +1 -0
  42. package/dist/util/argparse.js +1 -0
  43. package/dist/util/id.js +19 -0
  44. package/package.json +9 -2
  45. package/scripts/postinstall.js +89 -0
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCheckpoint = createCheckpoint;
6
7
  exports.runCheckpointNewCommand = runCheckpointNewCommand;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -76,7 +77,7 @@ function normalizeWorkspace(value) {
76
77
  }
77
78
  return normalized;
78
79
  }
79
- function runCheckpointNewCommand(options) {
80
+ function createCheckpoint(options) {
80
81
  const title = options.title.trim();
81
82
  if (!title) {
82
83
  throw new errors_1.UsageError("checkpoint title cannot be empty");
@@ -140,5 +141,21 @@ function runCheckpointNewCommand(options) {
140
141
  runId: options.runId,
141
142
  now,
142
143
  });
143
- console.log(`checkpoint created: ${ws}:${id} (${path_1.default.relative(options.root, filePath)})`);
144
+ return {
145
+ workspace: ws,
146
+ id,
147
+ qid: `${ws}:${id}`,
148
+ path: path_1.default.relative(options.root, filePath),
149
+ };
150
+ }
151
+ function runCheckpointNewCommand(options) {
152
+ const checkpoint = createCheckpoint(options);
153
+ if (options.json) {
154
+ console.log(JSON.stringify({
155
+ action: "created",
156
+ checkpoint,
157
+ }, null, 2));
158
+ return;
159
+ }
160
+ console.log(`checkpoint created: ${checkpoint.qid} (${checkpoint.path})`);
144
161
  }
@@ -6,6 +6,14 @@ const event_support_1 = require("./event_support");
6
6
  const errors_1 = require("../util/errors");
7
7
  function runEventEnableCommand(options) {
8
8
  const result = (0, event_support_1.ensureEventsEnabled)(options);
9
+ if (options.json) {
10
+ console.log(JSON.stringify({
11
+ action: "enabled",
12
+ workspace: result.ws,
13
+ created: result.created,
14
+ }, null, 2));
15
+ return;
16
+ }
9
17
  const createdLabel = result.created ? "created" : "already present";
10
18
  console.log(`event logging enabled: ${result.ws} (${createdLabel})`);
11
19
  }
@@ -41,5 +49,9 @@ function runEventAppendCommand(options) {
41
49
  skill: options.skill,
42
50
  tool: options.tool,
43
51
  });
52
+ if (options.json) {
53
+ console.log(JSON.stringify({ action: "appended", event: record }, null, 2));
54
+ return;
55
+ }
44
56
  console.log(`event appended: ${record.workspace}:${record.kind} (${record.run_id})`);
45
57
  }
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
9
9
  const config_1 = require("../core/config");
10
10
  const index_cache_1 = require("../graph/index_cache");
11
11
  const node_1 = require("../graph/node");
12
+ const agent_file_types_1 = require("../graph/agent_file_types");
12
13
  const indexer_1 = require("../graph/indexer");
13
14
  const loader_1 = require("../templates/loader");
14
15
  const date_1 = require("../util/date");
@@ -44,6 +45,13 @@ function normalizeIdRef(value, key) {
44
45
  }
45
46
  return normalized;
46
47
  }
48
+ function normalizeAgentFileId(value) {
49
+ const normalized = value.toLowerCase();
50
+ if (!(0, id_1.isPortableId)(normalized)) {
51
+ throw new errors_1.UsageError("--id must be a lowercase portable id for agent workflow file types");
52
+ }
53
+ return normalized;
54
+ }
47
55
  function normalizeList(raw) {
48
56
  return parseCsvList(raw);
49
57
  }
@@ -110,6 +118,9 @@ function idPrefixForType(type) {
110
118
  if (type === "checkpoint") {
111
119
  return "chk";
112
120
  }
121
+ if (type === "work_order") {
122
+ return "order";
123
+ }
113
124
  return type;
114
125
  }
115
126
  function folderForType(type) {
@@ -122,8 +133,17 @@ function folderForType(type) {
122
133
  if (node_1.WORK_TYPES.has(type)) {
123
134
  return "work";
124
135
  }
136
+ if ((0, agent_file_types_1.isAgentFileType)(type)) {
137
+ return "work";
138
+ }
125
139
  throw new errors_1.UsageError(`unsupported type: ${type}`);
126
140
  }
141
+ function fileNameForType(type, id, slug) {
142
+ if ((0, agent_file_types_1.isAgentFileType)(type)) {
143
+ return path_1.default.join(`${id}-${slug}`, agent_file_types_1.AGENT_FILE_BASENAMES[type]);
144
+ }
145
+ return `${id}-${slug}.md`;
146
+ }
127
147
  function ensureExists(index, value, ws, label) {
128
148
  const resolved = (0, qid_1.resolveQid)(index, value, ws);
129
149
  if (resolved.status !== "ok") {
@@ -152,10 +172,18 @@ function runNewCommand(options) {
152
172
  useCache: !noCache,
153
173
  allowReindex: !noReindex,
154
174
  });
175
+ if (options.id !== undefined && !(0, agent_file_types_1.isAgentFileType)(type)) {
176
+ throw new errors_1.UsageError("--id is only valid for agent workflow file types");
177
+ }
155
178
  const prefix = idPrefixForType(type);
156
- const id = nextIdForPrefix(index.nodes, ws, prefix);
179
+ const id = options.id !== undefined
180
+ ? normalizeAgentFileId(options.id)
181
+ : nextIdForPrefix(index.nodes, ws, prefix);
182
+ if (index.nodes[`${ws}:${id}`]) {
183
+ throw new errors_1.UsageError(`node already exists: ${ws}:${id}`);
184
+ }
157
185
  const slug = slugifyTitle(title);
158
- const fileName = `${id}-${slug}.md`;
186
+ const fileName = fileNameForType(type, id, slug);
159
187
  const wsEntry = config.workspaces[ws];
160
188
  const folder = folderForType(type);
161
189
  const targetDir = path_1.default.resolve(options.root, wsEntry.path, wsEntry.mdkg_dir, folder);
@@ -283,7 +311,7 @@ function runNewCommand(options) {
283
311
  created: today,
284
312
  updated: today,
285
313
  });
286
- fs_1.default.mkdirSync(targetDir, { recursive: true });
314
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
287
315
  fs_1.default.writeFileSync(filePath, content, "utf8");
288
316
  if (config.index.auto_reindex && !noReindex) {
289
317
  const updatedIndex = (0, indexer_1.buildIndex)(options.root, config, { tolerant: config.index.tolerant });
@@ -300,5 +328,23 @@ function runNewCommand(options) {
300
328
  runId: options.runId,
301
329
  now: options.now,
302
330
  });
303
- console.log(`node created: ${ws}:${id} (${path_1.default.relative(options.root, filePath)})`);
331
+ const relativePath = path_1.default.relative(options.root, filePath);
332
+ const receipt = {
333
+ workspace: ws,
334
+ id,
335
+ qid: `${ws}:${id}`,
336
+ path: relativePath,
337
+ type,
338
+ title,
339
+ ...(status ? { status } : {}),
340
+ ...(priority !== undefined ? { priority } : {}),
341
+ };
342
+ if (options.json) {
343
+ console.log(JSON.stringify({
344
+ action: "created",
345
+ node: receipt,
346
+ }, null, 2));
347
+ return;
348
+ }
349
+ console.log(`node created: ${receipt.qid} (${receipt.path})`);
304
350
  }
@@ -106,6 +106,19 @@ function renderSkillMetaBody(skill) {
106
106
  }
107
107
  lines.push(`has_scripts: ${skill.has_scripts ? "true" : "false"}`);
108
108
  lines.push(`has_references: ${skill.has_references ? "true" : "false"}`);
109
+ for (const [namespace, values] of Object.entries(skill.extensions).sort(([a], [b]) => a.localeCompare(b))) {
110
+ for (const [key, value] of Object.entries(values).sort(([a], [b]) => a.localeCompare(b))) {
111
+ if (Array.isArray(value)) {
112
+ lines.push(`extensions.${namespace}.${key}: ${value.join(", ")}`);
113
+ }
114
+ else if (typeof value === "boolean") {
115
+ lines.push(`extensions.${namespace}.${key}: ${value ? "true" : "false"}`);
116
+ }
117
+ else {
118
+ lines.push(`extensions.${namespace}.${key}: ${value}`);
119
+ }
120
+ }
121
+ }
109
122
  for (const [key, value] of Object.entries(skill.ochatr).sort(([a], [b]) => a.localeCompare(b))) {
110
123
  if (Array.isArray(value)) {
111
124
  lines.push(`${key}: ${value.join(", ")}`);
@@ -170,6 +183,7 @@ function appendSkillsToPack(pack, skillEntries, depth, root) {
170
183
  artifacts: [],
171
184
  refs: [],
172
185
  aliases: [skill.slug, ...skill.tags],
186
+ attributes: {},
173
187
  body: depth === "full" ? loadSkillFullBody(root, skill) : renderSkillMetaBody(skill),
174
188
  }));
175
189
  if (newNodes.length === 0) {
@@ -25,6 +25,7 @@ function toNodeSummaryJson(node) {
25
25
  refs: [...node.refs],
26
26
  aliases: [...node.aliases],
27
27
  skills: [...node.skills],
28
+ attributes: { ...(node.attributes ?? {}) },
28
29
  path: node.path,
29
30
  edges: {
30
31
  epic: node.edges.epic,
@@ -62,6 +63,7 @@ function toSkillSummaryJson(skill) {
62
63
  path: skill.path,
63
64
  has_scripts: skill.has_scripts,
64
65
  has_references: skill.has_references,
66
+ extensions: JSON.parse(JSON.stringify(skill.extensions)),
65
67
  ochatr: { ...skill.ochatr },
66
68
  };
67
69
  }
@@ -15,9 +15,16 @@ function normalizeWorkspace(value) {
15
15
  return value;
16
16
  }
17
17
  function buildSearchText(node) {
18
+ const attributeTokens = Object.entries(node.attributes ?? {}).flatMap(([key, value]) => {
19
+ if (Array.isArray(value)) {
20
+ return [key, ...value];
21
+ }
22
+ return [key, String(value)];
23
+ });
18
24
  const tokens = [
19
25
  node.id,
20
26
  node.qid,
27
+ node.type,
21
28
  node.title,
22
29
  node.path,
23
30
  ...node.tags,
@@ -27,6 +34,7 @@ function buildSearchText(node) {
27
34
  ...node.refs,
28
35
  ...node.aliases,
29
36
  ...node.skills,
37
+ ...attributeTokens,
30
38
  ];
31
39
  return tokens.join(" ").toLowerCase();
32
40
  }
@@ -25,6 +25,12 @@ function maybeLine(label, values) {
25
25
  }
26
26
  return `${label}: ${values.join(", ")}`;
27
27
  }
28
+ function formatAttributeLine(key, value) {
29
+ if (Array.isArray(value)) {
30
+ return `${key}: ${value.join(", ")}`;
31
+ }
32
+ return `${key}: ${String(value)}`;
33
+ }
28
34
  function runShowCommand(options) {
29
35
  const config = (0, config_1.loadConfig)(options.root);
30
36
  const ws = normalizeWorkspace(options.ws);
@@ -79,6 +85,7 @@ function runShowCommand(options) {
79
85
  maybeLine("skills", node.skills),
80
86
  ].filter((line) => Boolean(line));
81
87
  lines.push(...metaLines);
88
+ lines.push(...Object.entries(node.attributes ?? {}).map(([key, value]) => formatAttributeLine(key, value)));
82
89
  if (node.edges.epic) {
83
90
  lines.push(`epic: ${node.edges.epic}`);
84
91
  }
@@ -99,6 +99,16 @@ function filterSkills(skills, tags, tagsMode = "any") {
99
99
  });
100
100
  }
101
101
  function buildSkillSearchText(skill) {
102
+ const extensionTokens = Object.entries(skill.extensions).flatMap(([namespace, values]) => Object.entries(values).flatMap(([key, value]) => {
103
+ const field = `extensions.${namespace}.${key}`;
104
+ if (Array.isArray(value)) {
105
+ return [field, ...value];
106
+ }
107
+ if (typeof value === "boolean") {
108
+ return [field, value ? "true" : "false"];
109
+ }
110
+ return [field, value];
111
+ }));
102
112
  const ochatrTokens = Object.entries(skill.ochatr).flatMap(([key, value]) => {
103
113
  if (Array.isArray(value)) {
104
114
  return [key, ...value];
@@ -118,6 +128,7 @@ function buildSkillSearchText(skill) {
118
128
  ...skill.tags,
119
129
  ...skill.authors,
120
130
  ...skill.links,
131
+ ...extensionTokens,
121
132
  ...ochatrTokens,
122
133
  ];
123
134
  return tokens.join(" ").toLowerCase();
@@ -191,7 +202,23 @@ function runSkillNewCommand(options) {
191
202
  skill: slug,
192
203
  now: options.now,
193
204
  });
194
- console.log(`skill created: root:skill:${slug} (${path_1.default.relative(root, canonicalPath)})`);
205
+ const receipt = {
206
+ workspace: "root",
207
+ id: `skill:${slug}`,
208
+ qid: `root:skill:${slug}`,
209
+ slug,
210
+ name,
211
+ path: path_1.default.relative(root, canonicalPath),
212
+ with_scripts: Boolean(options.withScripts),
213
+ };
214
+ if (options.json) {
215
+ console.log(JSON.stringify({
216
+ action: "created",
217
+ skill: receipt,
218
+ }, null, 2));
219
+ return;
220
+ }
221
+ console.log(`skill created: ${receipt.qid} (${receipt.path})`);
195
222
  }
196
223
  function runSkillListCommand(options) {
197
224
  const config = (0, config_1.loadConfig)(options.root);
@@ -274,6 +301,19 @@ function runSkillShowCommand(options) {
274
301
  }
275
302
  lines.push(`has_scripts: ${skill.has_scripts ? "true" : "false"}`);
276
303
  lines.push(`has_references: ${skill.has_references ? "true" : "false"}`);
304
+ for (const [namespace, values] of Object.entries(skill.extensions).sort(([a], [b]) => a.localeCompare(b))) {
305
+ for (const [key, value] of Object.entries(values).sort(([a], [b]) => a.localeCompare(b))) {
306
+ if (Array.isArray(value)) {
307
+ lines.push(`extensions.${namespace}.${key}: ${value.join(", ")}`);
308
+ continue;
309
+ }
310
+ if (typeof value === "boolean") {
311
+ lines.push(`extensions.${namespace}.${key}: ${value ? "true" : "false"}`);
312
+ continue;
313
+ }
314
+ lines.push(`extensions.${namespace}.${key}: ${value}`);
315
+ }
316
+ }
277
317
  for (const [key, value] of Object.entries(skill.ochatr).sort(([a], [b]) => a.localeCompare(b))) {
278
318
  if (Array.isArray(value)) {
279
319
  lines.push(`${key}: ${value.join(", ")}`);
@@ -329,8 +369,11 @@ function runSkillValidateCommand(options) {
329
369
  const targetSlug = options.slug?.trim().toLowerCase();
330
370
  const warnings = [];
331
371
  const errors = [];
372
+ let checkedCount = 0;
332
373
  if (targetSlug) {
333
- const result = validateSingleSkill(options.root, normalizeSlug(targetSlug));
374
+ const normalizedSlug = normalizeSlug(targetSlug);
375
+ const result = validateSingleSkill(options.root, normalizedSlug);
376
+ checkedCount = 1;
334
377
  warnings.push(...result.warnings);
335
378
  errors.push(...result.errors);
336
379
  }
@@ -338,31 +381,49 @@ function runSkillValidateCommand(options) {
338
381
  const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(options.root, config);
339
382
  if (fs_1.default.existsSync(skillsRoot)) {
340
383
  const entries = fs_1.default.readdirSync(skillsRoot, { withFileTypes: true });
341
- for (const entry of entries.filter((value) => value.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
384
+ const skillDirs = entries
385
+ .filter((value) => value.isDirectory())
386
+ .sort((a, b) => a.name.localeCompare(b.name));
387
+ checkedCount = skillDirs.length;
388
+ for (const entry of skillDirs) {
342
389
  const result = validateSingleSkill(options.root, entry.name.toLowerCase());
343
390
  warnings.push(...result.warnings);
344
391
  errors.push(...result.errors);
345
392
  }
346
393
  }
347
394
  }
348
- for (const warning of Array.from(new Set(warnings))) {
395
+ const uniqueWarnings = Array.from(new Set(warnings));
396
+ const uniqueErrors = Array.from(new Set(errors));
397
+ const receipt = {
398
+ action: "validated",
399
+ ok: uniqueErrors.length === 0,
400
+ checked_count: checkedCount,
401
+ warning_count: uniqueWarnings.length,
402
+ error_count: uniqueErrors.length,
403
+ warnings: uniqueWarnings,
404
+ errors: uniqueErrors,
405
+ ...(targetSlug ? { target: normalizeSlug(targetSlug) } : {}),
406
+ };
407
+ if (options.json) {
408
+ console.log(JSON.stringify(receipt, null, 2));
409
+ if (uniqueErrors.length > 0) {
410
+ throw new errors_1.ValidationError(`skill validation failed with ${uniqueErrors.length} error(s)`);
411
+ }
412
+ return;
413
+ }
414
+ for (const warning of uniqueWarnings) {
349
415
  console.error(`warning: ${warning}`);
350
416
  }
351
- if (errors.length > 0) {
352
- for (const error of Array.from(new Set(errors))) {
417
+ if (uniqueErrors.length > 0) {
418
+ for (const error of uniqueErrors) {
353
419
  console.error(error);
354
420
  }
355
- throw new errors_1.ValidationError(`skill validation failed with ${Array.from(new Set(errors)).length} error(s)`);
421
+ throw new errors_1.ValidationError(`skill validation failed with ${uniqueErrors.length} error(s)`);
356
422
  }
357
423
  if (targetSlug) {
358
424
  console.log(`skill validation ok: ${targetSlug} (1 skill checked)`);
359
425
  return;
360
426
  }
361
- const checkedCount = fs_1.default.existsSync((0, skills_indexer_1.resolveSkillsRoot)(options.root, config))
362
- ? fs_1.default
363
- .readdirSync((0, skills_indexer_1.resolveSkillsRoot)(options.root, config), { withFileTypes: true })
364
- .filter((value) => value.isDirectory()).length
365
- : 0;
366
427
  console.log(`skill validation ok: ${checkedCount} skill${checkedCount === 1 ? "" : "s"} checked`);
367
428
  }
368
429
  function runSkillSyncCommand(options) {
@@ -373,5 +434,12 @@ function runSkillSyncCommand(options) {
373
434
  createRoots: true,
374
435
  force: options.force,
375
436
  });
437
+ if (options.json) {
438
+ console.log(JSON.stringify({
439
+ action: "synced",
440
+ sync: result,
441
+ }, null, 2));
442
+ return;
443
+ }
376
444
  console.log(`skill mirror sync ok: ${result.synced} synced, ${result.pruned} pruned across ${result.targets} target${result.targets === 1 ? "" : "s"}`);
377
445
  }
@@ -175,6 +175,30 @@ function maybeWarnEventsDisabled(root, config, ws) {
175
175
  }
176
176
  console.error(`note: events.jsonl is missing for workspace ${ws}; run mdkg event enable --ws ${ws} to restore JSONL provenance`);
177
177
  }
178
+ function taskReceipt(root, loaded) {
179
+ const rawPriority = loaded.frontmatter.priority;
180
+ const priority = rawPriority === undefined ? undefined : Number.parseInt(String(rawPriority), 10);
181
+ return {
182
+ workspace: loaded.ws,
183
+ id: loaded.id,
184
+ qid: loaded.qid,
185
+ path: path_1.default.relative(root, loaded.filePath),
186
+ type: loaded.type,
187
+ status: String(loaded.frontmatter.status ?? ""),
188
+ ...(Number.isInteger(priority) ? { priority } : {}),
189
+ };
190
+ }
191
+ function printTaskMutationReceipt(action, root, loaded, json, checkpoint) {
192
+ if (json) {
193
+ console.log(JSON.stringify({
194
+ action,
195
+ task: taskReceipt(root, loaded),
196
+ ...(checkpoint ? { checkpoint } : {}),
197
+ }, null, 2));
198
+ return;
199
+ }
200
+ console.log(`task ${action}: ${loaded.qid}`);
201
+ }
178
202
  function runTaskStartCommand(options) {
179
203
  const loaded = loadMutableTaskNode(options.root, options.id, options.ws);
180
204
  const now = options.now ?? new Date();
@@ -193,7 +217,7 @@ function runTaskStartCommand(options) {
193
217
  now,
194
218
  });
195
219
  maybeWarnEventsDisabled(options.root, loaded.config, loaded.ws);
196
- console.log(`task started: ${loaded.qid}`);
220
+ printTaskMutationReceipt("started", options.root, loaded, options.json);
197
221
  }
198
222
  function runTaskUpdateCommand(options) {
199
223
  const loaded = loadMutableTaskNode(options.root, options.id, options.ws);
@@ -235,7 +259,7 @@ function runTaskUpdateCommand(options) {
235
259
  runId: options.runId,
236
260
  now,
237
261
  });
238
- console.log(`task updated: ${loaded.qid}`);
262
+ printTaskMutationReceipt("updated", options.root, loaded, options.json);
239
263
  }
240
264
  function runTaskDoneCommand(options) {
241
265
  const loaded = loadMutableTaskNode(options.root, options.id, options.ws);
@@ -260,7 +284,20 @@ function runTaskDoneCommand(options) {
260
284
  runId: options.runId,
261
285
  now,
262
286
  });
263
- if (options.checkpoint) {
287
+ let checkpoint;
288
+ if (options.checkpoint && options.json) {
289
+ checkpoint = (0, checkpoint_1.createCheckpoint)({
290
+ root: options.root,
291
+ title: options.checkpoint,
292
+ ws: loaded.ws,
293
+ relates: loaded.id,
294
+ scope: loaded.id,
295
+ runId: options.runId,
296
+ note: `checkpoint created from mdkg task done for ${loaded.id}`,
297
+ now,
298
+ });
299
+ }
300
+ else if (options.checkpoint) {
264
301
  (0, checkpoint_1.runCheckpointNewCommand)({
265
302
  root: options.root,
266
303
  title: options.checkpoint,
@@ -274,5 +311,5 @@ function runTaskDoneCommand(options) {
274
311
  }
275
312
  maybeReindex(options.root, loaded.config);
276
313
  maybeWarnEventsDisabled(options.root, loaded.config, loaded.ws);
277
- console.log(`task done: ${loaded.qid}`);
314
+ printTaskMutationReceipt("done", options.root, loaded, options.json, checkpoint);
278
315
  }
@@ -65,6 +65,13 @@ const RECOMMENDED_HEADINGS = {
65
65
  "Rollout plan",
66
66
  ],
67
67
  dec: ["Context", "Decision", "Alternatives considered", "Consequences", "Links / references"],
68
+ spec: ["Purpose", "Runtime", "Work Contracts", "Capabilities"],
69
+ work: ["Capability", "Inputs", "Outputs", "Receipt"],
70
+ work_order: ["Request", "Inputs", "Constraints"],
71
+ receipt: ["Outcome", "Artifacts", "Notes"],
72
+ feedback: ["Feedback", "Evidence"],
73
+ dispute: ["Dispute", "Evidence", "Resolution"],
74
+ proposal: ["Summary", "Evidence", "Proposed Change", "Review"],
68
75
  };
69
76
  function normalizeHeading(value) {
70
77
  return value.trim().toLowerCase();
@@ -119,6 +126,7 @@ function buildIndexNode(root, ws, filePath, node) {
119
126
  refs: node.refs,
120
127
  aliases: node.aliases,
121
128
  skills: node.skills,
129
+ attributes: node.attributes,
122
130
  path: path_1.default.relative(root, filePath),
123
131
  edges: normalizeEdges(node.edges, ws),
124
132
  };
@@ -256,11 +264,10 @@ function runValidateCommand(options) {
256
264
  nodes,
257
265
  reverse_edges: {},
258
266
  };
259
- const graphErrors = (0, validate_graph_1.collectGraphErrors)(index, { allowMissing: false });
260
- errors.push(...graphErrors);
267
+ let knownSkills = new Set();
261
268
  try {
262
269
  const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(options.root, config);
263
- const knownSkills = new Set(Object.keys(skillsIndex.skills));
270
+ knownSkills = new Set(Object.keys(skillsIndex.skills));
264
271
  for (const node of Object.values(nodes)) {
265
272
  for (const slug of node.skills) {
266
273
  if (!knownSkills.has(slug)) {
@@ -273,6 +280,11 @@ function runValidateCommand(options) {
273
280
  const message = err instanceof Error ? err.message : "unknown skill validation error";
274
281
  errors.push(message);
275
282
  }
283
+ const graphErrors = (0, validate_graph_1.collectGraphErrors)(index, {
284
+ allowMissing: false,
285
+ knownSkillSlugs: knownSkills,
286
+ });
287
+ errors.push(...graphErrors);
276
288
  const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(options.root, config);
277
289
  for (const dirPath of listDirectories(skillsRoot)) {
278
290
  const canonicalPath = path_1.default.join(dirPath, "SKILL.md");
@@ -305,6 +317,22 @@ function runValidateCommand(options) {
305
317
  fs_1.default.mkdirSync(path_1.default.dirname(outPath), { recursive: true });
306
318
  fs_1.default.writeFileSync(outPath, reportLines.join("\n"), "utf8");
307
319
  }
320
+ const receipt = {
321
+ action: "validated",
322
+ ok: uniqueErrors.length === 0,
323
+ warning_count: uniqueWarnings.length,
324
+ error_count: uniqueErrors.length,
325
+ warnings: uniqueWarnings,
326
+ errors: uniqueErrors,
327
+ ...(outPath ? { report_path: outPath } : {}),
328
+ };
329
+ if (options.json) {
330
+ console.log(JSON.stringify(receipt, null, 2));
331
+ if (uniqueErrors.length > 0) {
332
+ throw new errors_1.ValidationError(`validation failed with ${uniqueErrors.length} error(s)`);
333
+ }
334
+ return;
335
+ }
308
336
  if (!options.quiet) {
309
337
  for (const warning of uniqueWarnings) {
310
338
  console.error(`warning: ${warning}`);