@milehimikey/em 1.13.1 → 1.14.0

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 (232) hide show
  1. package/.claude/agents/em-critic.md +26 -0
  2. package/.claude/agents/em-implementer.md +34 -0
  3. package/.claude/agents/em-reviewer.md +31 -0
  4. package/.claude/agents/em-validator.md +27 -0
  5. package/.claude/skills/event-modeling/SKILL.md +50 -11
  6. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  7. package/.claude/skills/event-modeling-design/SKILL.md +126 -18
  8. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  9. package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
  10. package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
  11. package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
  12. package/.claude/skills/event-modeling-implement/reference/implement.md +38 -11
  13. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  14. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -189
  15. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
  16. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +108 -5
  17. package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
  18. package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
  19. package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
  20. package/.claude-plugin/marketplace.json +14 -0
  21. package/README.md +25 -11
  22. package/dist/catalog/apiFirst.d.ts +28 -0
  23. package/dist/catalog/apiFirst.js +49 -0
  24. package/dist/catalog/apiFirst.js.map +1 -0
  25. package/dist/catalog/docJoin.d.ts +13 -0
  26. package/dist/catalog/docJoin.js +11 -0
  27. package/dist/catalog/docJoin.js.map +1 -1
  28. package/dist/catalog/docModelConsistencyValidate.js +44 -4
  29. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  30. package/dist/catalog/invariantsValidate.d.ts +4 -0
  31. package/dist/catalog/invariantsValidate.js +102 -0
  32. package/dist/catalog/invariantsValidate.js.map +1 -0
  33. package/dist/catalog/scenarios.d.ts +15 -0
  34. package/dist/catalog/scenarios.js +97 -0
  35. package/dist/catalog/scenarios.js.map +1 -0
  36. package/dist/catalog/sliceDoc.d.ts +8 -0
  37. package/dist/catalog/sliceDoc.js +9 -0
  38. package/dist/catalog/sliceDoc.js.map +1 -1
  39. package/dist/catalog/sliceReadyValidate.d.ts +31 -4
  40. package/dist/catalog/sliceReadyValidate.js +66 -8
  41. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  42. package/dist/catalog/sliceSections.d.ts +55 -0
  43. package/dist/catalog/sliceSections.js +317 -0
  44. package/dist/catalog/sliceSections.js.map +1 -0
  45. package/dist/cli/agentsMd.js +7 -1
  46. package/dist/cli/agentsMd.js.map +1 -1
  47. package/dist/cli/api.d.ts +90 -0
  48. package/dist/cli/api.js +318 -0
  49. package/dist/cli/api.js.map +1 -0
  50. package/dist/cli/ciInit.d.ts +82 -21
  51. package/dist/cli/ciInit.js +468 -64
  52. package/dist/cli/ciInit.js.map +1 -1
  53. package/dist/cli/conformScope.d.ts +28 -0
  54. package/dist/cli/conformScope.js +67 -0
  55. package/dist/cli/conformScope.js.map +1 -1
  56. package/dist/cli/coverage.d.ts +9 -0
  57. package/dist/cli/coverage.js +60 -8
  58. package/dist/cli/coverage.js.map +1 -1
  59. package/dist/cli/engagement.d.ts +210 -0
  60. package/dist/cli/engagement.js +444 -0
  61. package/dist/cli/engagement.js.map +1 -0
  62. package/dist/cli/engagementFile.d.ts +86 -0
  63. package/dist/cli/engagementFile.js +255 -0
  64. package/dist/cli/engagementFile.js.map +1 -0
  65. package/dist/cli/metrics.d.ts +10 -0
  66. package/dist/cli/metrics.js +49 -1
  67. package/dist/cli/metrics.js.map +1 -1
  68. package/dist/cli/pluginPin.d.ts +55 -0
  69. package/dist/cli/pluginPin.js +188 -0
  70. package/dist/cli/pluginPin.js.map +1 -0
  71. package/dist/cli/ratify.d.ts +27 -2
  72. package/dist/cli/ratify.js +112 -13
  73. package/dist/cli/ratify.js.map +1 -1
  74. package/dist/cli/reratify.d.ts +3 -2
  75. package/dist/cli/reratify.js +36 -14
  76. package/dist/cli/reratify.js.map +1 -1
  77. package/dist/cli/scopeInputs.d.ts +48 -0
  78. package/dist/cli/scopeInputs.js +176 -0
  79. package/dist/cli/scopeInputs.js.map +1 -0
  80. package/dist/cli/skillCheck.d.ts +15 -1
  81. package/dist/cli/skillCheck.js +22 -1
  82. package/dist/cli/skillCheck.js.map +1 -1
  83. package/dist/cli/skillDirs.d.ts +5 -0
  84. package/dist/cli/skillDirs.js +13 -1
  85. package/dist/cli/skillDirs.js.map +1 -1
  86. package/dist/cli/skillSync.d.ts +4 -0
  87. package/dist/cli/skillSync.js +23 -0
  88. package/dist/cli/skillSync.js.map +1 -1
  89. package/dist/cli/sliceNew.d.ts +11 -7
  90. package/dist/cli/sliceNew.js +91 -18
  91. package/dist/cli/sliceNew.js.map +1 -1
  92. package/dist/cli/sliceSync.d.ts +50 -0
  93. package/dist/cli/sliceSync.js +170 -0
  94. package/dist/cli/sliceSync.js.map +1 -0
  95. package/dist/cli/status.d.ts +24 -1
  96. package/dist/cli/status.js +20 -1
  97. package/dist/cli/status.js.map +1 -1
  98. package/dist/cli/systemInputs.d.ts +53 -10
  99. package/dist/cli/systemInputs.js +186 -11
  100. package/dist/cli/systemInputs.js.map +1 -1
  101. package/dist/cli/upgrade.d.ts +16 -3
  102. package/dist/cli/upgrade.js +366 -30
  103. package/dist/cli/upgrade.js.map +1 -1
  104. package/dist/cli/usageLog.d.ts +4 -2
  105. package/dist/cli/usageLog.js +4 -2
  106. package/dist/cli/usageLog.js.map +1 -1
  107. package/dist/cli.js +554 -94
  108. package/dist/cli.js.map +1 -1
  109. package/dist/emit/api.d.ts +29 -0
  110. package/dist/emit/api.js +220 -0
  111. package/dist/emit/api.js.map +1 -0
  112. package/dist/emit/apiCheckJson.d.ts +3 -0
  113. package/dist/emit/apiCheckJson.js +21 -0
  114. package/dist/emit/apiCheckJson.js.map +1 -0
  115. package/dist/emit/codeownersJson.d.ts +3 -0
  116. package/dist/emit/codeownersJson.js +19 -0
  117. package/dist/emit/codeownersJson.js.map +1 -0
  118. package/dist/emit/diffJson.d.ts +1 -1
  119. package/dist/emit/diffJson.js +8 -1
  120. package/dist/emit/diffJson.js.map +1 -1
  121. package/dist/emit/engagementJson.d.ts +8 -0
  122. package/dist/emit/engagementJson.js +46 -0
  123. package/dist/emit/engagementJson.js.map +1 -0
  124. package/dist/emit/glossaryJson.d.ts +1 -1
  125. package/dist/emit/glossaryJson.js +4 -1
  126. package/dist/emit/glossaryJson.js.map +1 -1
  127. package/dist/emit/json.d.ts +17 -1
  128. package/dist/emit/json.js +30 -1
  129. package/dist/emit/json.js.map +1 -1
  130. package/dist/emit/metricsJson.d.ts +1 -1
  131. package/dist/emit/metricsJson.js +4 -1
  132. package/dist/emit/metricsJson.js.map +1 -1
  133. package/dist/emit/queryJson.d.ts +1 -1
  134. package/dist/emit/queryJson.js +7 -1
  135. package/dist/emit/queryJson.js.map +1 -1
  136. package/dist/emit/scopeJson.d.ts +12 -0
  137. package/dist/emit/scopeJson.js +26 -0
  138. package/dist/emit/scopeJson.js.map +1 -0
  139. package/dist/emit/skillCheckJson.d.ts +3 -2
  140. package/dist/emit/skillCheckJson.js +10 -2
  141. package/dist/emit/skillCheckJson.js.map +1 -1
  142. package/dist/emit/statusJson.d.ts +1 -1
  143. package/dist/emit/statusJson.js +15 -1
  144. package/dist/emit/statusJson.js.map +1 -1
  145. package/dist/emit/systemJson.d.ts +15 -8
  146. package/dist/emit/systemJson.js +28 -13
  147. package/dist/emit/systemJson.js.map +1 -1
  148. package/dist/emit/validateJson.d.ts +3 -3
  149. package/dist/emit/validateJson.js +10 -4
  150. package/dist/emit/validateJson.js.map +1 -1
  151. package/dist/mcp/server.d.ts +1 -1
  152. package/dist/mcp/server.js +163 -31
  153. package/dist/mcp/server.js.map +1 -1
  154. package/dist/model/diff.d.ts +4 -1
  155. package/dist/model/diff.js +44 -8
  156. package/dist/model/diff.js.map +1 -1
  157. package/dist/model/glossary.d.ts +2 -0
  158. package/dist/model/glossary.js +1 -0
  159. package/dist/model/glossary.js.map +1 -1
  160. package/dist/model/model.d.ts +55 -2
  161. package/dist/model/model.js +57 -0
  162. package/dist/model/model.js.map +1 -1
  163. package/dist/model/qualifiedRef.d.ts +18 -0
  164. package/dist/model/qualifiedRef.js +26 -0
  165. package/dist/model/qualifiedRef.js.map +1 -1
  166. package/dist/model/queryIndex.d.ts +9 -1
  167. package/dist/model/queryIndex.js +18 -2
  168. package/dist/model/queryIndex.js.map +1 -1
  169. package/dist/model/rules.d.ts +143 -2
  170. package/dist/model/rules.js +162 -3
  171. package/dist/model/rules.js.map +1 -1
  172. package/dist/model/validate.d.ts +40 -0
  173. package/dist/model/validate.js +147 -1
  174. package/dist/model/validate.js.map +1 -1
  175. package/dist/parser/ast.d.ts +31 -3
  176. package/dist/parser/parser.js +148 -14
  177. package/dist/parser/parser.js.map +1 -1
  178. package/dist/query/format.js +1 -1
  179. package/dist/query/format.js.map +1 -1
  180. package/dist/query/verbs.d.ts +6 -0
  181. package/dist/query/verbs.js +6 -2
  182. package/dist/query/verbs.js.map +1 -1
  183. package/dist/system/adaptation.d.ts +14 -0
  184. package/dist/system/adaptation.js +49 -0
  185. package/dist/system/adaptation.js.map +1 -0
  186. package/dist/system/codeowners.d.ts +77 -0
  187. package/dist/system/codeowners.js +242 -0
  188. package/dist/system/codeowners.js.map +1 -0
  189. package/dist/system/manifest.d.ts +10 -5
  190. package/dist/system/manifest.js +54 -26
  191. package/dist/system/manifest.js.map +1 -1
  192. package/dist/system/migrateManifest.d.ts +28 -0
  193. package/dist/system/migrateManifest.js +155 -0
  194. package/dist/system/migrateManifest.js.map +1 -0
  195. package/dist/system/scope.d.ts +46 -0
  196. package/dist/system/scope.js +132 -0
  197. package/dist/system/scope.js.map +1 -0
  198. package/dist/system/verify.d.ts +50 -13
  199. package/dist/system/verify.js +179 -58
  200. package/dist/system/verify.js.map +1 -1
  201. package/dist/templates.d.ts +2 -2
  202. package/dist/templates.js +23 -2
  203. package/dist/templates.js.map +1 -1
  204. package/package.json +5 -2
  205. package/plugin/.claude-plugin/plugin.json +18 -0
  206. package/plugin/agents/em-critic.md +27 -0
  207. package/plugin/agents/em-implementer.md +35 -0
  208. package/plugin/agents/em-reviewer.md +32 -0
  209. package/plugin/agents/em-validator.md +28 -0
  210. package/plugin/shared/reference/em-dsl.md +866 -0
  211. package/plugin/shared/reference/methodology.md +329 -0
  212. package/plugin/shared/reference/operating-principles.md +222 -0
  213. package/plugin/shared/reference/slice-doc-schema.md +566 -0
  214. package/plugin/shared/templates/conformance-report.md +85 -0
  215. package/plugin/shared/templates/constitution.md +136 -0
  216. package/plugin/shared/templates/model-readme.md +46 -0
  217. package/plugin/shared/templates/slice.md +251 -0
  218. package/plugin/shared/templates/state.md +77 -0
  219. package/plugin/skills/conform/SKILL.md +93 -0
  220. package/plugin/skills/conform/reference/conform.md +428 -0
  221. package/plugin/skills/design/SKILL.md +297 -0
  222. package/plugin/skills/discover/SKILL.md +90 -0
  223. package/plugin/skills/discover/reference/extract.md +147 -0
  224. package/plugin/skills/engagement/SKILL.md +193 -0
  225. package/plugin/skills/engagement/reference/engagement.md +144 -0
  226. package/plugin/skills/event-modeling/SKILL.md +132 -0
  227. package/plugin/skills/implement/SKILL.md +61 -0
  228. package/plugin/skills/implement/reference/implement.md +439 -0
  229. package/plugin/skills/review/SKILL.md +74 -0
  230. package/dist/emit/typespec.d.ts +0 -26
  231. package/dist/emit/typespec.js +0 -356
  232. package/dist/emit/typespec.js.map +0 -1
