mdkg 0.3.7 → 0.3.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 (53) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CLI_COMMAND_MATRIX.md +85 -24
  3. package/README.md +51 -26
  4. package/dist/cli.js +72 -35
  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/init.js +84 -3
  11. package/dist/commands/new.js +12 -3
  12. package/dist/commands/skill.js +1 -1
  13. package/dist/commands/skill_mirror.js +22 -17
  14. package/dist/commands/skill_support.js +1 -1
  15. package/dist/commands/spec.js +76 -17
  16. package/dist/commands/subgraph.js +7 -4
  17. package/dist/commands/upgrade.js +137 -30
  18. package/dist/commands/validate.js +106 -3
  19. package/dist/commands/work.js +12 -5
  20. package/dist/core/config.js +132 -0
  21. package/dist/graph/agent_file_types.js +59 -20
  22. package/dist/graph/capabilities_indexer.js +45 -3
  23. package/dist/graph/indexer.js +5 -0
  24. package/dist/graph/template_schema.js +37 -17
  25. package/dist/graph/validate_graph.js +11 -5
  26. package/dist/init/AGENT_START.md +10 -9
  27. package/dist/init/CLI_COMMAND_MATRIX.md +30 -17
  28. package/dist/init/README.md +11 -9
  29. package/dist/init/config.json +15 -0
  30. package/dist/init/core/COLLABORATION.md +45 -0
  31. package/dist/init/core/HUMAN.md +7 -1
  32. package/dist/init/core/core.md +1 -0
  33. package/dist/init/core/rule-1-mdkg-conventions.md +3 -0
  34. package/dist/init/core/rule-3-cli-contract.md +5 -5
  35. package/dist/init/init-manifest.json +78 -13
  36. package/dist/init/llms.txt +2 -1
  37. package/dist/init/skills/default/author-mdkg-skill/SKILL.md +211 -0
  38. package/dist/init/skills/default/pursue-mdkg-goal/SKILL.md +10 -0
  39. package/dist/init/skills/default/select-work-and-ground-context/SKILL.md +8 -0
  40. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +73 -9
  41. package/dist/init/templates/default/manifest.md +45 -0
  42. package/dist/init/templates/specs/agent.MANIFEST.md +80 -0
  43. package/dist/init/templates/specs/api.MANIFEST.md +33 -0
  44. package/dist/init/templates/specs/base.MANIFEST.md +120 -0
  45. package/dist/init/templates/specs/capability.MANIFEST.md +45 -0
  46. package/dist/init/templates/specs/integration.MANIFEST.md +25 -0
  47. package/dist/init/templates/specs/model.MANIFEST.md +21 -0
  48. package/dist/init/templates/specs/project.MANIFEST.md +39 -0
  49. package/dist/init/templates/specs/runtime-agent.MANIFEST.md +49 -0
  50. package/dist/init/templates/specs/runtime-image.MANIFEST.md +21 -0
  51. package/dist/init/templates/specs/tool.MANIFEST.md +25 -0
  52. package/dist/util/argparse.js +3 -0
  53. package/package.json +19 -3
@@ -15,11 +15,13 @@ 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");
21
23
  const DEFAULT_SEED_SUBDIR = path_1.default.resolve(__dirname, "..", "init");
22
- const PROTECTED_CORE_DOCS = new Set([".mdkg/core/SOUL.md", ".mdkg/core/HUMAN.md"]);
24
+ const PROTECTED_CORE_DOCS = new Set([".mdkg/core/SOUL.md", ".mdkg/core/COLLABORATION.md", ".mdkg/core/HUMAN.md"]);
23
25
  const CREATE_ONLY_PRESERVED = new Set([".mdkg/core/core.md"]);
24
26
  const LOCAL_STATE_IGNORE_ENTRIES = [
25
27
  ".mdkg/state/",
@@ -49,12 +51,11 @@ function requireSeedAssets(seedRoot) {
49
51
  }
50
52
  }
51
53
  }
