mdkg 0.3.7 → 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 (37) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CLI_COMMAND_MATRIX.md +81 -22
  3. package/README.md +50 -25
  4. package/dist/cli.js +70 -34
  5. package/dist/command-contract.json +354 -9
  6. package/dist/commands/capability.js +1 -0
  7. package/dist/commands/doctor.js +7 -7
  8. package/dist/commands/format.js +40 -1
  9. package/dist/commands/handoff.js +6 -0
  10. package/dist/commands/new.js +12 -3
  11. package/dist/commands/spec.js +76 -17
  12. package/dist/commands/subgraph.js +7 -4
  13. package/dist/commands/upgrade.js +70 -6
  14. package/dist/commands/validate.js +106 -3
  15. package/dist/commands/work.js +12 -5
  16. package/dist/graph/agent_file_types.js +59 -20
  17. package/dist/graph/capabilities_indexer.js +45 -3
  18. package/dist/graph/indexer.js +5 -0
  19. package/dist/graph/template_schema.js +37 -17
  20. package/dist/graph/validate_graph.js +11 -5
  21. package/dist/init/AGENT_START.md +5 -5
  22. package/dist/init/CLI_COMMAND_MATRIX.md +29 -16
  23. package/dist/init/README.md +11 -9
  24. package/dist/init/init-manifest.json +59 -4
  25. package/dist/init/templates/default/manifest.md +45 -0
  26. package/dist/init/templates/specs/agent.MANIFEST.md +80 -0
  27. package/dist/init/templates/specs/api.MANIFEST.md +33 -0
  28. package/dist/init/templates/specs/base.MANIFEST.md +120 -0
  29. package/dist/init/templates/specs/capability.MANIFEST.md +45 -0
  30. package/dist/init/templates/specs/integration.MANIFEST.md +25 -0
  31. package/dist/init/templates/specs/model.MANIFEST.md +21 -0
  32. package/dist/init/templates/specs/project.MANIFEST.md +39 -0
  33. package/dist/init/templates/specs/runtime-agent.MANIFEST.md +49 -0
  34. package/dist/init/templates/specs/runtime-image.MANIFEST.md +21 -0
  35. package/dist/init/templates/specs/tool.MANIFEST.md +25 -0
  36. package/dist/util/argparse.js +3 -0
  37. package/package.json +17 -3
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runSpecListCommand = runSpecListCommand;
4
4
  exports.runSpecShowCommand = runSpecShowCommand;
5
5
  exports.runSpecValidateCommand = runSpecValidateCommand;
6
+ exports.runManifestListCommand = runManifestListCommand;
7
+ exports.runManifestShowCommand = runManifestShowCommand;
8
+ exports.runManifestValidateCommand = runManifestValidateCommand;
6
9
  const capability_1 = require("./capability");
7
10
  const validate_1 = require("./validate");
8
11
  const errors_1 = require("../util/errors");
@@ -24,6 +27,9 @@ function loadSpecRecords(options) {
24
27
  };
25
28
  return sortSpecRecords((0, capability_1.filterCapabilityRecords)((0, capability_1.loadCapabilityRecords)(listOptions), listOptions));
26
29
  }