package/dist/cli.js CHANGED
@@ -17,11 +17,12 @@ import { serializeBuilds, watchFile } from "./render/watch.js";
17
17
  import { startLiveServer } from "./render/serve.js";
18
18
  import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
19
19
  import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
20
- import { buildTypeSpec } from "./emit/typespec.js";
20
+ import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
21
+ import { buildApiCheckJson } from "./emit/apiCheckJson.js";
21
22
  import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
22
23
  import { buildDiffJson } from "./emit/diffJson.js";
23
24
  import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
24
- import { planDiffArgs, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
25
+ import { planDiffArgs, realGit, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
25
26
  import { readSliceDoc } from "./catalog/readSliceDoc.js";
26
27
  import { validateLineage } from "./catalog/lineageValidate.js";
27
28
  import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
@@ -29,7 +30,11 @@ import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
29
30
  import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
30
31
  import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
31
32
  import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
32
- import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
33
+ import { validateInvariants } from "./catalog/invariantsValidate.js";
34
+ import { computeSliceReadiness } from "./catalog/sliceReadyValidate.js";
35
+ import { applyEngagementClose, applyEngagementSet, buildNewEngagement, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
36
+ import { LEDGER_STATES, engagementPath, loadEngagement, readOpenEngagements } from "./cli/engagementFile.js";
37
+ import { buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
33
38
  import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
34
39
  import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
35
40
  import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
@@ -40,12 +45,15 @@ import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggr
40
45
  import { buildStatusJson } from "./emit/statusJson.js";
41
46
  import { buildFreshnessJson } from "./emit/freshnessJson.js";
42
47
  import { computeMetrics, formatMetricsText } from "./cli/metrics.js";
48
+ import { runSystemScope } from "./cli/scopeInputs.js";
49
+ import { buildScopeJson } from "./emit/scopeJson.js";
43
50
  import { buildMetricsJson } from "./emit/metricsJson.js";
44
- import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
45
- import { checkSkillSyncBundle } from "./cli/skillCheck.js";
51
+ import { planSkillSyncBundle, applySkillSyncBundle, planAgentSync, applySkillSync } from "./cli/skillSync.js";
52
+ import { checkSkillSyncBundle, checkAgentFiles } from "./cli/skillCheck.js";
46
53
  import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
54
+ import { checkPlugin, claudePluginsDir, detectPlugin, pluginInstallCommands } from "./cli/pluginPin.js";
47
55
  import { readContract } from "./cli/contract.js";
48
- import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR } from "./cli/skillDirs.js";
56
+ import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR, EM_AGENT_FILES } from "./cli/skillDirs.js";
49
57
  import { createServer as createMcpServer } from "./mcp/server.js";
50
58
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
51
59
  import { syncAgentsMd } from "./cli/agentsMd.js";
@@ -56,11 +64,13 @@ import { buildCatalog } from "./catalog/build.js";
56
64
  import { planCatalogArgs } from "./cli/catalog-inputs.js";
57
65
  import { runSliceIndex } from "./cli/sliceIndex.js";
58
66
  import { runMarkImplemented } from "./cli/markImplemented.js";
59
- import { runRatify } from "./cli/ratify.js";
67
+ import { runRatify, parseMeaningFlags, meaningConfirmationLine } from "./cli/ratify.js";
60
68
  import { runReview } from "./cli/review.js";
61
69
  import { runConformSupersede } from "./cli/conformSupersede.js";
62
70
  import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
63
71
  import { buildSliceDocContent, buildStubDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
72
+ import { buildSliceSyncJson, formatSliceSync, runSliceSync, sliceSyncFailed } from "./cli/sliceSync.js";
73
+ import { buildSliceRegions } from "./catalog/sliceSections.js";
64
74
  import { wireSliceNote } from "./cli/sliceLink.js";
65
75
  import { isStubStatus, runStubAll, STUB_STATUSES } from "./cli/sliceStubAll.js";
66
76
  import { listModelCommits } from "./cli/changelog-git.js";
@@ -72,7 +82,7 @@ import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase,
72
82
  import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
73
83
  import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
74
84
  import { kebabSlug } from "./util/slug.js";
75
- import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
85
+ import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, buildCiWorkflowFileMulti, buildConformWorkflowFileMulti, ciManagedBody, ciManagedBodyMulti, conformManagedBody, conformManagedBodyMulti, ciModelsFromManifest, describeOtherModels, describeSetChange, fileBlockModels, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
76
86
  import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
77
87
  import { buildUpgradeJson } from "./emit/upgradeJson.js";
78
88
  import { isOlder } from "./util/semver.js";
@@ -84,7 +94,9 @@ import { buildQuerySystem } from "./query/system.js";
84
94
  import { queryConsumers, queryProducers, queryDownstream, queryUpstream, querySlices, queryInvariant, queryField, queryPath, } from "./query/verbs.js";
85
95
  import { formatConsumers, formatProducers, formatClosure, formatSlices, formatInvariant, formatField, formatPath, } from "./query/format.js";
86
96
  import { buildQueryJson } from "./emit/queryJson.js";
87
- import { loadSystem } from "./cli/systemInputs.js";
97
+ import { gitProducerCommit, loadSystem, statusSystemBlock, SYSTEM_MANIFEST_FILE } from "./cli/systemInputs.js";
98
+ import { runCodeowners } from "./system/codeowners.js";
99
+ import { buildCodeownersJson } from "./emit/codeownersJson.js";
88
100
  import { verifySystem } from "./system/verify.js";
89
101
  import { buildSystemJson } from "./emit/systemJson.js";
90
102
  const program = new Command();
@@ -95,7 +107,10 @@ const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.me
95
107
  program
96
108
  .name("em")
97
109
  .description("Event Modeling CLI — slice-first DSL rendered as a strict Graphviz grid")
98
- .version(PKG_VERSION);
110
+ .version(PKG_VERSION)
111
+ // Program options (`--version`) only before the subcommand name: `em system codeowners --json`
112
+ // must reach the subcommand (MIL-234), and `em system` has its own `--json`.
113
+ .enablePositionalOptions();
99
114
  program
100
115
  .command("init")
101
116
  .description("scaffold a starter .em model")
@@ -265,30 +280,67 @@ program
265
280
  process.stdout.write(exported.text + "\n");
266
281
  }
267
282
  });
