mdkg 0.3.6 → 0.3.8

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 (68) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/CLI_COMMAND_MATRIX.md +132 -24
  3. package/README.md +80 -25
  4. package/dist/cli.js +193 -36
  5. package/dist/command-contract.json +882 -122
  6. package/dist/commands/bundle.js +2 -0
  7. package/dist/commands/capability.js +1 -0
  8. package/dist/commands/checkpoint.js +139 -1
  9. package/dist/commands/db.js +8 -0
  10. package/dist/commands/doctor.js +7 -7
  11. package/dist/commands/format.js +155 -0
  12. package/dist/commands/goal.js +60 -0
  13. package/dist/commands/graph.js +148 -0
  14. package/dist/commands/handoff.js +301 -0
  15. package/dist/commands/mcp.js +9 -0
  16. package/dist/commands/new.js +12 -3
  17. package/dist/commands/pack.js +3 -1
  18. package/dist/commands/query_output.js +2 -0
  19. package/dist/commands/show.js +8 -0
  20. package/dist/commands/spec.js +76 -17
  21. package/dist/commands/status.js +1 -0
  22. package/dist/commands/subgraph.js +7 -4
  23. package/dist/commands/task.js +2 -0
  24. package/dist/commands/upgrade.js +128 -3
  25. package/dist/commands/validate.js +268 -6
  26. package/dist/commands/work.js +176 -5
  27. package/dist/core/project_db_queue_contract.js +101 -0
  28. package/dist/graph/agent_file_types.js +59 -20
  29. package/dist/graph/capabilities_indexer.js +45 -3
  30. package/dist/graph/edges.js +15 -0
  31. package/dist/graph/frontmatter.js +4 -1
  32. package/dist/graph/indexer.js +13 -0
  33. package/dist/graph/node.js +12 -1
  34. package/dist/graph/sqlite_index.js +2 -0
  35. package/dist/graph/subgraphs.js +2 -0
  36. package/dist/graph/template_schema.js +37 -17
  37. package/dist/graph/validate_graph.js +16 -5
  38. package/dist/graph/visibility.js +6 -0
  39. package/dist/init/AGENT_START.md +9 -6
  40. package/dist/init/CLI_COMMAND_MATRIX.md +50 -16
  41. package/dist/init/README.md +26 -9
  42. package/dist/init/init-manifest.json +67 -12
  43. package/dist/init/templates/default/bug.md +2 -0
  44. package/dist/init/templates/default/chk.md +3 -0
  45. package/dist/init/templates/default/epic.md +2 -0
  46. package/dist/init/templates/default/feat.md +2 -0
  47. package/dist/init/templates/default/goal.md +3 -0
  48. package/dist/init/templates/default/manifest.md +45 -0
  49. package/dist/init/templates/default/spike.md +2 -0
  50. package/dist/init/templates/default/task.md +2 -0
  51. package/dist/init/templates/default/test.md +2 -0
  52. package/dist/init/templates/specs/agent.MANIFEST.md +80 -0
  53. package/dist/init/templates/specs/api.MANIFEST.md +33 -0
  54. package/dist/init/templates/specs/base.MANIFEST.md +120 -0
  55. package/dist/init/templates/specs/capability.MANIFEST.md +45 -0
  56. package/dist/init/templates/specs/integration.MANIFEST.md +25 -0
  57. package/dist/init/templates/specs/model.MANIFEST.md +21 -0
  58. package/dist/init/templates/specs/project.MANIFEST.md +39 -0
  59. package/dist/init/templates/specs/runtime-agent.MANIFEST.md +49 -0
  60. package/dist/init/templates/specs/runtime-image.MANIFEST.md +21 -0
  61. package/dist/init/templates/specs/tool.MANIFEST.md +25 -0
  62. package/dist/pack/export_json.js +20 -8
  63. package/dist/pack/export_md.js +15 -4
  64. package/dist/pack/export_xml.js +9 -4
  65. package/dist/pack/metrics.js +12 -4
  66. package/dist/pack/pack.js +9 -1
  67. package/dist/util/argparse.js +3 -0
  68. package/package.json +21 -3
@@ -3,13 +3,16 @@ 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.RECOMMENDED_HEADINGS = void 0;
6
7
  exports.collectValidateReceipt = collectValidateReceipt;
7
8
  exports.runValidateCommand = runValidateCommand;
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
10
12
  const config_1 = require("../core/config");