30
+ function commandSurface(options) {
31
+ return options.surface ?? "spec";
32
+ }
27
33
  function matchesSpecRef(record, id) {
28
34
  const normalized = id.toLowerCase();
29
35
  return (record.id === id ||
@@ -34,15 +40,15 @@ function matchesSpecRef(record, id) {
34
40
  record.path.toLowerCase() === normalized ||
35
41
  record.aliases.some((alias) => alias.toLowerCase() === normalized));
36
42
  }
37
- function resolveSpecRecord(records, id) {
43
+ function resolveSpecRecord(records, id, surface) {
38
44
  const matches = records.filter((record) => matchesSpecRef(record, id));
39
45
  if (matches.length === 1) {
40
46
  return matches[0];
41
47
  }
42
48
  if (matches.length > 1) {
43
- throw new errors_1.UsageError(`SPEC reference is ambiguous: ${id}`);
49
+ throw new errors_1.UsageError(`${surface === "manifest" ? "manifest capability" : "SPEC"} reference is ambiguous: ${id}`);
44
50
  }
45
- throw new errors_1.NotFoundError(`SPEC not found: ${id}`);
51
+ throw new errors_1.NotFoundError(`${surface === "manifest" ? "manifest capability" : "SPEC"} not found: ${id}`);
46
52
  }
47
53
  function stringValue(value) {
48
54
  return typeof value === "string" ? value : undefined;
@@ -50,33 +56,74 @@ function stringValue(value) {
50
56
  function stringArrayValue(value) {
51
57
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
52
58
  }
53
- function printSpecList(records, json) {
59
+ function compatibilityLabel(record) {
60
+ const mode = record.manifest?.compatibility_mode;
61
+ if (mode === "legacy") {
62
+ return "legacy SPEC.md";
63
+ }
64
+ if (mode === "transitional") {
65
+ return "transitional MANIFEST.md type: spec";
66
+ }
67
+ return "MANIFEST.md";
68
+ }
69
+ function legacySpecDeprecation() {
70
+ return "mdkg spec is a legacy alias for mdkg manifest during the one-compatibility-release bridge.";
71
+ }
72
+ function printSpecList(records, json, surface = "spec") {
54
73
  if (json) {
55
- console.log(JSON.stringify({
56
- kind: "spec",
57
- count: records.length,
58
- items: records,
59
- }, null, 2));
74
+ const receipt = surface === "manifest"
75
+ ? {
76
+ kind: "manifest",
77
+ compatibility_kind: "spec",
78
+ count: records.length,
79
+ items: records,
80
+ }
81
+ : {
82
+ kind: "spec",
83
+ canonical_kind: "manifest",
84
+ legacy_alias: true,
85
+ deprecation: legacySpecDeprecation(),
86
+ count: records.length,
87
+ items: records,
88
+ };
89
+ console.log(JSON.stringify(receipt, null, 2));
60
90
  return;
61
91
  }
92
+ if (surface === "spec") {
93
+ console.log(legacySpecDeprecation());
94
+ }
62
95
  if (records.length === 0) {
63
- console.log("no SPEC.md capabilities found");
96
+ console.log("no MANIFEST.md/SPEC.md capabilities found");
64
97
  return;
65
98
  }
66
- console.log(`SPEC.md capabilities: ${records.length}`);
99
+ console.log(`${surface === "manifest" ? "MANIFEST.md" : "MANIFEST.md/SPEC.md"} capabilities: ${records.length}`);
67
100
  for (const record of records) {
68
101
  const kind = stringValue(record.spec?.spec_kind);
69
102
  const kindLabel = kind ? ` | ${kind}` : "";
70
- console.log(`${record.qid} | ${record.visibility}${kindLabel} | ${record.title}`);
103
+ console.log(`${record.qid} | ${record.visibility}${kindLabel} | ${compatibilityLabel(record)} | ${record.title}`);
71
104
  }
72
105
  }
73
- function printSpec(record, json) {
106
+ function printSpec(record, json, surface = "spec") {
74
107
  if (json) {
75
- console.log(JSON.stringify({ kind: "spec", item: record }, null, 2));
108
+ const receipt = surface === "manifest"
109
+ ? { kind: "manifest", compatibility_kind: "spec", item: record }
110
+ : {
111
+ kind: "spec",
112
+ canonical_kind: "manifest",
113
+ legacy_alias: true,
114
+ deprecation: legacySpecDeprecation(),
115
+ item: record,
116
+ };
117
+ console.log(JSON.stringify(receipt, null, 2));
76
118
  return;
77
119
  }
120
+ if (surface === "spec") {
121
+ console.log(legacySpecDeprecation());
122
+ }
78
123
  console.log(`${record.qid} | ${record.visibility}`);
79
124
  console.log(`title: ${record.title}`);
125
+ console.log(`semantic_kind: ${record.manifest?.semantic_kind ?? "manifest"}`);
126
+ console.log(`compatibility: ${compatibilityLabel(record)}`);
80
127
  const specKind = stringValue(record.spec?.spec_kind);
81
128
  if (specKind) {
82
129
  console.log(`spec_kind: ${specKind}`);
@@ -88,14 +135,26 @@ function printSpec(record, json) {
88
135
  }
89
136
  }
90
137
  function runSpecListCommand(options) {
91
- printSpecList(loadSpecRecords(options), options.json);
138
+ const surface = commandSurface(options);
139
+ printSpecList(loadSpecRecords(options), options.json, surface);
92
140
  }
93
141
  function runSpecShowCommand(options) {
94
- printSpec(resolveSpecRecord(loadSpecRecords(options), options.id), options.json);
142
+ const surface = commandSurface(options);
143
+ printSpec(resolveSpecRecord(loadSpecRecords(options), options.id, surface), options.json, surface);
95
144
  }
96
145
  function runSpecValidateCommand(options) {
146
+ const surface = commandSurface(options);
97
147
  if (options.id) {
98
- resolveSpecRecord(loadSpecRecords(options), options.id);
148
+ resolveSpecRecord(loadSpecRecords(options), options.id, surface);
99
149
  }
100
150
  (0, validate_1.runValidateCommand)({ root: options.root, json: options.json });
101
151
  }
152
+ function runManifestListCommand(options) {
153
+ runSpecListCommand({ ...options, surface: "manifest" });
154
+ }
155
+ function runManifestShowCommand(options) {
156
+ runSpecShowCommand({ ...options, surface: "manifest" });
157
+ }
158
+ function runManifestValidateCommand(options) {
159
+ runSpecValidateCommand({ ...options, surface: "manifest" });
160
+ }
@@ -323,6 +323,9 @@ function runSubgraphVerifyCommand(options) {
323
323
  throw new errors_1.ValidationError("subgraph verify failed");
324
324
  }
325
325
  }
326
+ function dirtySourceGuidance(paths) {
327
+ return `source_path has dirty tracked changes: ${paths.join(", ")}; commit or stash the child repo first, then refresh the root-owned subgraph bundle from the clean accepted child commit`;
328
+ }
326
329
  function pushAuditCheck(receipt, check) {
327
330
  receipt.checks.push(check);
328
331
  if (!check.ok && check.severity === "error") {
@@ -349,7 +352,7 @@ function auditOneAlias(options) {
349
352
  source_repo: options.health.source_repo,
350
353
  capability_summary: {
351
354
  node_count: options.nodeTypes.length,
352
- spec_count: options.nodeTypes.filter((type) => type === "spec").length,
355
+ spec_count: options.nodeTypes.filter((type) => type === "manifest" || type === "spec").length,
353
356
  work_count: options.nodeTypes.filter((type) => type === "work").length,
354
357
  skill_count: options.nodeTypes.filter((type) => type === "skill").length,
355
358
  },
@@ -392,7 +395,7 @@ function auditOneAlias(options) {
392
395
  ok: !gitState.dirtyTracked,
393
396
  severity: "warning",
394
397
  message: gitState.dirtyTracked
395
- ? `source_path has dirty tracked changes: ${gitState.dirtyTrackedPaths.join(", ")}`
398
+ ? dirtySourceGuidance(gitState.dirtyTrackedPaths)
396
399
  : "source_path has no dirty tracked changes",
397
400
  path: options.subgraph.source_path,
398
401
  details: { dirty_tracked_paths: gitState.dirtyTrackedPaths },
@@ -539,7 +542,7 @@ function upgradePlanForAlias(options) {
539
542
  blockers.push(sourceGitError.message);
540
543
  }
541
544
  if (options.audit.dirty_tracked) {
542
- blockers.push(`source_path has dirty tracked changes: ${options.audit.dirty_tracked_paths?.join(", ")}`);
545
+ blockers.push(dirtySourceGuidance(options.audit.dirty_tracked_paths ?? []));
543
546
  }
544
547
  const rootOwnedErrors = options.audit.checks.filter((check) => check.id === "subgraph.bundle.root_owned" && !check.ok);
545
548
  blockers.push(...rootOwnedErrors.map((check) => check.message));
@@ -705,7 +708,7 @@ function inspectSourcePath(root, alias, subgraph, allowDirty) {
705
708
  ? status.stdout.split(/\r?\n/).map((line) => line.slice(3)).filter(Boolean).sort()
706
709
  : [];
707
710
  if (dirtyTrackedPaths.length > 0 && !allowDirty) {
708
- throw new errors_1.UsageError(`subgraph ${alias} source_path has dirty tracked changes: ${dirtyTrackedPaths.join(", ")}`);
711
+ throw new errors_1.UsageError(`subgraph ${alias} ${dirtySourceGuidance(dirtyTrackedPaths)}`);
709
712
  }
710
713
  return {
711
714
  sourceRoot,
@@ -15,6 +15,8 @@ const project_db_1 = require("../core/project_db");
15
15
  const errors_1 = require("../util/errors");
16
16
  const frontmatter_1 = require("../graph/frontmatter");
17
17
  const workspace_files_1 = require("../graph/workspace_files");
18
+ const agent_file_types_1 = require("../graph/agent_file_types");
19
+ const atomic_1 = require("../util/atomic");
18
20
  const init_manifest_1 = require("./init_manifest");
19
21
  const skill_support_1 = require("./skill_support");
20
22
  const skill_mirror_1 = require("./skill_mirror");
@@ -65,6 +67,13 @@ function writeFile(filePath, content) {
65
67
  fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
66
68
  fs_1.default.writeFileSync(filePath, content, "utf8");
67
69
  }
70
+ function repoRelativePath(root, filePath) {
71
+ return path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
72
+ }
73
+ function renderFrontmatterDocument(frontmatter, body) {
74
+ const frontmatterBlock = ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---"].join("\n");
75
+ return body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
76
+ }
68
77
  function createSummary() {
69
78
  return {
70
79
  created: 0,
@@ -323,9 +332,59 @@ function migrateClosedGoalActiveNodes(root, dryRun, summary, changes) {
323
332
  frontmatter.last_active_node = activeNode;
324
333
  }
325
334
  delete frontmatter.active_node;
326
- const frontmatterBlock = ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---"].join("\n");
327
- const body = parsed.body.length > 0 ? parsed.body : "";
328
- writeFile(filePath, body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock);
335
+ writeFile(filePath, renderFrontmatterDocument(frontmatter, parsed.body.length > 0 ? parsed.body : ""));
336
+ }
337
+ }
338
+ }
339
+ if (planned === 0) {
340
+ summary.unchanged += 1;
341
+ }
342
+ }
343
+ function migrateLegacySpecManifests(root, dryRun, summary, changes) {
344
+ const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
345
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
346
+ let planned = 0;
347
+ for (const files of Object.values(filesByAlias)) {
348
+ for (const filePath of files) {
349
+ if (path_1.default.basename(filePath) !== agent_file_types_1.LEGACY_SPEC_BASENAME) {
350
+ continue;
351
+ }
352
+ const content = fs_1.default.readFileSync(filePath, "utf8");
353
+ let parsed;
354
+ try {
355
+ parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
356
+ }
357
+ catch {
358
+ continue;
359
+ }
360
+ if (parsed.frontmatter.type !== "spec") {
361
+ continue;
362
+ }
363
+ planned += 1;
364
+ const targetPath = path_1.default.join(path_1.default.dirname(filePath), agent_file_types_1.CANONICAL_MANIFEST_BASENAME);
365
+ const relativePath = repoRelativePath(root, filePath);
366
+ const relativeTargetPath = repoRelativePath(root, targetPath);
367
+ if (fs_1.default.existsSync(targetPath)) {
368
+ record(summary, changes, {
369
+ path: relativePath,
370
+ target_path: relativeTargetPath,
371
+ category: "manifest_migration",
372
+ action: "conflict",
373
+ reason: "sibling MANIFEST.md already exists; legacy SPEC.md content preserved",
374
+ });
375
+ continue;
376
+ }
377
+ record(summary, changes, {
378
+ path: relativePath,
379
+ target_path: relativeTargetPath,
380
+ category: "manifest_migration",
381
+ action: "migrate",
382
+ reason: "rename legacy SPEC.md to MANIFEST.md and normalize type: spec to type: manifest",
383
+ });
384
+ if (!dryRun) {
385
+ const frontmatter = { ...parsed.frontmatter, type: "manifest" };
386
+ (0, atomic_1.atomicWriteFile)(filePath, renderFrontmatterDocument(frontmatter, parsed.body));
387
+ fs_1.default.renameSync(filePath, targetPath);
329
388
  }
330
389
  }
331
390
  }
@@ -422,6 +481,9 @@ function ensureArchiveIgnorePolicy(root, dryRun, summary, changes) {
422
481
  function isWritableChange(change) {
423
482
  return change.action === "create" || change.action === "update" || change.action === "migrate" || change.action === "sync";
424
483
  }
484
+ function writablePathForChange(change) {
485
+ return change.target_path ?? change.path;
486
+ }
425
487
  function buildApplySideEffects(options) {
426
488
  const hasDirectWrites = options.changes.some(isWritableChange);
427
489
  if (!hasDirectWrites && options.existingManifest) {
@@ -468,7 +530,8 @@ function emitHumanReceipt(receipt) {
468
530
  }
469
531
  else {
470
532
  for (const change of receipt.changes) {
471
- console.log(` ${change.action}: ${change.path} (${change.reason})`);
533
+ const target = change.target_path ? ` -> ${change.target_path}` : "";
534
+ console.log(` ${change.action}: ${change.path}${target} (${change.reason})`);
472
535
  }
473
536
  }
474
537
  if (receipt.apply_side_effects.length > 0) {
@@ -501,6 +564,7 @@ function runUpgradeCommand(options) {
501
564
  const managedCurrentFiles = [];
502
565
  migrateConfigIfNeeded(root, dryRun, summary, changes);
503
566
  migrateClosedGoalActiveNodes(root, dryRun, summary, changes);
567
+ migrateLegacySpecManifests(root, dryRun, summary, changes);
504
568
  for (const file of currentManifest.files) {
505
569
  if (!shouldIncludeFile(file, agentWorkspace)) {
506
570
  continue;
@@ -544,8 +608,8 @@ function runUpgradeCommand(options) {
544
608
  }
545
609
  }
546
610
  const preservedCustomizations = changes.filter((change) => change.action === "conflict");
547
- const blockingConflicts = [];
548
- const willWritePaths = changes.filter(isWritableChange).map((change) => change.path);
611
+ const blockingConflicts = changes.filter((change) => change.action === "conflict" && change.category === "manifest_migration");
612
+ const willWritePaths = changes.filter(isWritableChange).map(writablePathForChange);
549
613
  const receipt = {
550
614
  action: "upgrade",
551
615
  dry_run: dryRun,
@@ -129,6 +129,20 @@ function collectRawContentWarnings(qid, node) {
129
129
  }
130
130
  return warnings;
131
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
+ }
132
146
  function collectChangedPaths(root) {
133
147
  const result = (0, child_process_1.spawnSync)("git", ["-C", root, "status", "--porcelain", "--", ".mdkg"], {
134
148
  encoding: "utf8",
@@ -166,6 +180,7 @@ function warningPath(message, nodes) {
166
180
  }
167
181
  function warningDiagnostic(message, nodes) {
168
182
  const qid = qidFromWarning(message);
183
+ const nodeType = qid && nodes[qid] ? nodes[qid].type : undefined;
169
184
  const pathValue = warningPath(message, nodes);
170
185
  const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
171
186
  if (rawMatch) {
@@ -175,6 +190,7 @@ function warningDiagnostic(message, nodes) {
175
190
  severity: "warning",
176
191
  message,
177
192
  qid,
193
+ node_type: nodeType,
178
194
  path: pathValue,
179
195
  ref: qid,
180
196
  remediation: "Replace raw secrets, prompts, tokens, or payloads with refs, hashes, redacted summaries, or archive/artifact links.",
@@ -187,6 +203,7 @@ function warningDiagnostic(message, nodes) {
187
203
  severity: "warning",
188
204
  message,
189
205
  qid,
206
+ node_type: nodeType,
190
207
  path: pathValue,
191
208
  ref: qid,
192
209
  remediation: "Run mdkg format --headings --dry-run to review missing heading additions, then --apply if acceptable.",
@@ -198,16 +215,32 @@ function warningDiagnostic(message, nodes) {
198
215
  category: "templates",
199
216
  severity: "warning",
200
217
  message,
218
+ node_type: nodeType,
201
219
  path: pathValue,
202
220
  remediation: "Run mdkg upgrade --apply to vendor missing built-in template schemas when the managed asset update is safe.",
203
221
  };
204
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
+ }
205
237
  if (message.includes("sqlite") || message.includes("index")) {
206
238
  return {
207
239
  id: "cache.index",
208
240
  category: "cache",
209
241
  severity: "warning",
210
242
  message,
243
+ node_type: nodeType,
211
244
  path: pathValue,
212
245
  remediation: "Run mdkg index or mdkg db index rebuild when generated cache state should be refreshed.",
213
246
  };
@@ -218,6 +251,7 @@ function warningDiagnostic(message, nodes) {
218
251
  category: "skills",
219
252
  severity: "warning",
220
253
  message,
254
+ node_type: nodeType,
221
255
  path: pathValue,
222
256
  remediation: "Run mdkg skill sync after reviewing managed skill mirror drift.",
223
257
  };
@@ -228,6 +262,7 @@ function warningDiagnostic(message, nodes) {
228
262
  category: "subgraph",
229
263
  severity: "warning",
230
264
  message,
265
+ node_type: nodeType,
231
266
  path: pathValue,
232
267
  remediation: "Run mdkg subgraph verify or refresh the source bundle after reviewing child graph freshness.",
233
268
  };
@@ -238,11 +273,67 @@ function warningDiagnostic(message, nodes) {
238
273
  severity: "warning",
239
274
  message,
240
275
  qid,
276
+ node_type: nodeType,
241
277
  path: pathValue,
242
278
  ref: qid,
243
279
  remediation: "Review the warning and apply the focused mdkg command suggested by the message when appropriate.",
244
280
  };
245
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
+ }
246
337
  function isCoreListFile(filePath) {
247
338
  return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
248
339
  }
@@ -412,6 +503,7 @@ function collectValidateReceipt(options) {
412
503
  const idsByWorkspace = {};
413
504
  for (const [alias, files] of Object.entries(filesByAlias)) {
414
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("/") || "."));
415
507
  for (const filePath of files) {
416
508
  if (isCoreListFile(filePath)) {
417
509
  continue;
@@ -450,6 +542,7 @@ function collectValidateReceipt(options) {
450
542
  }
451
543
  }
452
544
  warnings.push(...collectRawContentWarnings(qid, node));
545
+ warnings.push(...collectManifestCompatibilityWarnings(qid, filePath, node));
453
546
  }
454
547
  catch (err) {
455
548
  const message = err instanceof Error ? err.message : "unknown error";
@@ -546,6 +639,7 @@ function collectValidateReceipt(options) {
546
639
  error_count: uniqueErrors.length,
547
640
  warnings: uniqueWarnings,
548
641
  warning_diagnostics: filteredWarningDiagnostics,
642
+ warning_summary: buildWarningSummary(filteredWarningDiagnostics),
549
643
  errors: uniqueErrors,
550
644
  ...(outPath ? { report_path: outPath } : {}),
551
645
  ...(options.changedOnly
@@ -555,18 +649,27 @@ function collectValidateReceipt(options) {
555
649
  return receipt;
556
650
  }
557
651
  function runValidateCommand(options) {
558
- 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;
559
659
  if (options.json) {
560
- console.log(JSON.stringify(receipt, null, 2));
660
+ console.log(JSON.stringify(displayReceipt, null, 2));
561
661
  if (receipt.error_count > 0) {
562
662
  throw new errors_1.ValidationError(`validation failed with ${receipt.error_count} error(s)`);
563
663
  }
564
664
  return;
565
665
  }
566
666
  if (!options.quiet) {
567
- for (const warning of receipt.warnings) {
667
+ for (const warning of displayReceipt.warnings) {
568
668
  console.error(`warning: ${warning}`);
569
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
+ }
570
673
  }
571
674
  if (receipt.error_count > 0) {
572
675
  if (receipt.report_path) {
@@ -247,6 +247,10 @@ function workflowDiagnosticCode(message) {
247
247
  if (message.includes("missing recommended heading")) {
248
248
  return "heading.missing";
249
249
  }
250
+ const manifestCompatMatch = /manifest\.compat\.([a-z_]+)/.exec(message);
251
+ if (manifestCompatMatch) {
252
+ return `manifest.compat.${manifestCompatMatch[1]}`;
253
+ }
250
254
  if (message.includes("references missing") || message.includes("references missing node")) {
251
255
  return "reference.missing";
252
256
  }
@@ -384,8 +388,8 @@ function resolveTriggerWorkNode(index, ws, refRaw) {
384
388
  if (node.type === "work") {
385
389
  return { workNode: node };
386
390
  }
387
- if (node.type !== "spec") {
388
- 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}`);
389
393
  }
390
394
  const candidates = new Map();
391
395
  const specDir = path_1.default.posix.dirname(node.path);
@@ -411,11 +415,12 @@ function resolveTriggerWorkNode(index, ws, refRaw) {
411
415
  }
412
416
  }
413
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";
414
419
  if (workNodes.length === 0) {
415
- 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`);
416
421
  }
417
422
  if (workNodes.length > 1) {
418
- 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
419
424
  .map((workNode) => workNode.qid)
420
425
  .join(", ")}`);
421
426
  }
@@ -757,7 +762,9 @@ function runWorkContractNewCommandLocked(options) {
757
762
  const agentId = normalizePortableIdRef(options.agentId, "--agent-id");
758
763
  const kind = options.kind.toLowerCase();
759
764
  const resolvedAgent = (0, qid_1.resolveQid)(index, agentId, ws);
760
- 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
+ : [];
761
768
  const requiredCapabilities = parseCsvList(options.requiredCapabilities);
762
769
  const receipt = createAgentWorkflowNode({
763
770
  root: options.root,