268
- program
269
- .command("typespec")
270
- .description("EXPERIMENTAL/POC (MIL-159): generate a TypeSpec contract for a model's commands, public " +
271
- "events, and public views (see docs/cli.md)")
272
- .argument("<file>", "input .em file")
273
- .option("-o, --out <path>", "write to a file instead of stdout")
274
- .action((file, opts) => {
275
- const { model, refs, diagnostics, source } = compileFile(file);
276
- printDiagnostics(diagnostics);
277
- if (hasErrors(diagnostics)) {
283
+ /** `em api generate` (and its deprecated `em typespec` alias): compile, refuse on errors,
284
+ * write `<modelDir>/contracts/<modelKey>.tsp` (R6) — or `-o`, or stdout. */
285
+ function runApiGenerate(file, opts) {
286
+ const compiled = compileFile(file);
287
+ const { source } = compiled;
288
+ printDiagnostics(compiled.diagnostics);
289
+ if (hasErrors(compiled.diagnostics)) {
278
290
  console.error("not generating: fix the errors above");
279
291
  process.exit(1);
280
292
  }
281
- const generated = buildTypeSpec(model, refs, diagnostics, source, file);
282
- printDiagnostics(newDiagnostics(generated.diagnostics, diagnostics));
283
- for (const note of generated.unmappedTypes)
284
- console.error(`note: ${note}`);
285
- if (opts.out) {
286
- writeFileSync(opts.out, generated.text + "\n");
287
- console.log(`wrote ${opts.out}`);
293
+ const generated = generateContract(file, source, compiled, opts.out);
294
+ printDiagnostics(newDiagnostics(generated.diagnostics, compiled.diagnostics));
295
+ if (opts.stdout) {
296
+ process.stdout.write(generated.text);
297
+ return;
288
298
  }
289
- else {
290
- process.stdout.write(generated.text + "\n");
299
+ mkdirSync(dirname(generated.contractPath), { recursive: true });
300
+ writeFileSync(generated.contractPath, generated.text);
301
+ console.log(`wrote ${generated.contractPath}`);
302
+ }
303
+ const api = program
304
+ .command("api")
305
+ .description("the model-owned TypeSpec contract for its `public` surface (MIL-237, see docs/cli.md)");
306
+ api
307
+ .command("generate")
308
+ .description("write the TypeSpec contract for the model's public commands, events and views to " +
309
+ "<model dir>/contracts/<model key>.tsp")
310
+ .argument("<file>", "input .em file")
311
+ .option("-o, --out <path>", "write to this path instead of <model dir>/contracts/<model key>.tsp")
312
+ .option("--stdout", "print the contract instead of writing it")
313
+ .action((file, opts) => runApiGenerate(file, opts));
314
+ api
315
+ .command("check")
316
+ .description("is the committed contract current? With --base, annotate each public-surface change since " +
317
+ "that revision additive or breaking (annotation only — exits 1 only when stale)")
318
+ .argument("<file>", "input .em file")
319
+ .option("--base <rev>", "git revision to diff the public surface against")
320
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
321
+ .action((file, opts) => {
322
+ const outcome = runApiCheck(file, opts.base);
323
+ printDiagnostics(outcome.diagnostics);
324
+ if (!outcome.ok) {
325
+ console.error(outcome.message);
326
+ process.exit(1);
291
327
  }
328
+ if (opts.json)
329
+ process.stdout.write(buildApiCheckJson(outcome.report) + "\n");
330
+ else
331
+ for (const line of formatApiCheckText(outcome.report))
332
+ console.log(line);
333
+ if (!outcome.report.current)
334
+ process.exitCode = 1;
335
+ });
336
+ program
337
+ .command("typespec")
338
+ .description("DEPRECATED alias of `em api generate --stdout` (use em api generate)")
339
+ .argument("<file>", "input .em file")
340
+ .option("-o, --out <path>", "write to a file instead of stdout")
341
+ .action((file, opts) => {
342
+ console.error("warn: em typespec is deprecated — use em api generate");
343
+ runApiGenerate(file, opts.out ? { out: opts.out } : { stdout: true });
292
344
  });
293
345
  program
294
346
  .command("diff")
@@ -437,9 +489,11 @@ program
437
489
  const slice = program.command("slice").description("author and maintain slice docs");
438
490
  slice
439
491
  .command("new")
440
- .description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft` " +
441
- "plus the `# Slice:` heading and diagram-image stub; judgment sections (Intent, " +
442
- "Scenarios, Open Questions, ...) stay hand-authored (see " +
492
+ .description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft`, " +
493
+ "the `# Slice:` heading and diagram-image stub, then every template section: the generated " +
494
+ "regions (command/event/view field tables, Invariants) filled from the model with --wire " +
495
+ "(template placeholders otherwise — `em slice sync` fills them), the authored sections " +
496
+ "(Intent, Scenarios, Open Questions, ...) with placeholder bullets (see " +
443
497
  ".claude/skills/event-modeling-shared/reference/slice-doc-schema.md, templates/slice.md)")
444
498
  .argument("<name>", "slice display name (e.g. \"Request Payment\") — kebab-cased for the filename")
445
499
  .requiredOption("--pattern <pattern>", `slice pattern: ${SLICE_PATTERNS.join(" | ")}`)
@@ -466,6 +520,9 @@ slice
466
520
  // (no matching slice, ambiguous primary element, already wired) leaves nothing written at
467
521
  // all rather than a doc with no wiring and a non-zero exit.
468
522
  let wired = null;
523
+ // MIL-266: with a model in hand (--wire), the doc's generated regions are filled from it;
524
+ // without one they carry the template's placeholders until `em slice sync` fills them.
525
+ let regions;
469
526
  if (opts.wire) {
470
527
  const { model, refs, diagnostics, source } = compileFile(opts.wire);
471
528
  printDiagnostics(diagnostics);
@@ -479,11 +536,12 @@ slice
479
536
  process.exit(1);
480
537
  }
481
538
  wired = result;
539
+ regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
482
540
  }
483
541
  mkdirSync(dir, { recursive: true });
484
542
  const content = opts.stub
485
543
  ? buildStubDocContent(name, opts.pattern, opts.swimlane)
486
- : buildSliceDocContent(name, key, opts.pattern, opts.swimlane);
544
+ : buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions);
487
545
  writeFileSync(path, content);
488
546
  console.log(`wrote ${path}`);
489
547
  if (wired) {
@@ -495,6 +553,39 @@ slice
495
553
  console.log(` note "${path}"`);
496
554
  }
497
555
  });
556
+ slice
557
+ .command("sync")
558
+ .description("regenerate the GENERATED regions (command/event/view field tables, the Invariants list) of " +
559
+ "existing slice docs in place from the model, authored sections untouched (MIL-266); docs " +
560
+ "without regions are skipped with a note. --check never writes and exits 1 when a doc is stale")
561
+ .argument("<model-file>", "the .em model whose bound slice docs to sync")
562
+ .argument("[key]", "only the doc this slice's export key resolves to")
563
+ .option("--check", "report ok / stale / no-regions per doc without writing; exit 1 when any is stale")
564
+ .option("--json", "print the machine-readable report (sliceSyncSchemaVersion 1.0)")
565
+ .action((file, key, opts) => {
566
+ const { model, refs, diagnostics } = compileFile(file);
567
+ printDiagnostics(diagnostics.filter((d) => d.severity === "error"));
568
+ if (hasErrors(diagnostics)) {
569
+ console.error(`em slice sync: not syncing — fix the errors in ${file} above`);
570
+ process.exit(1);
571
+ }
572
+ const check = opts.check === true;
573
+ const outcome = runSliceSync(model, refs, dirname(file), key, check);
574
+ if (!outcome.ok) {
575
+ console.error(`em slice sync: ${outcome.message}`);
576
+ process.exit(1);
577
+ }
578
+ const { out, err } = formatSliceSync(outcome.docs, check);
579
+ for (const line of err)
580
+ console.error(line);
581
+ if (opts.json)
582
+ process.stdout.write(buildSliceSyncJson(file, outcome.docs) + "\n");
583
+ else
584
+ for (const line of out)
585
+ console.log(line);
586
+ if (sliceSyncFailed(outcome.docs, check))
587
+ process.exit(1);
588
+ });
498
589
  slice