11
13
  const template_schema_1 = require("../graph/template_schema");
12
14
  const node_1 = require("../graph/node");
15
+ const agent_file_types_1 = require("../graph/agent_file_types");
13
16
  const skills_indexer_1 = require("../graph/skills_indexer");
14
17
  const workspace_files_1 = require("../graph/workspace_files");
15
18
  const validate_graph_1 = require("../graph/validate_graph");
@@ -18,7 +21,7 @@ const visibility_1 = require("../graph/visibility");
18
21
  const sqlite_index_1 = require("../graph/sqlite_index");
19
22
  const errors_1 = require("../util/errors");
20
23
  const skill_mirror_1 = require("./skill_mirror");
21
- const RECOMMENDED_HEADINGS = {
24
+ exports.RECOMMENDED_HEADINGS = {
22
25
  task: [
23
26
  "Overview",
24
27
  "Acceptance Criteria",
@@ -103,6 +106,234 @@ function extractHeadings(body) {
103
106
  }
104
107
  return headings;
105
108
  }
109
+ const RAW_CONTENT_MARKERS = [
110
+ { id: "raw_prompt", pattern: /\bRAW_PROMPT_MARKER\b/i, description: "raw prompt marker" },
111
+ { id: "raw_payload", pattern: /\bRAW_PAYLOAD_MARKER\b/i, description: "raw payload marker" },
112
+ { id: "raw_secret", pattern: /\bRAW_SECRET_MARKER\b|BEGIN [A-Z ]*PRIVATE KEY|secret\s*=/i, description: "raw secret marker" },
113
+ ];
114
+ function shouldCheckRawContentWarnings(node) {
115
+ if ((0, agent_file_types_1.isAgentFileType)(node.type)) {
116
+ return true;
117
+ }
118
+ return node.type === "checkpoint" && typeof node.frontmatter.checkpoint_kind === "string";
119
+ }
120
+ function collectRawContentWarnings(qid, node) {
121
+ if (!shouldCheckRawContentWarnings(node)) {
122
+ return [];
123
+ }
124
+ const warnings = [];
125
+ for (const marker of RAW_CONTENT_MARKERS) {
126
+ if (marker.pattern.test(node.body)) {
127
+ warnings.push(`${qid}: raw-content.${marker.id} warning: ${marker.description} detected; use refs, hashes, summaries, or artifact links instead of raw secrets/prompts/payloads`);
128
+ }
129
+ }
130
+ return warnings;
131
+ }
132
+ function collectManifestCompatibilityWarnings(qid, filePath, node) {
133
+ const basename = path_1.default.basename(filePath);
134
+ if (basename === agent_file_types_1.LEGACY_SPEC_BASENAME && node.type === "spec") {
135
+ return [
136
+ `${qid}: manifest.compat.spec_legacy warning: SPEC.md is legacy; MANIFEST.md is the canonical manifest filename. Rename this file before the compatibility release closes.`,
137
+ ];
138
+ }
139
+ if (basename === agent_file_types_1.CANONICAL_MANIFEST_BASENAME && node.type === "spec") {
140
+ return [
141
+ `${qid}: manifest.compat.type_spec warning: MANIFEST.md uses legacy type: spec; use type: manifest before the compatibility release closes.`,
142
+ ];
143
+ }
144
+ return [];
145
+ }
146
+ function collectChangedPaths(root) {
147
+ const result = (0, child_process_1.spawnSync)("git", ["-C", root, "status", "--porcelain", "--", ".mdkg"], {
148
+ encoding: "utf8",
149
+ });
150
+ if (result.status !== 0) {
151
+ return new Set();
152
+ }
153
+ const changed = new Set();
154
+ for (const line of result.stdout.split(/\r?\n/)) {
155
+ if (!line.trim()) {
156
+ continue;
157
+ }
158
+ const rawPath = line.slice(3).trim();
159
+ const filePath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
160
+ changed.add(filePath.replace(/\\/g, "/"));
161
+ }
162
+ return changed;
163
+ }
164
+ function qidFromWarning(message) {
165
+ const match = /^([a-z0-9_-]+:[^\s:]+):/.exec(message);
166
+ return match?.[1];
167
+ }
168
+ function warningPath(message, nodes) {
169
+ const qid = qidFromWarning(message);
170
+ if (qid && nodes[qid]) {
171
+ return nodes[qid].path;
172
+ }
173
+ for (const node of Object.values(nodes)) {
174
+ if (message.includes(node.path)) {
175
+ return node.path;
176
+ }
177
+ }
178
+ const match = /([.]mdkg\/[^\s:]+\.md|[.]mdkg\/[^\s:]+\/SKILLS?\.md)/.exec(message);
179
+ return match?.[1];
180
+ }
181
+ function warningDiagnostic(message, nodes) {
182
+ const qid = qidFromWarning(message);
183
+ const nodeType = qid && nodes[qid] ? nodes[qid].type : undefined;
184
+ const pathValue = warningPath(message, nodes);
185
+ const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
186
+ if (rawMatch) {
187
+ return {
188
+ id: `raw-content.${rawMatch[1]}`,
189
+ category: "raw-content",
190
+ severity: "warning",
191
+ message,
192
+ qid,
193
+ node_type: nodeType,
194
+ path: pathValue,
195
+ ref: qid,
196
+ remediation: "Replace raw secrets, prompts, tokens, or payloads with refs, hashes, redacted summaries, or archive/artifact links.",
197
+ };
198
+ }
199
+ if (message.includes("missing recommended heading")) {
200
+ return {
201
+ id: "heading.missing",
202
+ category: "headings",
203
+ severity: "warning",
204
+ message,
205
+ qid,
206
+ node_type: nodeType,
207
+ path: pathValue,
208
+ ref: qid,
209
+ remediation: "Run mdkg format --headings --dry-run to review missing heading additions, then --apply if acceptable.",
210
+ };
211
+ }
212
+ if (message.includes("bundled template schema fallback")) {
213
+ return {
214
+ id: "template_schema.fallback",
215
+ category: "templates",
216
+ severity: "warning",
217
+ message,
218
+ node_type: nodeType,
219
+ path: pathValue,
220
+ remediation: "Run mdkg upgrade --apply to vendor missing built-in template schemas when the managed asset update is safe.",
221
+ };
222
+ }
223
+ const manifestCompatMatch = /manifest\.compat\.([a-z_]+)/.exec(message);
224
+ if (manifestCompatMatch) {
225
+ return {
226
+ id: `manifest.compat.${manifestCompatMatch[1]}`,
227
+ category: "manifest-compatibility",
228
+ severity: "warning",
229
+ message,
230
+ qid,
231
+ node_type: nodeType,
232
+ path: pathValue,
233
+ ref: qid,
234
+ remediation: "Rename legacy SPEC.md files to MANIFEST.md and update transitional type: spec frontmatter to type: manifest before the compatibility release closes.",
235
+ };
236
+ }
237
+ if (message.includes("sqlite") || message.includes("index")) {
238
+ return {
239
+ id: "cache.index",
240
+ category: "cache",
241
+ severity: "warning",
242
+ message,
243
+ node_type: nodeType,
244
+ path: pathValue,
245
+ remediation: "Run mdkg index or mdkg db index rebuild when generated cache state should be refreshed.",
246
+ };
247
+ }
248
+ if (message.includes("skill") || message.includes("mirror")) {
249
+ return {
250
+ id: "skill_mirror.warning",
251
+ category: "skills",
252
+ severity: "warning",
253
+ message,
254
+ node_type: nodeType,
255
+ path: pathValue,
256
+ remediation: "Run mdkg skill sync after reviewing managed skill mirror drift.",
257
+ };
258
+ }
259
+ if (message.includes("subgraph")) {
260
+ return {
261
+ id: "subgraph.warning",
262
+ category: "subgraph",
263
+ severity: "warning",
264
+ message,
265
+ node_type: nodeType,
266
+ path: pathValue,
267
+ remediation: "Run mdkg subgraph verify or refresh the source bundle after reviewing child graph freshness.",
268
+ };
269
+ }
270
+ return {
271
+ id: "warning.generic",
272
+ category: "general",
273
+ severity: "warning",
274
+ message,
275
+ qid,
276
+ node_type: nodeType,
277
+ path: pathValue,
278
+ ref: qid,
279
+ remediation: "Review the warning and apply the focused mdkg command suggested by the message when appropriate.",
280
+ };
281
+ }
282
+ function countBuckets(values) {
283
+ const counts = new Map();
284
+ for (const value of values) {
285
+ if (!value) {
286
+ continue;
287
+ }
288
+ counts.set(value, (counts.get(value) ?? 0) + 1);
289
+ }
290
+ return Array.from(counts.entries())
291
+ .map(([key, count]) => ({ key, count }))
292
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
293
+ }
294
+ function normalizeLimit(limit) {
295
+ if (limit === undefined) {
296
+ return 50;
297
+ }
298
+ if (!Number.isInteger(limit) || limit < 0) {
299
+ throw new errors_1.ValidationError("--limit must be a non-negative integer");
300
+ }
301
+ return limit;
302
+ }
303
+ function buildWarningSummary(diagnostics, limit) {
304
+ const effectiveLimit = limit === null || limit === undefined ? diagnostics.length : limit;
305
+ const emitted = Math.min(diagnostics.length, effectiveLimit);
306
+ const truncated = emitted < diagnostics.length;
307
+ return {
308
+ total: diagnostics.length,
309
+ emitted,
310
+ truncated,
311
+ omitted_count: diagnostics.length - emitted,
312
+ limit: limit === undefined ? null : limit,
313
+ affected_file_count: new Set(diagnostics.map((diagnostic) => diagnostic.path).filter(Boolean)).size,
314
+ by_id: countBuckets(diagnostics.map((diagnostic) => diagnostic.id)),
315
+ by_category: countBuckets(diagnostics.map((diagnostic) => diagnostic.category)),
316
+ by_node_type: countBuckets(diagnostics.map((diagnostic) => diagnostic.node_type)),
317
+ top_qids: countBuckets(diagnostics.map((diagnostic) => diagnostic.qid)).slice(0, 25),
318
+ top_paths: countBuckets(diagnostics.map((diagnostic) => diagnostic.path)).slice(0, 25),
319
+ };
320
+ }
321
+ function shapeValidateReceiptForSummary(receipt, limit) {
322
+ const effectiveLimit = normalizeLimit(limit);
323
+ const warningDiagnostics = receipt.warning_diagnostics.slice(0, effectiveLimit);
324
+ return {
325
+ ...receipt,
326
+ warnings: warningDiagnostics.map((warning) => warning.message),
327
+ warning_diagnostics: warningDiagnostics,
328
+ warning_summary: buildWarningSummary(receipt.warning_diagnostics, effectiveLimit),
329
+ };
330
+ }
331
+ function writeJsonReceipt(root, jsonOut, receipt) {
332
+ const outPath = path_1.default.resolve(root, jsonOut);
333
+ fs_1.default.mkdirSync(path_1.default.dirname(outPath), { recursive: true });
334
+ fs_1.default.writeFileSync(outPath, `${JSON.stringify(receipt, null, 2)}\n`, "utf8");
335
+ return outPath;
336
+ }
106
337
  function isCoreListFile(filePath) {
107
338
  return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
108
339
  }
@@ -121,6 +352,8 @@ function normalizeEdges(edges, ws) {
121
352
  relates: edges.relates.map((value) => normalizeEdgeTarget(value, ws)),
122
353
  blocked_by: edges.blocked_by.map((value) => normalizeEdgeTarget(value, ws)),
123
354
  blocks: edges.blocks.map((value) => normalizeEdgeTarget(value, ws)),
355
+ context_refs: (edges.context_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
356
+ evidence_refs: (edges.evidence_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
124
357
  };
125
358
  }
126
359
  function buildIndexNode(root, ws, filePath, node) {
@@ -178,6 +411,12 @@ function buildReverseEdges(nodes) {
178
411
  for (const target of node.edges.blocks) {
179
412
  addReverseEdge(reverse, "blocks", target, qid);
180
413
  }
414
+ for (const target of node.edges.context_refs ?? []) {
415
+ addReverseEdge(reverse, "context_refs", target, qid);
416
+ }
417
+ for (const target of node.edges.evidence_refs ?? []) {
418
+ addReverseEdge(reverse, "evidence_refs", target, qid);
419
+ }
181
420
  }
182
421
  for (const targets of Object.values(reverse)) {
183
422
  for (const sources of Object.values(targets)) {
@@ -264,6 +503,7 @@ function collectValidateReceipt(options) {
264
503
  const idsByWorkspace = {};
265
504
  for (const [alias, files] of Object.entries(filesByAlias)) {
266
505
  idsByWorkspace[alias] = new Map();
506
+ errors.push(...(0, agent_file_types_1.collectManifestSiblingConflicts)(files, (dirPath) => path_1.default.relative(options.root, dirPath).split(path_1.default.sep).join("/") || "."));
267
507
  for (const filePath of files) {
268
508
  if (isCoreListFile(filePath)) {
269
509
  continue;
@@ -292,7 +532,7 @@ function collectValidateReceipt(options) {
292
532
  idsByWorkspace[alias].set(node.id, filePath);
293
533
  const qid = `${alias}:${node.id}`;
294
534
  nodes[qid] = buildIndexNode(options.root, alias, filePath, node);
295
- const recommended = RECOMMENDED_HEADINGS[node.type];
535
+ const recommended = exports.RECOMMENDED_HEADINGS[node.type];
296
536
  if (recommended) {
297
537
  const headings = extractHeadings(node.body);
298
538
  for (const heading of recommended) {
@@ -301,6 +541,8 @@ function collectValidateReceipt(options) {
301
541
  }
302
542
  }
303
543
  }
544
+ warnings.push(...collectRawContentWarnings(qid, node));
545
+ warnings.push(...collectManifestCompatibilityWarnings(qid, filePath, node));
304
546
  }
305
547
  catch (err) {
306
548
  const message = err instanceof Error ? err.message : "unknown error";
@@ -372,7 +614,13 @@ function collectValidateReceipt(options) {
372
614
  }
373
615
  warnings.push(...(0, skill_mirror_1.auditSkillMirrors)(options.root, config));
374
616
  validateEventsJsonl(options.root, config, errors);
375
- const uniqueWarnings = Array.from(new Set(warnings));
617
+ const allUniqueWarnings = Array.from(new Set(warnings));
618
+ const allWarningDiagnostics = allUniqueWarnings.map((warning) => warningDiagnostic(warning, nodes));
619
+ const changedPaths = options.changedOnly ? collectChangedPaths(options.root) : new Set();
620
+ const filteredWarningDiagnostics = options.changedOnly
621
+ ? allWarningDiagnostics.filter((warning) => warning.path !== undefined && changedPaths.has(warning.path))
622
+ : allWarningDiagnostics;
623
+ const uniqueWarnings = filteredWarningDiagnostics.map((warning) => warning.message);
376
624
  const uniqueErrors = Array.from(new Set(errors));
377
625
  const reportLines = [
378
626
  ...uniqueWarnings.map((warning) => `warning: ${warning}`),
@@ -390,24 +638,38 @@ function collectValidateReceipt(options) {
390
638
  warning_count: uniqueWarnings.length,
391
639
  error_count: uniqueErrors.length,
392
640
  warnings: uniqueWarnings,
641
+ warning_diagnostics: filteredWarningDiagnostics,
642
+ warning_summary: buildWarningSummary(filteredWarningDiagnostics),
393
643
  errors: uniqueErrors,
394
644
  ...(outPath ? { report_path: outPath } : {}),
645
+ ...(options.changedOnly
646
+ ? { warning_filter: { mode: "changed-only", changed_paths: Array.from(changedPaths).sort() } }
647
+ : {}),
395
648
  };
396
649
  return receipt;
397
650
  }
398
651
  function runValidateCommand(options) {
399
- const receipt = collectValidateReceipt(options);
652
+ let receipt = collectValidateReceipt(options);
653
+ if (options.jsonOut) {
654
+ const jsonOutPath = path_1.default.resolve(options.root, options.jsonOut);
655
+ receipt = { ...receipt, json_receipt_path: jsonOutPath };
656
+ writeJsonReceipt(options.root, options.jsonOut, receipt);
657
+ }
658
+ const displayReceipt = options.summary ? shapeValidateReceiptForSummary(receipt, options.limit) : receipt;
400
659
  if (options.json) {
401
- console.log(JSON.stringify(receipt, null, 2));
660
+ console.log(JSON.stringify(displayReceipt, null, 2));
402
661
  if (receipt.error_count > 0) {
403
662
  throw new errors_1.ValidationError(`validation failed with ${receipt.error_count} error(s)`);
404
663
  }
405
664
  return;
406
665
  }
407
666
  if (!options.quiet) {
408
- for (const warning of receipt.warnings) {
667
+ for (const warning of displayReceipt.warnings) {
409
668
  console.error(`warning: ${warning}`);
410
669
  }
670
+ if (displayReceipt.warning_summary.truncated) {
671
+ console.error(`warning summary: omitted ${displayReceipt.warning_summary.omitted_count} warning(s); rerun without --summary or use --json-out <path> for full diagnostics`);
672
+ }
411
673
  }
412
674
  if (receipt.error_count > 0) {
413
675
  if (receipt.report_path) {
@@ -11,6 +11,7 @@ exports.runWorkTriggerCommand = runWorkTriggerCommand;
11
11
  exports.runWorkReceiptNewCommand = runWorkReceiptNewCommand;
12
12
  exports.runWorkReceiptUpdateCommand = runWorkReceiptUpdateCommand;
13
13
  exports.runWorkReceiptVerifyCommand = runWorkReceiptVerifyCommand;
14
+ exports.runWorkValidateCommand = runWorkValidateCommand;
14
15
  exports.runWorkArtifactAddCommand = runWorkArtifactAddCommand;
15
16
  const fs_1 = __importDefault(require("fs"));
16
17
  const path_1 = __importDefault(require("path"));
@@ -34,11 +35,15 @@ const archive_1 = require("./archive");
34
35
  const project_db_1 = require("../core/project_db");
35
36
  const project_db_migrations_1 = require("../core/project_db_migrations");
36
37
  const project_db_queue_1 = require("../core/project_db_queue");
38
+ const validate_1 = require("./validate");
39
+ const query_output_1 = require("./query_output");
40
+ const workspace_files_1 = require("../graph/workspace_files");
37
41
  const PRICING_MODELS = new Set(["free", "included", "quoted", "fixed", "metered", "subscription"]);
38
42
  const ORDER_STATUSES = new Set(["submitted", "accepted", "running", "completed", "cancelled", "failed"]);
39
43
  const RECEIPT_STATUSES = new Set(["recorded", "verified", "rejected", "superseded"]);
40
44
  const OUTCOMES = new Set(["success", "partial", "failure"]);
41
45
  const REDACTION_POLICIES = new Set(["refs_and_hashes_only", "redacted_summary", "external_private"]);
46
+ const WORKFLOW_VALIDATE_TYPES = new Set(agent_file_types_1.AGENT_FILE_TYPES);
42
47
  function parseCsvList(raw) {
43
48
  if (!raw) {
44
49
  return [];
@@ -210,6 +215,166 @@ function resolveReadableWorkNode(index, idOrQid, ws, type, label) {
210
215
  }
211
216
  return node;
212
217
  }
218
+ function normalizeWorkflowValidateType(value) {
219
+ if (value === undefined) {
220
+ return undefined;
221
+ }
222
+ const normalized = value.toLowerCase();
223
+ if (!WORKFLOW_VALIDATE_TYPES.has(normalized)) {
224
+ throw new errors_1.UsageError(`--type must be one of ${agent_file_types_1.AGENT_FILE_TYPES.join(", ")}`);
225
+ }
226
+ return normalized;
227
+ }
228
+ function isWorkflowNode(node) {
229
+ return WORKFLOW_VALIDATE_TYPES.has(node.type);
230
+ }
231
+ function resolveWorkflowTarget(index, idOrQid, ws) {
232
+ const resolved = (0, qid_1.resolveQid)(index, idOrQid, ws);
233
+ if (resolved.status !== "ok") {
234
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("workflow record", idOrQid, resolved, ws));
235
+ }
236
+ const node = index.nodes[resolved.qid];
237
+ if (!node || !isWorkflowNode(node)) {
238
+ throw new errors_1.NotFoundError(`workflow record not found: ${idOrQid}`);
239
+ }
240
+ return node;
241
+ }
242
+ function workflowDiagnosticCode(message) {
243
+ const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
244
+ if (rawMatch) {
245
+ return `raw-content.${rawMatch[1]}`;
246
+ }
247
+ if (message.includes("missing recommended heading")) {
248
+ return "heading.missing";
249
+ }
250
+ const manifestCompatMatch = /manifest\.compat\.([a-z_]+)/.exec(message);
251
+ if (manifestCompatMatch) {
252
+ return `manifest.compat.${manifestCompatMatch[1]}`;
253
+ }
254
+ if (message.includes("references missing") || message.includes("references missing node")) {
255
+ return "reference.missing";
256
+ }
257
+ if (message.includes("must be named")) {
258
+ return "schema.basename";
259
+ }
260
+ if (message.includes("must be") || message.includes("is required")) {
261
+ return "schema.invalid";
262
+ }
263
+ if (message.includes("visibility:")) {
264
+ return "visibility.policy";
265
+ }
266
+ return "validation.message";
267
+ }
268
+ function workflowDiagnosticQid(message, nodes) {
269
+ for (const node of nodes) {
270
+ if (message.includes(node.qid) || message.includes(node.path)) {
271
+ return node.qid;
272
+ }
273
+ }
274
+ return undefined;
275
+ }
276
+ function workflowCandidatePaths(options) {
277
+ const values = new Set();
278
+ if (options.target) {
279
+ values.add(options.target.path);
280
+ values.add(path_1.default.resolve(options.root, options.target.path));
281
+ return Array.from(values);
282
+ }
283
+ const basenames = options.type
284
+ ? new Set([agent_file_types_1.AGENT_FILE_BASENAMES[options.type]])
285
+ : new Set(Object.values(agent_file_types_1.AGENT_FILE_BASENAMES));
286
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, options.config);
287
+ for (const [alias, files] of Object.entries(filesByAlias)) {
288
+ if (options.ws !== "root" && alias !== options.ws) {
289
+ continue;
290
+ }
291
+ for (const filePath of files) {
292
+ if (!basenames.has(path_1.default.basename(filePath))) {
293
+ continue;
294
+ }
295
+ values.add(filePath);
296
+ values.add(path_1.default.relative(options.root, filePath).split(path_1.default.sep).join("/"));
297
+ }
298
+ }
299
+ return Array.from(values);
300
+ }
301
+ function filterWorkflowMessages(messages, nodes, candidatePaths) {
302
+ if (nodes.length === 0 && candidatePaths.length === 0) {
303
+ return [];
304
+ }
305
+ return messages.filter((message) => nodes.some((node) => message.includes(node.qid) || message.includes(node.path)) ||
306
+ candidatePaths.some((filePath) => message.includes(filePath)));
307
+ }
308
+ function buildWorkValidateReceipt(options) {
309
+ const config = (0, config_1.loadConfig)(options.root);
310
+ const ws = normalizeWorkspace(options.ws);
311
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, tolerant: true });
312
+ const type = normalizeWorkflowValidateType(options.type);
313
+ let targets;
314
+ let target;
315
+ if (options.id) {
316
+ target = resolveWorkflowTarget(index, options.id, ws);
317
+ if (type && target.type !== type) {
318
+ throw new errors_1.UsageError(`workflow record ${target.qid} is ${target.type}, not ${type}`);
319
+ }
320
+ targets = [target];
321
+ }
322
+ else {
323
+ targets = Object.values(index.nodes)
324
+ .filter((node) => isWorkflowNode(node) && (!type || node.type === type))
325
+ .sort((a, b) => a.qid.localeCompare(b.qid));
326
+ }
327
+ const candidatePaths = workflowCandidatePaths({ root: options.root, config, ws, type, target });
328
+ const validation = (0, validate_1.collectValidateReceipt)({ root: options.root });
329
+ const warnings = filterWorkflowMessages(validation.warnings, targets, candidatePaths);
330
+ const errors = filterWorkflowMessages(validation.errors, targets, candidatePaths);
331
+ const diagnostics = [
332
+ ...warnings.map((message) => ({
333
+ severity: "warning",
334
+ code: workflowDiagnosticCode(message),
335
+ message,
336
+ qid: workflowDiagnosticQid(message, targets),
337
+ })),
338
+ ...errors.map((message) => ({
339
+ severity: "error",
340
+ code: workflowDiagnosticCode(message),
341
+ message,
342
+ qid: workflowDiagnosticQid(message, targets),
343
+ })),
344
+ ];
345
+ return {
346
+ action: "work.validate",
347
+ ok: errors.length === 0,
348
+ type: type ?? "all",
349
+ ...(target ? { target: (0, query_output_1.toNodeSummaryJson)(target) } : {}),
350
+ checked_count: target ? 1 : candidatePaths.filter((value) => !path_1.default.isAbsolute(value)).length,
351
+ nodes: targets.map((node) => (0, query_output_1.toNodeSummaryJson)(node)),
352
+ warning_count: warnings.length,
353
+ error_count: errors.length,
354
+ warnings,
355
+ errors,
356
+ diagnostics,
357
+ };
358
+ }
359
+ function printWorkValidateReceipt(receipt, json) {
360
+ if (json) {
361
+ (0, query_output_1.writeJson)(receipt);
362
+ if (!receipt.ok) {
363
+ throw new errors_1.ValidationError(`workflow validation failed with ${receipt.error_count} error(s)`);
364
+ }
365
+ return;
366
+ }
367
+ for (const warning of receipt.warnings) {
368
+ console.error(`warning: ${warning}`);
369
+ }
370
+ if (!receipt.ok) {
371
+ for (const error of receipt.errors) {
372
+ console.error(error);
373
+ }
374
+ throw new errors_1.ValidationError(`workflow validation failed with ${receipt.error_count} error(s)`);
375
+ }
376
+ console.log(`workflow validation ok: ${receipt.checked_count} file(s)`);
377
+ }
213
378
  function resolveTriggerWorkNode(index, ws, refRaw) {
214
379
  const ref = normalizePortableIdRef(refRaw, "<work-or-capability-ref>");
215
380
  const resolved = (0, qid_1.resolveQid)(index, ref, ws);
@@ -223,8 +388,8 @@ function resolveTriggerWorkNode(index, ws, refRaw) {
223
388
  if (node.type === "work") {
224
389
  return { workNode: node };
225
390
  }
226
- if (node.type !== "spec") {
227
- throw new errors_1.UsageError(`work trigger requires a WORK.md or SPEC.md ref, got ${node.type}: ${node.qid}`);
391
+ if (!(0, agent_file_types_1.isManifestSemanticType)(node.type)) {
392
+ throw new errors_1.UsageError(`work trigger requires a WORK.md or MANIFEST.md/SPEC.md ref, got ${node.type}: ${node.qid}`);
228
393
  }
229
394
  const candidates = new Map();
230
395
  const specDir = path_1.default.posix.dirname(node.path);
@@ -250,11 +415,12 @@ function resolveTriggerWorkNode(index, ws, refRaw) {
250
415
  }
251
416
  }
252
417
  const workNodes = Array.from(candidates.values()).sort((a, b) => a.qid.localeCompare(b.qid));
418
+ const manifestLabel = node.type === "spec" ? "legacy SPEC.md" : "MANIFEST.md";
253
419
  if (workNodes.length === 0) {
254
- throw new errors_1.NotFoundError(`SPEC.md ${node.qid} has no resolvable WORK.md contract`);
420
+ throw new errors_1.NotFoundError(`${manifestLabel} ${node.qid} has no resolvable WORK.md contract`);
255
421
  }
256
422
  if (workNodes.length > 1) {
257
- throw new errors_1.UsageError(`SPEC.md ${node.qid} has multiple work contracts; trigger one explicitly: ${workNodes
423
+ throw new errors_1.UsageError(`${manifestLabel} ${node.qid} has multiple work contracts; trigger one explicitly: ${workNodes
258
424
  .map((workNode) => workNode.qid)
259
425
  .join(", ")}`);
260
426
  }
@@ -596,7 +762,9 @@ function runWorkContractNewCommandLocked(options) {
596
762
  const agentId = normalizePortableIdRef(options.agentId, "--agent-id");
597
763
  const kind = options.kind.toLowerCase();
598
764
  const resolvedAgent = (0, qid_1.resolveQid)(index, agentId, ws);
599
- const relates = resolvedAgent.status === "ok" && index.nodes[resolvedAgent.qid]?.type === "spec" ? [agentId] : [];
765
+ const relates = resolvedAgent.status === "ok" && (0, agent_file_types_1.isManifestSemanticType)(index.nodes[resolvedAgent.qid]?.type ?? "")
766
+ ? [agentId]
767
+ : [];
600
768
  const requiredCapabilities = parseCsvList(options.requiredCapabilities);
601
769
  const receipt = createAgentWorkflowNode({
602
770
  root: options.root,
@@ -943,6 +1111,9 @@ function runWorkReceiptUpdateCommand(options) {
943
1111
  function runWorkReceiptVerifyCommand(options) {
944
1112
  return runWorkReceiptVerifyCommandLocked(options);
945
1113
  }
1114
+ function runWorkValidateCommand(options) {
1115
+ return printWorkValidateReceipt(buildWorkValidateReceipt(options), options.json);
1116
+ }
946
1117
  function runWorkArtifactAddCommand(options) {
947
1118
  return withWorkLock(options.root, () => runWorkArtifactAddCommandLocked(options));
948
1119
  }