52
- function isAgentWorkspace(root) {
54
+ function isAgentWorkspace(root, config) {
53
55
  return [
54
56
  path_1.default.join(root, ".mdkg", "skills"),
55
- path_1.default.join(root, ".agents", "skills"),
56
- path_1.default.join(root, ".claude", "skills"),
57
57
  path_1.default.join(root, ".mdkg", "work", "events", "events.jsonl"),
58
+ ...(0, skill_mirror_1.configuredSkillMirrorTargets)(config).map((target) => path_1.default.join(root, target)),
58
59
  ].some((candidate) => fs_1.default.existsSync(candidate));
59
60
  }
60
61
  function copyFile(src, dest) {
@@ -65,6 +66,13 @@ function writeFile(filePath, content) {
65
66
  fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
66
67
  fs_1.default.writeFileSync(filePath, content, "utf8");
67
68
  }
69
+ function repoRelativePath(root, filePath) {
70
+ return path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
71
+ }
72
+ function renderFrontmatterDocument(frontmatter, body) {
73
+ const frontmatterBlock = ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---"].join("\n");
74
+ return body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
75
+ }
68
76
  function createSummary() {
69
77
  return {
70
78
  created: 0,
@@ -148,15 +156,6 @@ function planSeedFile(options) {
148
156
  options.summary.unchanged += 1;
149
157
  return false;
150
158
  }
151
- if (PROTECTED_CORE_DOCS.has(options.file.path)) {
152
- record(options.summary, options.changes, {
153
- path: options.file.path,
154
- category: options.file.category,
155
- action: "conflict",
156
- reason: "protected core document exists; local content preserved",
157
- });
158
- return false;
159
- }
160
159
  const known = options.knownHashes.get(options.file.path);
161
160
  if (known?.has(currentHash)) {
162
161
  record(options.summary, options.changes, {
@@ -171,6 +170,15 @@ function planSeedFile(options) {
171
170
  options.managedCurrentFiles.push(options.file);
172
171
  return true;
173
172
  }
173
+ if (PROTECTED_CORE_DOCS.has(options.file.path)) {
174
+ record(options.summary, options.changes, {
175
+ path: options.file.path,
176
+ category: options.file.category,
177
+ action: "conflict",
178
+ reason: "protected core document exists; local content preserved",
179
+ });
180
+ return false;
181
+ }
174
182
  record(options.summary, options.changes, {
175
183
  path: options.file.path,
176
184
  category: options.file.category,
@@ -244,14 +252,26 @@ function migrateProjectDbConfig(input) {
244
252
  };
245
253
  return { config: raw, changed: true };
246
254
  }
255
+ function migrateCustomizationConfig(input) {
256
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
257
+ return { config: input, changed: false };
258
+ }
259
+ const raw = { ...input };
260
+ if (raw.customization !== undefined) {
261
+ return { config: raw, changed: false };
262
+ }
263
+ raw.customization = (0, config_1.defaultCustomizationConfig)();
264
+ return { config: raw, changed: true };
265
+ }
247
266
  function migrateConfigIfNeeded(root, dryRun, summary, changes) {
248
267
  const cfgPath = (0, paths_1.configPath)(root);
249
268
  const raw = JSON.parse(fs_1.default.readFileSync(cfgPath, "utf8"));
250
269
  const migrated = (0, migrate_1.migrateConfig)(raw);
251
270
  const bundleConfig = migrateLegacyBundleImportsConfig(migrated.config);
252
271
  const nextConfig = migrateProjectDbConfig(bundleConfig.config);
253
- (0, config_1.validateConfigSchema)(nextConfig.config);
254
- if (migrated.from === migrated.to && !bundleConfig.changed && !nextConfig.changed) {
272
+ const customizationConfig = migrateCustomizationConfig(nextConfig.config);
273
+ (0, config_1.validateConfigSchema)(customizationConfig.config);
274
+ if (migrated.from === migrated.to && !bundleConfig.changed && !nextConfig.changed && !customizationConfig.changed) {
255
275
  summary.unchanged += 1;
256
276
  return;
257
277
  }
@@ -262,6 +282,9 @@ function migrateConfigIfNeeded(root, dryRun, summary, changes) {
262
282
  if (nextConfig.changed) {
263
283
  reasons.push("project db config defaults");
264
284
  }
285
+ if (customizationConfig.changed) {
286
+ reasons.push("customization overlay defaults");
287
+ }
265
288
  if (migrated.from !== migrated.to) {
266
289
  reasons.push(`schema_version ${migrated.from} -> ${migrated.to}`);
267
290
  }
@@ -272,9 +295,32 @@ function migrateConfigIfNeeded(root, dryRun, summary, changes) {
272
295
  reason: reasons.join(" and "),
273
296
  });
274
297
  if (!dryRun) {
275
- writeFile(cfgPath, `${JSON.stringify(nextConfig.config, null, 2)}\n`);
298
+ writeFile(cfgPath, `${JSON.stringify(customizationConfig.config, null, 2)}\n`);
276
299
  }
277
300
  }
301
+ function sameStringArray(left, right) {
302
+ return left.length === right.length && left.every((value, index) => value === right[index]);
303
+ }
304
+ function hasOperatorCustomization(customization) {
305
+ const defaults = (0, config_1.defaultCustomizationConfig)();
306
+ return (customization.standards.profile !== defaults.standards.profile ||
307
+ customization.standards.refs.length > 0 ||
308
+ customization.core_docs.custom_paths.length > 0 ||
309
+ !sameStringArray(customization.skill_mirrors.targets, defaults.skill_mirrors.targets));
310
+ }
311
+ function reportPreservedCustomizationOverlay(root, summary, changes) {
312
+ const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
313
+ if (!hasOperatorCustomization(config.customization)) {
314
+ summary.unchanged += 1;
315
+ return;
316
+ }
317
+ record(summary, changes, {
318
+ path: ".mdkg/config.json",
319
+ category: "customization_overlay",
320
+ action: "skip",
321
+ reason: "operator customization overlay is preserved; upgrade does not replace organization standards, custom core docs, or configured skill mirror targets",
322
+ });
323
+ }
278
324
  function migrateClosedGoalActiveNodes(root, dryRun, summary, changes) {
279
325
  const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
280
326
  const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
@@ -323,9 +369,59 @@ function migrateClosedGoalActiveNodes(root, dryRun, summary, changes) {
323
369
  frontmatter.last_active_node = activeNode;
324
370
  }
325
371
  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);
372
+ writeFile(filePath, renderFrontmatterDocument(frontmatter, parsed.body.length > 0 ? parsed.body : ""));
373
+ }
374
+ }
375
+ }
376
+ if (planned === 0) {
377
+ summary.unchanged += 1;
378
+ }
379
+ }
380
+ function migrateLegacySpecManifests(root, dryRun, summary, changes) {
381
+ const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
382
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
383
+ let planned = 0;
384
+ for (const files of Object.values(filesByAlias)) {
385
+ for (const filePath of files) {
386
+ if (path_1.default.basename(filePath) !== agent_file_types_1.LEGACY_SPEC_BASENAME) {
387
+ continue;
388
+ }
389
+ const content = fs_1.default.readFileSync(filePath, "utf8");
390
+ let parsed;
391
+ try {
392
+ parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
393
+ }
394
+ catch {
395
+ continue;
396
+ }
397
+ if (parsed.frontmatter.type !== "spec") {
398
+ continue;
399
+ }
400
+ planned += 1;
401
+ const targetPath = path_1.default.join(path_1.default.dirname(filePath), agent_file_types_1.CANONICAL_MANIFEST_BASENAME);
402
+ const relativePath = repoRelativePath(root, filePath);
403
+ const relativeTargetPath = repoRelativePath(root, targetPath);
404
+ if (fs_1.default.existsSync(targetPath)) {
405
+ record(summary, changes, {
406
+ path: relativePath,
407
+ target_path: relativeTargetPath,
408
+ category: "manifest_migration",
409
+ action: "conflict",
410
+ reason: "sibling MANIFEST.md already exists; legacy SPEC.md content preserved",
411
+ });
412
+ continue;
413
+ }
414
+ record(summary, changes, {
415
+ path: relativePath,
416
+ target_path: relativeTargetPath,
417
+ category: "manifest_migration",
418
+ action: "migrate",
419
+ reason: "rename legacy SPEC.md to MANIFEST.md and normalize type: spec to type: manifest",
420
+ });
421
+ if (!dryRun) {
422
+ const frontmatter = { ...parsed.frontmatter, type: "manifest" };
423
+ (0, atomic_1.atomicWriteFile)(filePath, renderFrontmatterDocument(frontmatter, parsed.body));
424
+ fs_1.default.renameSync(filePath, targetPath);
329
425
  }
330
426
  }
331
427
  }
@@ -422,6 +518,9 @@ function ensureArchiveIgnorePolicy(root, dryRun, summary, changes) {
422
518
  function isWritableChange(change) {
423
519
  return change.action === "create" || change.action === "update" || change.action === "migrate" || change.action === "sync";
424
520
  }
521
+ function writablePathForChange(change) {
522
+ return change.target_path ?? change.path;
523
+ }
425
524
  function buildApplySideEffects(options) {
426
525
  const hasDirectWrites = options.changes.some(isWritableChange);
427
526
  if (!hasDirectWrites && options.existingManifest) {
@@ -441,12 +540,15 @@ function buildApplySideEffects(options) {
441
540
  category: "skill_registry",
442
541
  action: "update",
443
542
  reason: "refreshes canonical skill registry after apply",
444
- }, {
445
- path: ".agents/skills,.claude/skills",
446
- category: "skill_mirror",
447
- action: "sync",
448
- reason: "syncs managed skill mirrors after apply",
449
543
  });
544
+ if (options.mirrorTargets.length > 0) {
545
+ effects.push({
546
+ path: options.mirrorTargets.join(","),
547
+ category: "skill_mirror",
548
+ action: "sync",
549
+ reason: "syncs configured managed skill mirrors after apply",
550
+ });
551
+ }
450
552
  }
451
553
  return effects;
452
554
  }
@@ -468,7 +570,8 @@ function emitHumanReceipt(receipt) {
468
570
  }
469
571
  else {
470
572
  for (const change of receipt.changes) {
471
- console.log(` ${change.action}: ${change.path} (${change.reason})`);
573
+ const target = change.target_path ? ` -> ${change.target_path}` : "";
574
+ console.log(` ${change.action}: ${change.path}${target} (${change.reason})`);
472
575
  }
473
576
  }
474
577
  if (receipt.apply_side_effects.length > 0) {
@@ -497,10 +600,13 @@ function runUpgradeCommand(options) {
497
600
  const knownHashes = buildKnownHashes([existingManifest, ...legacyManifests]);
498
601
  const summary = createSummary();
499
602
  const changes = [];
500
- const agentWorkspace = isAgentWorkspace(root);
603
+ const initialConfig = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
604
+ const agentWorkspace = isAgentWorkspace(root, initialConfig);
501
605
  const managedCurrentFiles = [];
502
606
  migrateConfigIfNeeded(root, dryRun, summary, changes);
607
+ reportPreservedCustomizationOverlay(root, summary, changes);
503
608
  migrateClosedGoalActiveNodes(root, dryRun, summary, changes);
609
+ migrateLegacySpecManifests(root, dryRun, summary, changes);
504
610
  for (const file of currentManifest.files) {
505
611
  if (!shouldIncludeFile(file, agentWorkspace)) {
506
612
  continue;
@@ -523,6 +629,7 @@ function runUpgradeCommand(options) {
523
629
  const applySideEffects = buildApplySideEffects({
524
630
  existingManifest,
525
631
  agentWorkspace,
632
+ mirrorTargets: (0, skill_mirror_1.configuredSkillMirrorTargets)(initialConfig),
526
633
  changes,
527
634
  });
528
635
  for (const effect of applySideEffects) {
@@ -539,13 +646,13 @@ function runUpgradeCommand(options) {
539
646
  if (agentWorkspace && applySideEffects.length > 0) {
540
647
  const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
541
648
  (0, skill_support_1.refreshSkillsRegistry)(root, config);
542
- (0, skill_mirror_1.scaffoldMirrorRoots)(root);
649
+ (0, skill_mirror_1.scaffoldMirrorRoots)(root, config);
543
650
  (0, skill_mirror_1.syncSkillMirrors)({ root, config, createRoots: true });
544
651
  }
545
652
  }
546
- const preservedCustomizations = changes.filter((change) => change.action === "conflict");
547
- const blockingConflicts = [];
548
- const willWritePaths = changes.filter(isWritableChange).map((change) => change.path);
653
+ const preservedCustomizations = changes.filter((change) => change.action === "conflict" || change.category === "customization_overlay");
654
+ const blockingConflicts = changes.filter((change) => change.action === "conflict" && change.category === "manifest_migration");
655
+ const willWritePaths = changes.filter(isWritableChange).map(writablePathForChange);
549
656
  const receipt = {
550
657
  action: "upgrade",
551
658
  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,