499
590
  .command("stub-all")
500
591
  .description("scaffold + wire a near-free stub (`em slice new --stub`'s content, MIL-184) for every " +
@@ -642,6 +733,10 @@ slice
642
733
  ? `reviewed: ${result.path} (reviewedBy: ${opts.by}, reviewedOn: ${reviewedOn})`
643
734
  : `already reviewed (no-op): ${result.path}`);
644
735
  });
736
+ /** MIL-238: `, meaningConfirmed: true` / `, contractChange: "<why>"` for a success line, or "". */
737
+ function confirmationSuffix(c) {
738
+ return c ? `, ${meaningConfirmationLine(c)}` : "";
739
+ }
645
740
  slice
646
741
  .command("ratify")
647
742
  .description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
@@ -659,7 +754,16 @@ slice
659
754
  .requiredOption("--by <name>", "the ratifier's name")
660
755
  .option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
661
756
  .option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
757
+ .option("--meaning-unchanged", "API-first sign-off (MIL-238): this version does not change what the public contract means — " +
758
+ "records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
759
+ .option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read this change differently, and why — records " +
760
+ "`contractChange: \"<why>\"`")
662
761
  .action((file, sliceKey, opts) => {
762
+ const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
763
+ if (!flags.ok) {
764
+ console.error(`em slice ratify: ${flags.message}`);
765
+ process.exit(1);
766
+ }
663
767
  const { model, refs, diagnostics, source } = compileFile(file);
664
768
  printDiagnostics(diagnostics);
665
769
  // Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
@@ -675,7 +779,7 @@ slice
675
779
  console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
676
780
  process.exit(1);
677
781
  }
678
- const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true);
782
+ const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true, flags.confirmation);
679
783
  if (!result.ok) {
680
784
  console.error(`em slice ratify: ${result.message}`);
681
785
  process.exit(1);
@@ -693,7 +797,7 @@ slice
693
797
  console.error(`warn: ratifying "${sliceKey}" ahead of upstream slice "${upstream.sliceKey}" (status: ${upstream.status})`);
694
798
  }
695
799
  console.log(result.changed
696
- ? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
800
+ ? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn}${confirmationSuffix(flags.confirmation)})`
697
801
  : `already ratified (no-op): ${result.path}`);
698
802
  warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
699
803
  });
@@ -710,7 +814,16 @@ slice
710
814
  "fresh review; never touches `implementedIn:` or the doc body")
711
815
  .argument("<file>", "input .em file")
712
816
  .argument("<slice-key>", "slice export key (kebab-case)")
713
- .action((file, sliceKey) => {
817
+ .option("--meaning-unchanged", "API-first sign-off (MIL-238): the new version does not change what the public contract means — " +
818
+ "records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
819
+ .option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read the new version differently, and why — records " +
820
+ "`contractChange: \"<why>\"`")
821
+ .action((file, sliceKey, opts) => {
822
+ const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
823
+ if (!flags.ok) {
824
+ console.error(`em slice reratify: ${flags.message}`);
825
+ process.exit(1);
826
+ }
714
827
  const { model, refs, diagnostics, source } = compileFile(file);
715
828
  printDiagnostics(diagnostics);
716
829
  // Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
@@ -720,7 +833,7 @@ slice
720
833
  console.error(`em slice reratify: slice "${sliceKey}" has errors — fix them first`);
721
834
  process.exit(1);
722
835
  }
723
- const result = runReratify(model, refs, dirname(file), sliceKey);
836
+ const result = runReratify(model, refs, dirname(file), sliceKey, flags.confirmation);
724
837
  if (!result.ok) {
725
838
  console.error(`em slice reratify: ${result.message}`);
726
839
  process.exit(1);
@@ -736,10 +849,11 @@ slice
736
849
  if (result.kind === "unshipped") {
737
850
  // MIL-258: nothing was flipped — say what DID happen, and that the new version is unsigned.
738
851
  console.log(`reratified: ${result.path} (version: ${result.newVersion - 1} -> ${result.newVersion}, sign-off cleared, ` +
739
- `status unchanged: ready-to-implement) — not ratified until \`em slice ratify --by <name>\` records the new sign-off`);
852
+ `status unchanged: ready-to-implement${confirmationSuffix(flags.confirmation)}) — not ratified until ` +
853
+ `\`em slice ratify --by <name>\` records the new sign-off`);
740
854
  }
741
855
  else {
742
- console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
856
+ console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement${confirmationSuffix(flags.confirmation)})`);
743
857
  }
744
858
  warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
745
859
  });
@@ -1323,10 +1437,12 @@ program
1323
1437
  .allowExcessArguments(false)
1324
1438
  .option("--list-issues", "print only open `issue` diagnostics (slice, element, line, text)")
1325
1439
  .option("--list-divergences", "print only accepted-divergence annotations (slice, element, line, text) — never fails the build")
1326
- .option("--list-public", "print only events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build")
1440
+ .option("--list-public", "print only commands, events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build")
1327
1441
  .option("--fail-on-issues", "exit non-zero if the model has any open `issue`s (opt-in — issues are warnings and don't block by default)")
1328
1442
  .option("--slice-ready <key>", "readiness gate for one slice (export key): status ready-to-implement, doc resolvable via " +
1329
- "note binding, zero unchecked Open Questions — exits non-zero if not ready (MIL-87)")
1443
+ "note binding, zero unchecked Open Questions, a recorded ratifiedBy, and — for a slice " +
1444
+ "touching a `public` element — a current contracts/<model key>.tsp; exits non-zero if not " +
1445
+ "ready (MIL-87, MIL-259, MIL-238)")
1330
1446
  .option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
1331
1447
  "(MIL-128, see docs/cli.md); exit codes are unchanged")
1332
1448
  .action((file, opts) => {
@@ -1347,17 +1463,18 @@ program
1347
1463
  // diagnostics printed — on an unrelated slice's breakage; scoping to this slice alone
1348
1464
  // matches the ticket's own scenario (check one slice while the rest of a large model is
1349
1465
  // still WIP) and this module's own "single-slice" framing.
1350
- const readyDiagnostics = validateSliceReady(model, refs, dirname(file), opts.sliceReady);
1351
- const combined = [...allDiagnostics, ...readyDiagnostics];
1466
+ // MIL-238: the API-first gate needs the model path as given (the contract path is built
1467
+ // from it, as `em api generate` does), its source, and the compile diagnostics.
1468
+ // R34 (MIL-268): one shared verdict helper for this flag, the MCP `slice_ready` tool, and
1469
+ // `em engagement plan`.
1470
+ const contractInput = { file, source, diagnostics };
1352
1471
  const key = opts.sliceReady;
1353
- const scoped = combined.filter((d) => d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
1472
+ const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics);
1354
1473
  printDiagnostics(scoped);
1355
- const ready = scoped.length === 0;
1356
1474
  if (opts.json) {
1357
- // MIL-128: the 4 named gates (see computeSliceReadyGates) plus the same `scoped`
1475
+ // MIL-128: the named gates (see computeSliceReadyGates) plus the same `scoped`
1358
1476
  // diagnostics and `ready` verdict driving the exit code below — replaces both the
1359
1477
  // scraped warning prose and the two hand-parsed English sentences.
1360
- const result = computeSliceReadyGates(model, refs, dirname(file), key);
1361
1478
  process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null) + "\n");
1362
1479
  }
1363
1480
  else {
@@ -1719,7 +1836,9 @@ program
1719
1836
  const emVersion = compiled
1720
1837
  .map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
1721
1838
  .filter((e) => e !== null);
1722
- const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion);
1839
+ // MIL-239 (R11): the nearest system.yaml above the first input file, consumer-adaptation counted.
1840
+ const system = statusSystemBlock(files[0], realGit, (m) => console.error(`warn: em status: ${m}`));
1841
+ const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files));
1723
1842
  let output;
1724
1843
  if (opts.json)
1725
1844
  output = buildStatusJson(report);
@@ -1951,37 +2070,180 @@ query
1951
2070
  .action((files, opts) => {
1952
2071
  runQueryVerb(files, "path", { from: opts.from, to: opts.to }, opts.json, (s) => queryPath(s, opts.from, opts.to), formatPath);
1953
2072
  });
1954
- program
2073
+ const engagement = program
2074
+ .command("engagement")
2075
+ .description("a named chunk of the model built as one stack of slice PRs: its levelled build plan and its " +
2076
+ "in-flight Ledger (MIL-268, see docs/cli.md and docs/engagement-schema.md)");
2077
+ engagement
2078
+ .command("new")
2079
+ .description("write <model dir>/engagements/<slug>.md — frontmatter (slices, parallel ceiling) plus the " +
2080
+ "generated Ledger table; selection by --slices, --context, or --downstream-of (exactly one)")
2081
+ .argument("<file>", "input .em file")
2082
+ .argument("<slug>", "engagement slug (kebab-case; the file name)")
2083
+ .option("--slices <keys>", "comma-separated slice export keys")
2084
+ .option("--context <name>", "every slice with an event in this context")
2085
+ .option("--downstream-of <ref>", "a slice key, element ref, or display name: it and everything downstream of it (loops-to excluded)")
2086
+ .option("--parallel <n>", "how many slices of one level may be in flight at once", "3")
2087
+ .option("--by <name>", "who created the engagement (createdBy)")
2088
+ .option("--force", "overwrite an existing engagement file with this slug")
2089
+ .action((file, slug, opts) => {
2090
+ const fail = (m) => {
2091
+ console.error(`em engagement new: ${m}`);
2092
+ process.exit(1);
2093
+ };
2094
+ const given = [opts.slices, opts.context, opts.downstreamOf].filter((v) => v !== undefined).length;
2095
+ if (given !== 1)
2096
+ fail("pass exactly one of --slices, --context, --downstream-of");
2097
+ const selector = opts.slices !== undefined
2098
+ ? { kind: "slices", keys: opts.slices.split(",").map((k) => k.trim()).filter((k) => k !== "") }
2099
+ : opts.context !== undefined
2100
+ ? { kind: "context", context: opts.context }
2101
+ : { kind: "downstream-of", ref: opts.downstreamOf };
2102
+ const parallel = /^\d+$/.test(opts.parallel) ? Number(opts.parallel) : NaN;
2103
+ const { model, refs } = compileFile(file);
2104
+ const path = engagementPath(file, slug);
2105
+ const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, created: localIsoDate() });
2106
+ if (!result.ok)
2107
+ return fail(result.message);
2108
+ if (existsSync(path) && !opts.force)
2109
+ fail(`engagement "${slug}" already exists (${path}) — pass --force to overwrite`);
2110
+ if (result.warning)
2111
+ console.error(result.warning);
2112
+ mkdirSync(dirname(path), { recursive: true });
2113
+ writeFileSync(path, result.text);
2114
+ console.log(`wrote ${path} (${result.units.length} slice${result.units.length === 1 ? "" : "s"}: ${result.units.join(", ")})`);
2115
+ });
2116
+ /** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
2117
+ * each slice's `--slice-ready` verdict (R34). */
2118
+ function engagementModelInput(file) {
2119
+ const { model, refs, diagnostics, source } = compileFile(file);
2120
+ const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
2121
+ return { file, model, refs, baseDir: dirname(file), contract: { file, source, diagnostics }, allDiagnostics };
2122
+ }
2123
+ engagement
2124
+ .command("plan")
2125
+ .description("the engagement's build plan: dependency levels over model.edges (loops-to excluded), and per " +
2126
+ "slice its readiness, branch impl/<key>, base (main or impl/<upstream>), and any hold")
2127
+ .argument("<file>", "input .em file")
2128
+ .argument("<slug>", "engagement slug")
2129
+ .option("--json", "print a JSON document instead of text (same document as the MCP engagement_plan tool)")
2130
+ .action((file, slug, opts) => {
2131
+ const result = engagementPlanFor(engagementModelInput(file), slug);
2132
+ if (!result.ok) {
2133
+ console.error(`em engagement plan: ${result.message}`);
2134
+ process.exit(1);
2135
+ }
2136
+ process.stdout.write((opts.json ? buildEngagementPlanJson(result.plan) : formatEngagementPlanText(result.plan)) + "\n");
2137
+ });
2138
+ engagement
2139
+ .command("set")
2140
+ .description("the only write path to the Ledger: set one slice's state (and optionally branch/base/pr); " +
2141
+ "idempotent; merged is terminal; --state held records a human hold")
2142
+ .argument("<file>", "input .em file")
2143
+ .argument("<slug>", "engagement slug")
2144
+ .argument("<key>", "slice export key")
2145
+ .requiredOption("--state <state>", `one of: ${LEDGER_STATES.join(" | ")}`)
2146
+ .option("--branch <name>", "the slice's branch")
2147
+ .option("--base <name>", "the branch the slice's PR targets")
2148
+ .option("--pr <url>", "the slice's PR URL")
2149
+ .action((file, slug, key, opts) => {
2150
+ const fail = (m) => {
2151
+ console.error(`em engagement set: ${m}`);
2152
+ process.exit(1);
2153
+ };
2154
+ const loaded = loadEngagement(file, slug);
2155
+ if (!loaded.ok)
2156
+ return fail(loaded.message);
2157
+ const { model, refs } = compileFile(file);
2158
+ const result = applyEngagementSet({ file, model, refs, baseDir: dirname(file) }, slug, loaded.text, loaded.file, { key, ...opts });
2159
+ if (!result.ok)
2160
+ return fail(result.message);
2161
+ if (!result.changed) {
2162
+ console.log(`no change: ${key} is already ${result.entry.state} (${loaded.path})`);
2163
+ return;
2164
+ }
2165
+ writeFileSync(loaded.path, result.text);
2166
+ console.log(`set ${key}: ${result.entry.state} (${loaded.path})`);
2167
+ });
2168
+ engagement
2169
+ .command("status")
2170
+ .description("the Ledger joined with each slice doc's current status, plus whether the engagement is closable")
2171
+ .argument("<file>", "input .em file")
2172
+ .argument("<slug>", "engagement slug")
2173
+ .option("--json", "print a JSON document instead of text (same document as the MCP engagement_status tool)")
2174
+ .action((file, slug, opts) => {
2175
+ const { model, refs } = compileFile(file);
2176
+ const result = engagementStatusFor({ file, model, refs, baseDir: dirname(file) }, slug);
2177
+ if (!result.ok) {
2178
+ console.error(`em engagement status: ${result.message}`);
2179
+ process.exit(1);
2180
+ }
2181
+ process.stdout.write((opts.json ? buildEngagementStatusJson(result.status) : formatEngagementStatusText(result.status)) + "\n");
2182
+ });
2183
+ engagement
2184
+ .command("close")
2185
+ .description("set status: closed — refuses unless every slice is merged (recorded or inferred) or gap")
2186
+ .argument("<file>", "input .em file")
2187
+ .argument("<slug>", "engagement slug")
2188
+ .action((file, slug) => {
2189
+ const fail = (m) => {
2190
+ console.error(`em engagement close: ${m}`);
2191
+ process.exit(1);
2192
+ };
2193
+ const { model, refs } = compileFile(file);
2194
+ const status = engagementStatusFor({ file, model, refs, baseDir: dirname(file) }, slug);
2195
+ if (!status.ok)
2196
+ return fail(status.message);
2197
+ const loaded = loadEngagement(file, slug);
2198
+ if (!loaded.ok)
2199
+ return fail(loaded.message);
2200
+ const result = applyEngagementClose(slug, loaded.text, status.status);
2201
+ if (!result.ok)
2202
+ return fail(result.message);
2203
+ if (!result.changed) {
2204
+ console.log(`no change: engagement "${slug}" is already closed (${loaded.path})`);
2205
+ return;
2206
+ }
2207
+ writeFileSync(loaded.path, result.text);
2208
+ console.log(`closed engagement "${slug}" (${loaded.path})`);
2209
+ });
2210
+ const systemCommand = program
1955
2211
  .command("system")
1956
- .description("verify a seam manifest (system.yaml) against its models' exports — every `public` event/view " +
1957
- "bound to a reaction in another model, the cross-model half of \"both ends of a flow\" (MIL-194, see docs/cli.md)")
1958
- .argument("<manifest>", "seam manifest path (YAML, or JSON); each model's `source` resolves relative to it")
2212
+ // `--json` exists on both `em system` and `em system codeowners`: parent options are only
2213
+ // recognized before the subcommand name, so the subcommand owns the one that follows it.
2214
+ .enablePositionalOptions()
2215
+ .description("verify a system — every model's `consumes` refs resolved against the other models' `public` " +
2216
+ "events/views, the cross-model half of \"both ends of a flow\" (MIL-194/MIL-235, see docs/cli.md). " +
2217
+ "Reads system.yaml when given (or found in the directory), else discovers every *.em in the repo")
2218
+ .argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
1959
2219
  // Same reasoning as `em validate`'s allowExcessArguments(false) (MIL-123): a second positional
1960
- // would be silently dropped, and "one manifest per run" is the whole contract here.
2220
+ // would be silently dropped, and "one system per run" is the whole contract here.
1961
2221
  .allowExcessArguments(false)
1962
2222
  .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
1963
- .action((manifest, opts) => {
1964
- const loaded = loadSystem(manifest);
2223
+ .action((target, opts) => {
2224
+ const shown = target ?? ".";
2225
+ const loaded = loadSystem(shown);
1965
2226
  if (!loaded.ok) {
1966
2227
  // Same refusal posture as `em export`/`em status`: a manifest that can't be parsed, or a
1967
2228
  // source that can't be read/compiled, is not a system to verify — no document, exit 1.
1968
2229
  printSystemDiagnostics(loaded.diagnostics);
1969
- console.error(`em system: not verifying — ${manifest} could not be loaded; fix the above first`);
2230
+ console.error(`em system: not verifying — ${shown} could not be loaded; fix the above first`);
1970
2231
  process.exit(1);
1971
2232
  }
1972
- const report = verifySystem(loaded.manifest, loaded.models, manifest);
2233
+ const report = verifySystem(loaded.manifest, loaded.models, loaded.manifestPath, loaded.diagnostics, { producerCommit: gitProducerCommit() });
1973
2234
  if (opts.json) {
1974
2235
  // `em validate --json`'s convention: diagnostics still go to stderr in the human format,
1975
2236
  // the document (which carries the same diagnostics) to stdout; exit code unchanged.
1976
2237
  printSystemDiagnostics(report.diagnostics);
1977
- process.stdout.write(buildSystemJson(manifest, loaded.manifestText, report) + "\n");
2238
+ process.stdout.write(buildSystemJson(loaded, report) + "\n");
1978
2239
  }
1979
2240
  else {
1980
2241
  const verified = report.seams.filter((s) => s.status === "verified").length;
1981
2242
  const failing = report.seams.length - verified;
1982
2243
  const label = report.name === null ? "system" : `system "${report.name}"`;
2244
+ const via = loaded.discovery ? ` (discovered under ${loaded.discovery.root} — no ${SYSTEM_MANIFEST_FILE})` : "";
1983
2245
  console.log(`${label}: ${report.models.length} model${report.models.length === 1 ? "" : "s"}, ` +
1984
- `${report.seams.length} seam${report.seams.length === 1 ? "" : "s"} (${verified} verified, ${failing} failing)`);
2246
+ `${report.seams.length} seam${report.seams.length === 1 ? "" : "s"} (${verified} verified, ${failing} failing)${via}`);
1985
2247
  printSystemDiagnostics(report.diagnostics);
1986
2248
  if (report.diagnostics.length === 0)
1987
2249
  console.log("ok — no issues");
@@ -1991,6 +2253,96 @@ program
1991
2253
  if (hasErrors(report.diagnostics))
1992
2254
  process.exitCode = 1;
1993
2255
  });
2256
+ systemCommand
2257
+ .command("codeowners")
2258
+ .description("generate (or --check) the managed CODEOWNERS block that routes review: each model's team on its " +
2259
+ "design directory, and every team that `consumes` a model's public surface on that model's " +
2260
+ "contract file (MIL-234, see docs/ci.md and docs/cli.md)")
2261
+ .argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
2262
+ .allowExcessArguments(false)
2263
+ .option("-o, --output <path>", "the CODEOWNERS file to splice into (default: the first of CODEOWNERS, .github/CODEOWNERS, docs/CODEOWNERS that exists, else CODEOWNERS at the repo root)")
2264
+ .option("--check", "verify the committed file already carries the generated block; never writes, exit 1 on drift (CI)")
2265
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
2266
+ .action((target, opts) => {
2267
+ const shown = target ?? ".";
2268
+ const result = runCodeowners(shown, { output: opts.output, write: !opts.check });
2269
+ if (!result.ok) {
2270
+ console.error(`em system codeowners: ${result.message}`);
2271
+ process.exitCode = 1;
2272
+ return;
2273
+ }
2274
+ for (const note of result.plan.notes)
2275
+ console.error(`note: ${note}`);
2276
+ if (opts.json) {
2277
+ process.stdout.write(buildCodeownersJson(result.file, result.plan.entries, result.status) + "\n");
2278
+ }
2279
+ else if (opts.check) {
2280
+ const hint = `run \`em system codeowners${target ? ` ${target}` : ""}${opts.output ? ` -o ${opts.output}` : ""}\` and commit the result`;
2281
+ if (result.status === "ok")
2282
+ console.log(`ok: ${result.file} carries the generated block`);
2283
+ else if (result.status === "missing")
2284
+ console.log(`missing: ${result.file} does not exist — ${hint}`);
2285
+ else if (result.status === "no-markers")
2286
+ console.log(`no-markers: ${result.file} has no GENERATED:em-codeowners block — ${hint}`);
2287
+ else
2288
+ console.log(`stale: ${result.file} differs from the generated block — ${hint}`);
2289
+ }
2290
+ else {
2291
+ const verb = result.status === "created" ? "installed" : result.status === "updated" ? "updated" : "already up to date:";
2292
+ console.log(`${verb} ${result.file} (${result.plan.entries.length} entr${result.plan.entries.length === 1 ? "y" : "ies"})`);
2293
+ }
2294
+ // --check never writes and fails on anything but ok (same vocabulary as `em ci init --check`).
2295
+ if (opts.check && result.status !== "ok")
2296
+ process.exitCode = 1;
2297
+ });
2298
+ const scopeCommand = systemCommand
2299
+ .command("scope")
2300
+ .description("check a change set against the seams (MIL-240): fail only when it alters a producer's public surface " +
2301
+ "(or contract file) AND a consuming model's design dir together (`seam-crossing`); warn on other " +
2302
+ "multi-model change sets. Files changed only by `Em-Upgrade:` commits are exempt. There is no " +
2303
+ "override - review on the contract file is the gate (see docs/cli.md, docs/ci.md)")
2304
+ .argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
2305
+ .allowExcessArguments(false)
2306
+ .option("--base <rev>", "the change set committed since <rev> (merge-base form, <rev>...HEAD); CI passes the PR base")
2307
+ .option("--staged", "also (or only) check what is staged right now; never exempted")
2308
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
2309
+ .action((target, opts) => {
2310
+ const shown = target ?? ".";
2311
+ const run = runSystemScope(shown, { base: opts.base, staged: opts.staged });
2312
+ if (!run.ok) {
2313
+ printSystemDiagnostics(run.diagnostics);
2314
+ console.error(run.message);
2315
+ process.exit(1);
2316
+ }
2317
+ const { facts, report } = run;
2318
+ // `--json` is also an option of the parent `system` command, which swallows it before this
2319
+ // subcommand sees it: read the merged options.
2320
+ const json = scopeCommand.optsWithGlobals().json === true;
2321
+ const printFindings = () => {
2322
+ for (const d of report.diagnostics) {
2323
+ const line = `${d.file}: ${d.severity} [${d.code}] ${d.message}`;
2324
+ if (d.severity === "error")
2325
+ console.error(line);
2326
+ else
2327
+ console.warn(line);
2328
+ }
2329
+ };
2330
+ if (json) {
2331
+ printFindings();
2332
+ process.stdout.write(buildScopeJson(facts, report) + "\n");
2333
+ }
2334
+ else {
2335
+ const range = [facts.base !== null ? `since ${facts.base}` : null, facts.staged ? "staged" : null].filter(Boolean).join(" + ");
2336
+ console.log(`scope ${range}: ${facts.changedPaths.length} changed path${facts.changedPaths.length === 1 ? "" : "s"}, ` +
2337
+ `${report.touched.length} model${report.touched.length === 1 ? "" : "s"} touched` +
2338
+ (facts.exemptPaths.length > 0 ? `, ${facts.exemptPaths.length} exempt (Em-Upgrade)` : ""));
2339
+ printFindings();
2340
+ if (report.diagnostics.length === 0)
2341
+ console.log("ok - no issues");
2342
+ }
2343
+ if (report.crossings > 0)
2344
+ process.exitCode = 1;
2345
+ });
1994
2346
  // Shared by install/sync/check: the `.claude/skills/` root bundled with whatever em package is
1995
2347
  // actually running (works whether em was installed from npm or run from a checkout, and
1996
2348
  // regardless of a symlinked global install — see pkgDir's own resolution above for
@@ -2005,6 +2357,14 @@ function packagedSkillsRoot() {
2005
2357
  function vendoredSkillsRoot(repoRoot) {
2006
2358
  return join(resolve(repoRoot), ".claude", "skills");
2007
2359
  }
2360
+ // MIL-269 (R32): the four em sub-agent definitions live in `.claude/agents/`, a directory shared
2361
+ // with the consumer's own agents - install/sync/check only ever handle the fixed EM_AGENT_FILES.
2362
+ function packagedAgentsRoot() {
2363
+ return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "agents");
2364
+ }
2365
+ function vendoredAgentsRoot(repoRoot) {
2366
+ return join(resolve(repoRoot), ".claude", "agents");
2367
+ }
2008
2368
  program
2009
2369
  .command("contract")
2010
2370
  .description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
@@ -2036,9 +2396,27 @@ function agentsMdMessage(result) {
2036
2396
  return `updated the agent-contract section in ${result.path}`;
2037
2397
  return `${result.path} already has an up-to-date agent-contract section`;
2038
2398
  }
2399
+ /** MIL-231: `em skill install`/`sync` keep working but the vendored bundle is deprecated in favour of
2400
+ * the em plugin. One stderr line naming the two-command install; the exit code is untouched. */
2401
+ function warnVendoredDeprecated() {
2402
+ console.error(`warn: the vendored skill bundle is deprecated since em 1.14 — install the em plugin instead: ${pluginInstallCommands(PKG_VERSION).join(" && ")}`);
2403
+ }
2039
2404
  const skill = program
2040
2405
  .command("skill")
2041
2406
  .description("manage Claude Code skills bundled with em");
2407
+ // MIL-269: reconcile the four `.claude/agents/em-*.md` files (create the directory if absent);
2408
+ // byte-exact restore, never touches any other file there.
2409
+ function installAgentFiles(repoRoot) {
2410
+ const packaged = packagedAgentsRoot();
2411
+ const vendored = vendoredAgentsRoot(repoRoot);
2412
+ const plan = planAgentSync(packaged, vendored, EM_AGENT_FILES);
2413
+ if (plan.changes.length === 0)
2414
+ return;
2415
+ applySkillSync(plan, packaged, vendored);
2416
+ for (const c of plan.changes)
2417
+ console.log(`${c.kind}: agents/${c.relPath}`);
2418
+ console.log(`installed em sub-agent definitions → ${vendored} (${plan.changes.length} file(s))`);
2419
+ }
2042
2420
  skill
2043
2421
  .command("install")
2044
2422
  .description("copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-" +
@@ -2051,6 +2429,7 @@ skill
2051
2429
  "unrelated sibling skill (MIL-180)")
2052
2430
  .option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
2053
2431
  .action(async (opts) => {
2432
+ warnVendoredDeprecated();
2054
2433
  const srcRoot = packagedSkillsRoot();
2055
2434
  const destRoot = vendoredSkillsRoot(process.cwd());
2056
2435
  const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
@@ -2082,6 +2461,7 @@ skill
2082
2461
  `(${removedCount} removed), ${totalUnchanged} unchanged`);
2083
2462
  }
2084
2463
  console.log("in Claude Code, run /event-modeling to start a guided session");
2464
+ installAgentFiles(process.cwd());
2085
2465
  }
2086
2466
  else {
2087
2467
  await mkdir(destRoot, { recursive: true });
@@ -2090,6 +2470,7 @@ skill
2090
2470
  }
2091
2471
  console.log(`installed event-modeling skill bundle → ${destRoot}`);
2092
2472
  console.log("in Claude Code, run /event-modeling to start a guided session");
2473
+ installAgentFiles(process.cwd());
2093
2474
  }
2094
2475
  if (opts.agentsMd !== false) {
2095
2476
  console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
@@ -2102,20 +2483,29 @@ skill
2102
2483
  .argument("[path]", "consumer repo root", ".")
2103
2484
  .option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
2104
2485
  .action(async (path, opts) => {
2486
+ warnVendoredDeprecated();
2105
2487
  const packagedRoot = packagedSkillsRoot();
2106
2488
  const vendoredRoot = vendoredSkillsRoot(path);
2107
2489
  const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
2108
- const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
2109
- const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0);
2490
+ const skillChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
2491
+ // MIL-269 (R32): the fixed agent-file list, never the shared .claude/agents/ directory as a whole.
2492
+ const packagedAgents = packagedAgentsRoot();
2493
+ const vendoredAgents = vendoredAgentsRoot(path);
2494
+ const agentPlan = planAgentSync(packagedAgents, vendoredAgents, EM_AGENT_FILES);
2495
+ const totalChanges = skillChanges + agentPlan.changes.length;
2496
+ const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0) + agentPlan.unchangedCount;
2110
2497
  if (totalChanges === 0) {
2111
2498
  console.log(`up to date — ${vendoredRoot} already matches the installed skill bundle (${totalUnchanged} file(s))`);
2112
2499
  }
2113
2500
  else {
2114
2501
  applySkillSyncBundle(bundlePlan, packagedRoot, vendoredRoot);
2502
+ applySkillSync(agentPlan, packagedAgents, vendoredAgents);
2115
2503
  for (const { dirName, plan } of bundlePlan) {
2116
2504
  for (const c of plan.changes)
2117
2505
  console.log(`${c.kind}: ${dirName}/${c.relPath}`);
2118
2506
  }
2507
+ for (const c of agentPlan.changes)
2508
+ console.log(`${c.kind}: agents/${c.relPath}`);
2119
2509
  console.log(`synced ${vendoredRoot} — ${totalChanges} file(s) changed, ${totalUnchanged} unchanged`);
2120
2510
  }
2121
2511
  if (opts.agentsMd !== false) {
@@ -2124,21 +2514,55 @@ skill
2124
2514
  });
2125
2515
  skill
2126
2516
  .command("check")
2127
- .description("check the vendored .claude/skills/ event-modeling skill bundle in [path] for drift against " +
2128
- "the installed em package; exits non-zero on any mismatch (CI-ready, MIL-93)")
2517
+ .description("check [path]'s em skills for drift against the installed em package: the em Claude Code plugin's " +
2518
+ "pin (settings + this machine's marketplace registry) when the repo declares it, and the vendored " +
2519
+ ".claude/skills/ bundle when present; exits non-zero on any mismatch (CI-ready, MIL-93, MIL-231)")
2129
2520
  .argument("[path]", "consumer repo root", ".")
2130
2521
  .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
2522
+ .option("--ci", "also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); " +
2523
+ "without it that finding prints the install commands on stderr and exits 0")
2131
2524
  .action((path, opts) => {
2132
2525
  const packagedRoot = packagedSkillsRoot();
2133
2526
  const vendoredRoot = vendoredSkillsRoot(path);
2134
- const result = checkSkillSyncBundle(packagedRoot, vendoredRoot, PKG_VERSION, EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES);
2527
+ const ciMode = !!opts.ci || process.env.CI === "true";
2528
+ // MIL-231: the plugin signal (a pinned marketplace in .claude/settings*.json) sits beside the
2529
+ // vendored signal (the anchor directory). Plugin only -> only the plugin is checked; vendored
2530
+ // only -> exactly the pre-1.14 behavior; both -> both; neither -> skill-check-not-installed.
2531
+ const pluginCheck = checkPlugin(resolve(path), PKG_VERSION, claudePluginsDir());
2532
+ const vendoredPresent = existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR));
2533
+ const vendored = pluginCheck && !vendoredPresent
2534
+ ? { findings: [], ok: true }
2535
+ : checkSkillSyncBundle(packagedRoot, vendoredRoot, PKG_VERSION, EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES);
2536
+ // MIL-269 (R32): the four agent files, only when the vendored bundle is present (a plugin-only
2537
+ // repo ships them via the plugin); checked by fixed name, never the shared directory.
2538
+ const agents = vendoredPresent
2539
+ ? checkAgentFiles(packagedAgentsRoot(), vendoredAgentsRoot(path), EM_AGENT_FILES)
2540
+ : { findings: [], ok: true };
2541
+ const findings = [...vendored.findings, ...agents.findings, ...(pluginCheck?.findings ?? [])];
2542
+ const failing = findings.filter((f) => f.code !== "plugin-not-installed-locally" || ciMode);
2543
+ const ok = failing.length === 0;
2544
+ const result = { findings, ok };
2135
2545
  if (opts.json) {
2136
- process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION) + "\n");
2546
+ process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION, pluginCheck?.plugin ?? null) + "\n");
2137
2547
  }
2138
2548
  else {
2139
- for (const f of result.findings)
2140
- console.log(f.message);
2141
- console.log(result.ok ? `ok — vendored skill matches em ${PKG_VERSION}` : `${result.findings.length} mismatch(es)`);
2549
+ for (const f of findings) {
2550
+ if (f.code === "plugin-not-installed-locally" && !ciMode)
2551
+ console.error(f.message);
2552
+ else
2553
+ console.log(f.message);
2554
+ }
2555
+ if (!ok)
2556
+ console.log(`${failing.length} mismatch(es)`);
2557
+ else {
2558
+ if (!pluginCheck || vendoredPresent)
2559
+ console.log(`ok — vendored skill matches em ${PKG_VERSION}`);
2560
+ if (pluginCheck) {
2561
+ const unregistered = findings.some((f) => f.code === "plugin-not-installed-locally");
2562
+ console.log(`ok — em plugin ${pluginCheck.plugin.name} (${pluginCheck.plugin.ref}) matches em ${PKG_VERSION}` +
2563
+ (unregistered ? " (not registered on this machine - see the install commands on stderr)" : ""));
2564
+ }
2565
+ }
2142
2566
  }
2143
2567
  // MIL-219 ruling A: `em skill check` also warns when [path]'s own state file records an
2144
2568
  // `Em version:` behind the installed em — best-effort (no state file, or an unparseable
@@ -2154,7 +2578,7 @@ skill
2154
2578
  }
2155
2579
  // Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
2156
2580
  // a pipe (a --json document) shouldn't risk truncation.
2157
- if (!result.ok)
2581
+ if (!ok)
2158
2582
  process.exitCode = 1;
2159
2583
  });
2160
2584
  const ci = program.command("ci").description("scaffold/check the CI enforcement preset (MIL-166, see docs/ci.md)");
@@ -2163,10 +2587,12 @@ ci.command("init")
2163
2587
  "coverage --strict, em ledger, em skill check, em glossary --fail-on-conflicts, plus a " +
2164
2588
  "push-triggered status-badge rebuild) and em-conform.yml (scheduled, advisory-only " +
2165
2589
  "conformance cadence) — same install discipline as `em skill install`: marker-delimited, " +
2166
- "idempotent, --check for CI self-verification (MIL-166, see docs/ci.md)")
2167
- .argument("<model>", "anchor .em file the coverage/ledger/slice-index/status steps point at")
2590
+ "idempotent, --check for CI self-verification (MIL-166, see docs/ci.md). Give it a " +
2591
+ "system.yaml (or its directory) instead of a model to cover every model in the system: one " +
2592
+ "workflow, per-model jobs, a conform matrix (MIL-233)")
2593
+ .argument("<model>", "anchor .em file the coverage/ledger/slice-index/status steps point at, or a system.yaml (or a directory holding one) to cover every model in it")
2168
2594
  .option("--tests <dir>", "test directory the coverage/status steps scan for INV-* citations", "test")
2169
- .option("-f, --force", "replace an existing workflow file that has no GENERATED markers, or a managed block generated for a different model")
2595
+ .option("-f, --force", "replace an existing workflow file that has no GENERATED markers, or a managed block generated for a different set of models")
2170
2596
  .option("--check", "verify both files match the current preset; exit non-zero on drift without writing (CI)")
2171
2597
  .action((model, opts) => {
2172
2598
  const unsafe = findUnsafeCiInitArg(model) ?? findUnsafeCiInitArg(opts.tests);
@@ -2178,24 +2604,52 @@ ci.command("init")
2178
2604
  const repoRoot = process.cwd();
2179
2605
  const ciPath = ciWorkflowPath(repoRoot);
2180
2606
  const conformPath = conformWorkflowPath(repoRoot);
2181
- const files = [
2182
- [
2183
- ciPath,
2184
- planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
2185
- ],
2186
- [
2187
- conformPath,
2188
- planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION), conformManagedBody(model, PKG_VERSION), CONFORM_WORKFLOW_MARKER, !!opts.force),
2189
- ],
2190
- ];
2191
- // MIL-256 (#174): the managed block is single-model (multi-model: MIL-233). Another model's
2192
- // block is its own condition - never `stale`, never silently replaced.
2193
- const otherModel = (previous, path) => `${path} was generated for ${previous}, not ${model} - the managed block is single-model until multi-model support lands (MIL-233)`;
2607
+ // MIL-231: a repo that declares the em plugin gets a conform workflow that installs the plugin
2608
+ // and runs /em:conform; any other repo keeps the vendored install lines.
2609
+ const usePlugin = detectPlugin(repoRoot) !== null;
2610
+ // MIL-233: a system manifest (a .yaml/.yml file, or a directory) covers every model in it;
2611
+ // anything else is the single-model form, unchanged.
2612
+ const isDir = existsSync(model) && statSync(model).isDirectory();
2613
+ const manifestForm = isDir || /\.ya?ml$/i.test(model);
2614
+ let files;
2615
+ if (manifestForm) {
2616
+ const loaded = ciModelsFromManifest(model, repoRoot);
2617
+ if (!loaded.ok) {
2618
+ console.error(`em ci init: ${loaded.message}`);
2619
+ process.exit(1);
2620
+ }
2621
+ files = [
2622
+ [
2623
+ ciPath,
2624
+ planCiFile(ciPath, buildCiWorkflowFileMulti(model, loaded.models, opts.tests, PKG_VERSION), ciManagedBodyMulti(loaded.models, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force, true),
2625
+ ],
2626
+ [
2627
+ conformPath,
2628
+ planCiFile(conformPath, buildConformWorkflowFileMulti(model, loaded.models, PKG_VERSION, usePlugin), conformManagedBodyMulti(loaded.models, PKG_VERSION, usePlugin), CONFORM_WORKFLOW_MARKER, !!opts.force, true),
2629
+ ],
2630
+ ];
2631
+ }
2632
+ else {
2633
+ files = [
2634
+ [
2635
+ ciPath,
2636
+ planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
2637
+ ],
2638
+ [
2639
+ conformPath,
2640
+ planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION, usePlugin), conformManagedBody(model, PKG_VERSION, usePlugin), CONFORM_WORKFLOW_MARKER, !!opts.force),
2641
+ ],
2642
+ ];
2643
+ }
2644
+ // MIL-256 (#174) / MIL-233: a managed block is identified by the SET of models it names. A
2645
+ // disjoint set - or a single-model argument against a multi-model block - is its own
2646
+ // condition: never `stale`, never silently replaced.
2647
+ const otherModels = (status, path) => describeOtherModels(path, status);
2194
2648
  if (opts.check) {
2195
2649
  let drift = false;
2196
2650
  for (const [path, status] of files) {
2197
- if (status.kind === "other-model" || status.kind === "replace-model") {
2198
- console.log(`different model: ${otherModel(status.previous, path)} (re-run \`em ci init ${model} --force\` to replace it)`);
2651
+ if (status.kind === "other-models" || status.kind === "replace-models") {
2652
+ console.log(`different models: ${otherModels(status, path)}`);
2199
2653
  drift = true;
2200
2654
  }
2201
2655
  else if (status.kind === "create") {
@@ -2207,7 +2661,11 @@ ci.command("init")
2207
2661
  drift = true;
2208
2662
  }
2209
2663
  else if (status.kind === "stale") {
2210
- console.log(`stale: ${path} — run \`em ci init ${model}\` and commit the result`);
2664
+ const marker = path === ciPath ? CI_WORKFLOW_MARKER : CONFORM_WORKFLOW_MARKER;
2665
+ const change = manifestForm
2666
+ ? describeSetChange(fileBlockModels(status.current, marker), fileBlockModels(status.content, marker))
2667
+ : null;
2668
+ console.log(`stale: ${path} — run \`em ci init ${model}\` and commit the result${change ? ` (${change})` : ""}`);
2211
2669
  drift = true;
2212
2670
  }
2213
2671
  else {
@@ -2220,11 +2678,11 @@ ci.command("init")
2220
2678
  return;
2221
2679
  }
2222
2680
  // Refuse before writing anything, so a refusal on one file never leaves the other updated.
2223
- const refused = files.filter(([, s]) => s.kind === "other-model");
2681
+ const refused = files.filter(([, s]) => s.kind === "other-models");
2224
2682
  if (refused.length > 0) {
2225
2683
  for (const [path, status] of refused) {
2226
- if (status.kind === "other-model")
2227
- console.error(`em ci init: ${otherModel(status.previous, path)}; re-run with --force to replace it`);
2684
+ if (status.kind === "other-models")
2685
+ console.error(`em ci init: ${otherModels(status, path)}`);
2228
2686
  }
2229
2687
  process.exitCode = 1;
2230
2688
  return;
@@ -2232,8 +2690,8 @@ ci.command("init")
2232
2690
  mkdirSync(dirname(ciPath), { recursive: true });
2233
2691
  for (const [path, status] of files) {
2234
2692
  applyCiFile(path, status);
2235
- if (status.kind === "replace-model")
2236
- console.log(`replaced ${path} (--force; was generated for ${status.previous})`);
2693
+ if (status.kind === "replace-models")
2694
+ console.log(`replaced ${path} (--force; was generated for ${status.previous.join(", ")})`);
2237
2695
  else if (status.kind === "create")
2238
2696
  console.log(`installed ${path}`);
2239
2697
  else if (status.kind === "stale")
@@ -2289,6 +2747,7 @@ program
2289
2747
  repoRoot: repoRootResult.repoRoot,
2290
2748
  installedVersion: PKG_VERSION,
2291
2749
  packagedSkillsRoot: packagedSkillsRoot(),
2750
+ packagedAgentsRoot: packagedAgentsRoot(),
2292
2751
  model,
2293
2752
  refs,
2294
2753
  };
@@ -2407,6 +2866,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
2407
2866
  ...validateDocModelConsistency(model, refs, dirname(file)),
2408
2867
  ...validateOrphanedSliceDocs(model, refs, dirname(file)),
2409
2868
  ...validateModelVersionStale(model, refs, dirname(file), source),
2869
+ ...validateInvariants(model, refs, dirname(file)),
2410
2870
  ];
2411
2871
  }
2412
2872
  /** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
@@ -2512,7 +2972,7 @@ function printDivergences(model) {
2512
2972
  * cross-model `public` view's reader legitimately lives outside this file (see
2513
2973
  * `publicViewsWithoutInModelReader`'s own header for why this stays advisory). */
2514
2974
  function printPublicElements(model) {
2515
- const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
2975
+ const pub = model.elements.filter((el) => el.public && (el.kind === "command" || el.kind === "event" || el.kind === "view"));
2516
2976
  if (pub.length === 0) {
2517
2977
  console.log("no public elements");
2518
2978
  return;