@milehimikey/em 1.13.0 → 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 (239) 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 +52 -12
  6. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  7. package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
  8. package/.claude/skills/event-modeling-design/SKILL.md +140 -18
  9. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  10. package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
  11. package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
  12. package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
  13. package/.claude/skills/event-modeling-implement/reference/implement.md +51 -12
  14. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  15. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -188
  16. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
  17. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +116 -4
  18. package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
  19. package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
  20. package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
  21. package/.claude-plugin/marketplace.json +14 -0
  22. package/README.md +25 -11
  23. package/dist/catalog/apiFirst.d.ts +28 -0
  24. package/dist/catalog/apiFirst.js +49 -0
  25. package/dist/catalog/apiFirst.js.map +1 -0
  26. package/dist/catalog/docJoin.d.ts +13 -0
  27. package/dist/catalog/docJoin.js +11 -0
  28. package/dist/catalog/docJoin.js.map +1 -1
  29. package/dist/catalog/docModelConsistencyValidate.js +44 -4
  30. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  31. package/dist/catalog/invariantsValidate.d.ts +4 -0
  32. package/dist/catalog/invariantsValidate.js +102 -0
  33. package/dist/catalog/invariantsValidate.js.map +1 -0
  34. package/dist/catalog/scenarios.d.ts +15 -0
  35. package/dist/catalog/scenarios.js +97 -0
  36. package/dist/catalog/scenarios.js.map +1 -0
  37. package/dist/catalog/sliceDoc.d.ts +8 -0
  38. package/dist/catalog/sliceDoc.js +9 -0
  39. package/dist/catalog/sliceDoc.js.map +1 -1
  40. package/dist/catalog/sliceReadyValidate.d.ts +31 -4
  41. package/dist/catalog/sliceReadyValidate.js +66 -8
  42. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  43. package/dist/catalog/sliceSections.d.ts +55 -0
  44. package/dist/catalog/sliceSections.js +317 -0
  45. package/dist/catalog/sliceSections.js.map +1 -0
  46. package/dist/cli/agentsMd.js +7 -1
  47. package/dist/cli/agentsMd.js.map +1 -1
  48. package/dist/cli/api.d.ts +90 -0
  49. package/dist/cli/api.js +318 -0
  50. package/dist/cli/api.js.map +1 -0
  51. package/dist/cli/ciInit.d.ts +88 -3
  52. package/dist/cli/ciInit.js +507 -50
  53. package/dist/cli/ciInit.js.map +1 -1
  54. package/dist/cli/conformScope.d.ts +28 -0
  55. package/dist/cli/conformScope.js +67 -0
  56. package/dist/cli/conformScope.js.map +1 -1
  57. package/dist/cli/coverage.d.ts +25 -2
  58. package/dist/cli/coverage.js +86 -12
  59. package/dist/cli/coverage.js.map +1 -1
  60. package/dist/cli/engagement.d.ts +210 -0
  61. package/dist/cli/engagement.js +444 -0
  62. package/dist/cli/engagement.js.map +1 -0
  63. package/dist/cli/engagementFile.d.ts +86 -0
  64. package/dist/cli/engagementFile.js +255 -0
  65. package/dist/cli/engagementFile.js.map +1 -0
  66. package/dist/cli/metrics.d.ts +10 -0
  67. package/dist/cli/metrics.js +49 -1
  68. package/dist/cli/metrics.js.map +1 -1
  69. package/dist/cli/pluginPin.d.ts +55 -0
  70. package/dist/cli/pluginPin.js +188 -0
  71. package/dist/cli/pluginPin.js.map +1 -0
  72. package/dist/cli/ratify.d.ts +27 -2
  73. package/dist/cli/ratify.js +112 -13
  74. package/dist/cli/ratify.js.map +1 -1
  75. package/dist/cli/reratify.d.ts +11 -4
  76. package/dist/cli/reratify.js +89 -35
  77. package/dist/cli/reratify.js.map +1 -1
  78. package/dist/cli/scopeInputs.d.ts +48 -0
  79. package/dist/cli/scopeInputs.js +176 -0
  80. package/dist/cli/scopeInputs.js.map +1 -0
  81. package/dist/cli/skillCheck.d.ts +15 -1
  82. package/dist/cli/skillCheck.js +22 -1
  83. package/dist/cli/skillCheck.js.map +1 -1
  84. package/dist/cli/skillDirs.d.ts +5 -0
  85. package/dist/cli/skillDirs.js +13 -1
  86. package/dist/cli/skillDirs.js.map +1 -1
  87. package/dist/cli/skillSync.d.ts +4 -0
  88. package/dist/cli/skillSync.js +23 -0
  89. package/dist/cli/skillSync.js.map +1 -1
  90. package/dist/cli/sliceNew.d.ts +11 -7
  91. package/dist/cli/sliceNew.js +91 -18
  92. package/dist/cli/sliceNew.js.map +1 -1
  93. package/dist/cli/sliceSync.d.ts +50 -0
  94. package/dist/cli/sliceSync.js +170 -0
  95. package/dist/cli/sliceSync.js.map +1 -0
  96. package/dist/cli/status.d.ts +24 -1
  97. package/dist/cli/status.js +20 -1
  98. package/dist/cli/status.js.map +1 -1
  99. package/dist/cli/systemInputs.d.ts +53 -10
  100. package/dist/cli/systemInputs.js +186 -11
  101. package/dist/cli/systemInputs.js.map +1 -1
  102. package/dist/cli/upgrade.d.ts +30 -7
  103. package/dist/cli/upgrade.js +434 -50
  104. package/dist/cli/upgrade.js.map +1 -1
  105. package/dist/cli/usageLog.d.ts +4 -2
  106. package/dist/cli/usageLog.js +4 -2
  107. package/dist/cli/usageLog.js.map +1 -1
  108. package/dist/cli.js +631 -100
  109. package/dist/cli.js.map +1 -1
  110. package/dist/emit/api.d.ts +29 -0
  111. package/dist/emit/api.js +220 -0
  112. package/dist/emit/api.js.map +1 -0
  113. package/dist/emit/apiCheckJson.d.ts +3 -0
  114. package/dist/emit/apiCheckJson.js +21 -0
  115. package/dist/emit/apiCheckJson.js.map +1 -0
  116. package/dist/emit/codeownersJson.d.ts +3 -0
  117. package/dist/emit/codeownersJson.js +19 -0
  118. package/dist/emit/codeownersJson.js.map +1 -0
  119. package/dist/emit/coverageJson.d.ts +5 -2
  120. package/dist/emit/coverageJson.js +6 -2
  121. package/dist/emit/coverageJson.js.map +1 -1
  122. package/dist/emit/diffJson.d.ts +1 -1
  123. package/dist/emit/diffJson.js +8 -1
  124. package/dist/emit/diffJson.js.map +1 -1
  125. package/dist/emit/engagementJson.d.ts +8 -0
  126. package/dist/emit/engagementJson.js +46 -0
  127. package/dist/emit/engagementJson.js.map +1 -0
  128. package/dist/emit/glossaryJson.d.ts +1 -1
  129. package/dist/emit/glossaryJson.js +4 -1
  130. package/dist/emit/glossaryJson.js.map +1 -1
  131. package/dist/emit/json.d.ts +17 -1
  132. package/dist/emit/json.js +30 -1
  133. package/dist/emit/json.js.map +1 -1
  134. package/dist/emit/metricsJson.d.ts +1 -1
  135. package/dist/emit/metricsJson.js +4 -1
  136. package/dist/emit/metricsJson.js.map +1 -1
  137. package/dist/emit/queryJson.d.ts +1 -1
  138. package/dist/emit/queryJson.js +7 -1
  139. package/dist/emit/queryJson.js.map +1 -1
  140. package/dist/emit/scopeJson.d.ts +12 -0
  141. package/dist/emit/scopeJson.js +26 -0
  142. package/dist/emit/scopeJson.js.map +1 -0
  143. package/dist/emit/skillCheckJson.d.ts +3 -2
  144. package/dist/emit/skillCheckJson.js +10 -2
  145. package/dist/emit/skillCheckJson.js.map +1 -1
  146. package/dist/emit/statusJson.d.ts +1 -1
  147. package/dist/emit/statusJson.js +15 -1
  148. package/dist/emit/statusJson.js.map +1 -1
  149. package/dist/emit/systemJson.d.ts +15 -8
  150. package/dist/emit/systemJson.js +28 -13
  151. package/dist/emit/systemJson.js.map +1 -1
  152. package/dist/emit/upgradeJson.d.ts +1 -1
  153. package/dist/emit/upgradeJson.js +4 -1
  154. package/dist/emit/upgradeJson.js.map +1 -1
  155. package/dist/emit/validateJson.d.ts +3 -3
  156. package/dist/emit/validateJson.js +10 -4
  157. package/dist/emit/validateJson.js.map +1 -1
  158. package/dist/mcp/server.d.ts +1 -1
  159. package/dist/mcp/server.js +186 -37
  160. package/dist/mcp/server.js.map +1 -1
  161. package/dist/model/diff.d.ts +4 -1
  162. package/dist/model/diff.js +44 -8
  163. package/dist/model/diff.js.map +1 -1
  164. package/dist/model/glossary.d.ts +2 -0
  165. package/dist/model/glossary.js +1 -0
  166. package/dist/model/glossary.js.map +1 -1
  167. package/dist/model/model.d.ts +55 -2
  168. package/dist/model/model.js +57 -0
  169. package/dist/model/model.js.map +1 -1
  170. package/dist/model/qualifiedRef.d.ts +18 -0
  171. package/dist/model/qualifiedRef.js +26 -0
  172. package/dist/model/qualifiedRef.js.map +1 -1
  173. package/dist/model/queryIndex.d.ts +9 -1
  174. package/dist/model/queryIndex.js +18 -2
  175. package/dist/model/queryIndex.js.map +1 -1
  176. package/dist/model/rules.d.ts +143 -2
  177. package/dist/model/rules.js +162 -3
  178. package/dist/model/rules.js.map +1 -1
  179. package/dist/model/validate.d.ts +40 -0
  180. package/dist/model/validate.js +147 -1
  181. package/dist/model/validate.js.map +1 -1
  182. package/dist/parser/ast.d.ts +31 -3
  183. package/dist/parser/parser.js +148 -14
  184. package/dist/parser/parser.js.map +1 -1
  185. package/dist/query/format.js +1 -1
  186. package/dist/query/format.js.map +1 -1
  187. package/dist/query/verbs.d.ts +6 -0
  188. package/dist/query/verbs.js +6 -2
  189. package/dist/query/verbs.js.map +1 -1
  190. package/dist/system/adaptation.d.ts +14 -0
  191. package/dist/system/adaptation.js +49 -0
  192. package/dist/system/adaptation.js.map +1 -0
  193. package/dist/system/codeowners.d.ts +77 -0
  194. package/dist/system/codeowners.js +242 -0
  195. package/dist/system/codeowners.js.map +1 -0
  196. package/dist/system/manifest.d.ts +10 -5
  197. package/dist/system/manifest.js +54 -26
  198. package/dist/system/manifest.js.map +1 -1
  199. package/dist/system/migrateManifest.d.ts +28 -0
  200. package/dist/system/migrateManifest.js +155 -0
  201. package/dist/system/migrateManifest.js.map +1 -0
  202. package/dist/system/scope.d.ts +46 -0
  203. package/dist/system/scope.js +132 -0
  204. package/dist/system/scope.js.map +1 -0
  205. package/dist/system/verify.d.ts +50 -13
  206. package/dist/system/verify.js +179 -58
  207. package/dist/system/verify.js.map +1 -1
  208. package/dist/templates.d.ts +2 -2
  209. package/dist/templates.js +23 -2
  210. package/dist/templates.js.map +1 -1
  211. package/package.json +6 -3
  212. package/plugin/.claude-plugin/plugin.json +18 -0
  213. package/plugin/agents/em-critic.md +27 -0
  214. package/plugin/agents/em-implementer.md +35 -0
  215. package/plugin/agents/em-reviewer.md +32 -0
  216. package/plugin/agents/em-validator.md +28 -0
  217. package/plugin/shared/reference/em-dsl.md +866 -0
  218. package/plugin/shared/reference/methodology.md +329 -0
  219. package/plugin/shared/reference/operating-principles.md +222 -0
  220. package/plugin/shared/reference/slice-doc-schema.md +566 -0
  221. package/plugin/shared/templates/conformance-report.md +85 -0
  222. package/plugin/shared/templates/constitution.md +136 -0
  223. package/plugin/shared/templates/model-readme.md +46 -0
  224. package/plugin/shared/templates/slice.md +251 -0
  225. package/plugin/shared/templates/state.md +77 -0
  226. package/plugin/skills/conform/SKILL.md +93 -0
  227. package/plugin/skills/conform/reference/conform.md +428 -0
  228. package/plugin/skills/design/SKILL.md +297 -0
  229. package/plugin/skills/discover/SKILL.md +90 -0
  230. package/plugin/skills/discover/reference/extract.md +147 -0
  231. package/plugin/skills/engagement/SKILL.md +193 -0
  232. package/plugin/skills/engagement/reference/engagement.md +144 -0
  233. package/plugin/skills/event-modeling/SKILL.md +132 -0
  234. package/plugin/skills/implement/SKILL.md +61 -0
  235. package/plugin/skills/implement/reference/implement.md +439 -0
  236. package/plugin/skills/review/SKILL.md +74 -0
  237. package/dist/emit/typespec.d.ts +0 -26
  238. package/dist/emit/typespec.js +0 -356
  239. package/dist/emit/typespec.js.map +0 -1
@@ -21,32 +21,55 @@
21
21
  // ci-block/constitution check needs. Requires the model directory to be inside a git
22
22
  // repository — same requirement `em conform-scope` holds, for the same reason (there is no
23
23
  // "changed since when" or "commit per step" without one).
24
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
25
- import { join } from "node:path";
24
+ import { existsSync, readFileSync, realpathSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
25
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
26
+ import { PUBLIC_SCALAR_TYPE_NAMES } from "../model/model.js";
27
+ import { findUnresolvedPublicFieldTypes } from "../model/validate.js";
26
28
  import { realGit } from "./diff-inputs.js";
27
29
  import { resolveSliceDocJoin } from "../catalog/docJoin.js";
28
30
  import { validateOrphanedSliceDocs } from "../catalog/orphanedSliceDocValidate.js";
29
31
  import { planMigration, verifyMigration } from "./migrateReactionShape.js";
30
- import { planSkillSyncBundle, applySkillSyncBundle } from "./skillSync.js";
31
- import { EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR } from "./skillDirs.js";
32
- import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./ciInit.js";
32
+ import { planSkillSyncBundle, applySkillSyncBundle, planAgentSync, applySkillSync } from "./skillSync.js";
33
+ import { EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR, EM_AGENT_FILES } from "./skillDirs.js";
34
+ import { detectPlugin, checkPlugin, claudePluginsDir, mergePluginSettings, pluginInstallCommands, pluginSettingsEntries } from "./pluginPin.js";
35
+ import { syncAgentsMd } from "./agentsMd.js";
36
+ import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, buildCiWorkflowFileMulti, buildConformWorkflowFileMulti, ciManagedBody, ciManagedBodyMulti, conformManagedBody, conformManagedBodyMulti, ciModelsFromManifest, planCiFile, applyCiFile, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./ciInit.js";
33
37
  import { findSpecifyRoot } from "./status.js";
34
- import { scaffoldConstitution } from "../templates.js";
35
- import { loadStateFile, parseState, setEmVersion, ensureOptionalBullets } from "./stateFile.js";
38
+ import { scaffoldConstitution, scaffoldStateFile } from "../templates.js";
39
+ import { loadStateFile, parseState, setEmVersion, ensureOptionalBullets, resolveStateFilePath } from "./stateFile.js";
36
40
  import { fieldLineRegex, normalizeFieldValue, locateFrontmatterInner } from "./frontmatterSurgery.js";
37
41
  import { localIsoDate } from "../util/localDate.js";
42
+ import { compile } from "../pipeline.js";
43
+ import { hasErrors } from "../model/validate.js";
44
+ import { LEGACY_SYSTEM_MANIFEST_SCHEMA_VERSION, parseManifest } from "../system/manifest.js";
45
+ import { planManifestMigration } from "../system/migrateManifest.js";
46
+ import { loadSource, SYSTEM_MANIFEST_FILE } from "./systemInputs.js";
38
47
  export const UPGRADE_MIN_SUPPORTED_VERSION = "1.6.0";
39
48
  // ---- Step 1: skill-bundle — delegates to `em skill sync`'s own plan/apply. ----
40
49
  function vendoredSkillsRootOf(repoRoot) {
41
50
  return join(repoRoot, ".claude", "skills");
42
51
  }
52
+ function vendoredAgentsRootOf(repoRoot) {
53
+ return join(repoRoot, ".claude", "agents");
54
+ }
55
+ function packagedAgentsRootOf(ctx) {
56
+ return ctx.packagedAgentsRoot ?? join(ctx.packagedSkillsRoot, "..", "agents");
57
+ }
43
58
  function detectSkillBundle(ctx) {
44
59
  const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
60
+ // MIL-231/232: a repo that declares the plugin has no bundle to sync (any stray vendored copy is
61
+ // the `skill-plugin` step's business, not this one's).
62
+ if (detectPlugin(ctx.repoRoot))
63
+ return { applicable: false, reason: "plugin repo — nothing to sync" };
64
+ // MIL-232: the vendored bundle is about to be removed by `skill-plugin` (ordered first) — syncing it would be a throwaway commit.
65
+ if (detectSkillPlugin(ctx).applicable)
66
+ return { applicable: false, reason: "the vendored bundle is migrated to the plugin by skill-plugin — nothing to sync" };
45
67
  if (!existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR))) {
46
68
  return { applicable: false, reason: "no vendored skill bundle installed at .claude/skills/ — run `em skill install` first if you want one" };
47
69
  }
48
70
  const bundlePlan = planSkillSyncBundle(ctx.packagedSkillsRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
49
- const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
71
+ const agentPlan = planAgentSync(packagedAgentsRootOf(ctx), vendoredAgentsRootOf(ctx.repoRoot), EM_AGENT_FILES);
72
+ const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0) + agentPlan.changes.length;
50
73
  if (totalChanges === 0)
51
74
  return { applicable: false, reason: "vendored skill bundle already matches the installed em" };
52
75
  return { applicable: true, reason: `vendored skill bundle differs from the installed em in ${totalChanges} file(s)` };
@@ -54,11 +77,71 @@ function detectSkillBundle(ctx) {
54
77
  function applySkillBundle(ctx) {
55
78
  const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
56
79
  const bundlePlan = planSkillSyncBundle(ctx.packagedSkillsRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
57
- const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
80
+ const vendoredAgents = vendoredAgentsRootOf(ctx.repoRoot);
81
+ const agentPlan = planAgentSync(packagedAgentsRootOf(ctx), vendoredAgents, EM_AGENT_FILES);
82
+ const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0) + agentPlan.changes.length;
58
83
  if (totalChanges === 0)
59
84
  return { ok: false, message: "nothing to sync" };
60
85
  applySkillSyncBundle(bundlePlan, ctx.packagedSkillsRoot, vendoredRoot);
61
- const changedFiles = bundlePlan.flatMap(({ dirName, plan }) => plan.changes.map((c) => join(".claude", "skills", dirName, c.relPath)));
86
+ applySkillSync(agentPlan, packagedAgentsRootOf(ctx), vendoredAgents);
87
+ const changedFiles = [
88
+ ...bundlePlan.flatMap(({ dirName, plan }) => plan.changes.map((c) => join(".claude", "skills", dirName, c.relPath))),
89
+ ...agentPlan.changes.map((c) => join(".claude", "agents", c.relPath)),
90
+ ];
91
+ return { ok: true, changedFiles };
92
+ }
93
+ // ---- Step 1b: skill-plugin (MIL-232, R14) — vendored bundle -> pinned em plugin, one commit. ----
94
+ function detectSkillPlugin(ctx) {
95
+ const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
96
+ if (!existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR))) {
97
+ return { applicable: false, reason: "no vendored skill bundle installed at .claude/skills/ — nothing to migrate to the plugin" };
98
+ }
99
+ if (detectPlugin(ctx.repoRoot))
100
+ return { applicable: false, reason: "plugin already declared in .claude/settings.json" };
101
+ const settingsPath = join(ctx.repoRoot, ".claude", "settings.json");
102
+ if (existsSync(settingsPath) && mergePluginSettings(readFileSync(settingsPath, "utf8"), ctx.installedVersion) === null) {
103
+ return { applicable: false, reason: ".claude/settings.json is not a JSON object — fix it by hand, then re-run em upgrade" };
104
+ }
105
+ const e = pluginSettingsEntries(ctx.installedVersion);
106
+ return {
107
+ applicable: true,
108
+ reason: `vendored skill bundle → em plugin: remove ${EM_ALL_SKILL_BUNDLE_DIRS.map((d) => `.claude/skills/${d}`).join(", ")} and any of ${EM_AGENT_FILES.map((f) => `.claude/agents/${f}`).join(", ")}; ` +
109
+ `add extraKnownMarketplaces["${e.name}"] (github ${e.marketplace.source.repo}@${e.marketplace.source.ref}) and enabledPlugins["${e.enabledKey}"] to .claude/settings.json; ` +
110
+ `refresh the AGENTS.md managed section with the /em:* skill names`,
111
+ };
112
+ }
113
+ function applySkillPlugin(ctx) {
114
+ const settingsPath = join(ctx.repoRoot, ".claude", "settings.json");
115
+ const existing = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null;
116
+ // Verify before any write: an unparseable settings.json must not cost the user their skills.
117
+ const merged = mergePluginSettings(existing, ctx.installedVersion);
118
+ if (merged === null)
119
+ return { ok: false, message: ".claude/settings.json is not a JSON object — nothing changed" };
120
+ const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
121
+ const changedFiles = [];
122
+ // Only the bundle's own directories; a sibling skill under .claude/skills/ is never touched.
123
+ for (const d of EM_ALL_SKILL_BUNDLE_DIRS) {
124
+ const target = join(vendoredRoot, d);
125
+ if (!existsSync(target))
126
+ continue;
127
+ rmSync(target, { recursive: true, force: true });
128
+ changedFiles.push(join(".claude", "skills", d));
129
+ }
130
+ // MIL-269 (R32): exactly the four em agent files (the plugin ships them as em:em-*); any other
131
+ // agent in the shared directory is never listed or touched.
132
+ for (const f of EM_AGENT_FILES) {
133
+ const target = join(vendoredAgentsRootOf(ctx.repoRoot), f);
134
+ if (!existsSync(target))
135
+ continue;
136
+ rmSync(target, { force: true });
137
+ changedFiles.push(join(".claude", "agents", f));
138
+ }
139
+ mkdirSync(dirname(settingsPath), { recursive: true });
140
+ writeFileSync(settingsPath, merged);
141
+ changedFiles.push(join(".claude", "settings.json"));
142
+ const agents = syncAgentsMd(ctx.repoRoot);
143
+ if (agents.wrote)
144
+ changedFiles.push("AGENTS.md");
62
145
  return { ok: true, changedFiles };
63
146
  }
64
147
  // ---- Step 2: reaction-shape — delegates to `em migrate`'s own plan/verify/apply. ----
@@ -91,13 +174,40 @@ function applyReactionShape(ctx) {
91
174
  writeFileSync(ctx.modelFile, plan.rewritten);
92
175
  return { ok: true, changedFiles: [ctx.modelFile] };
93
176
  }
94
- // ---- Step 3: state-file — ensure Model version:/Certified: are present (MIL-218 defaults). ----
95
- // `Em version:` itself is deliberately NOT this step's job — `runUpgradeApply` always writes it
96
- // as its own dedicated final commit, whether or not this step ran (module header).
97
- function detectStateFile(ctx) {
177
+ // ---- Step 3: state-file — scaffold when absent (MIL-257); else ensure Model version:/Certified:
178
+ // are present (MIL-218 defaults). ----
179
+ // `Em version:` itself is deliberately NOT this step's job when the file already exists —
180
+ // `runUpgradeApply` always writes it as its own dedicated final commit, whether or not this step
181
+ // ran (module header). A freshly scaffolded file is written already carrying the installed
182
+ // version (scaffoldStateFile's own `Em version:` fill), so that final commit is then a no-op.
183
+ /** MIL-257: the one human-readable sentence for "the state file exists but `parseState` rejected
184
+ * it", naming the file so the `--check` final line / `--apply` refusal is self-explanatory.
185
+ * `parseState`'s "missing bullet line(s): ..." reads as `is missing ...`; its format-mismatch
186
+ * messages (`"- **X:**" doesn't match ...`) follow a colon instead. */
187
+ export function describeStateFileFailure(path, parseMessage) {
188
+ return parseMessage.startsWith("missing ") ? `state file ${path} is ${parseMessage}` : `state file ${path}: ${parseMessage}`;
189
+ }
190
+ /** MIL-257: non-null when the state file exists but isn't parseable — shared by `detectStateFile`
191
+ * (so the checklist line shows the real cause rather than the migration-tolerant
192
+ * `Model version:`/`Certified:` item) and `applyStateFile`. */
193
+ function stateFileParseFailure(ctx) {
98
194
  const loaded = loadStateFile(ctx.baseDir);
99
195
  if (!loaded.ok)
100
- return { applicable: false, reason: loaded.message };
196
+ return null; // absent — scaffolded, not an error (MIL-257)
197
+ const parsed = parseState(loaded.text);
198
+ return parsed.ok ? null : describeStateFileFailure(loaded.path, parsed.message);
199
+ }
200
+ function detectStateFile(ctx) {
201
+ const loaded = loadStateFile(ctx.baseDir);
202
+ if (!loaded.ok) {
203
+ return {
204
+ applicable: true,
205
+ reason: `no state file at ${resolveStateFilePath(ctx.baseDir)} — em upgrade will scaffold one (Current phase: discover, Current step: 1, Last conformance/Last stakeholder review: never)`,
206
+ };
207
+ }
208
+ const failure = stateFileParseFailure(ctx);
209
+ if (failure !== null)
210
+ return { applicable: false, reason: failure };
101
211
  const patched = ensureOptionalBullets(loaded.text, "1970-01-01");
102
212
  if (!patched.ok)
103
213
  return { applicable: false, reason: patched.message };
@@ -107,9 +217,18 @@ function detectStateFile(ctx) {
107
217
  }
108
218
  function applyStateFile(ctx) {
109
219
  const loaded = loadStateFile(ctx.baseDir);
110
- if (!loaded.ok)
111
- return { ok: false, message: loaded.message };
112
220
  const today = localIsoDate();
221
+ if (!loaded.ok) {
222
+ // MIL-257: reuse `em scaffold`'s own generator (templates.ts scaffoldStateFile) rather than a
223
+ // second one. `Model file:` names the model actually being upgraded (its own basename).
224
+ const path = resolveStateFilePath(ctx.baseDir);
225
+ const slug = basename(ctx.modelFile, extname(ctx.modelFile));
226
+ writeFileSync(path, scaffoldStateFile(ctx.model.name, slug, today, ctx.installedVersion));
227
+ return { ok: true, changedFiles: [path] };
228
+ }
229
+ const failure = stateFileParseFailure(ctx);
230
+ if (failure !== null)
231
+ return { ok: false, message: failure };
113
232
  const patched = ensureOptionalBullets(loaded.text, today);
114
233
  if (!patched.ok)
115
234
  return { ok: false, message: patched.message };
@@ -126,32 +245,68 @@ function applyStateFile(ctx) {
126
245
  // Not anchored on a literal "em coverage" — the generated line pins an exact version
127
246
  // (`npx @milehimikey/em@1.9.0 coverage "..."`, ciInit.ts's own `em` template variable), so
128
247
  // "em" and "coverage" are never adjacent in the real file.
129
- const CI_INIT_ARGS_RE = /\bcoverage "([^"]*)" --tests "([^"]*)" --strict\b/;
130
- function extractCiInitArgs(emCiContent) {
131
- const m = CI_INIT_ARGS_RE.exec(emCiContent);
132
- return m ? { model: m[1], testsDir: m[2] } : null;
248
+ const CI_INIT_ARGS_RE = /\bcoverage "([^"]*)" --tests "([^"]*)" --strict\b/g;
249
+ function extractCiInitArgs(emCiContent, repoRoot) {
250
+ const all = [...emCiContent.matchAll(CI_INIT_ARGS_RE)];
251
+ if (all.length === 0)
252
+ return { args: null };
253
+ const named = [...new Set(all.map((m) => m[1]))];
254
+ const base = { model: all[0][1], testsDir: all[0][2] };
255
+ if (named.length < 2)
256
+ return { args: { ...base, models: null } };
257
+ // A multi-model block: regenerate it from the manifest, never from a single model.
258
+ const loaded = ciModelsFromManifest(repoRoot, repoRoot);
259
+ if (!loaded.ok) {
260
+ return {
261
+ args: null,
262
+ blocked: `em-ci.yml covers ${named.length} models (${named.join(", ")}) but no readable system.yaml sits at the repo root (${loaded.message}) - run \`em ci init <system.yaml>\` by hand if it needs refreshing`,
263
+ };
264
+ }
265
+ return { args: { ...base, models: loaded.models } };
266
+ }
267
+ function ciBlockFiles(args, ctx) {
268
+ // MIL-231: same plugin signal `em ci init` uses, so the two never disagree about the conform lines.
269
+ const usePlugin = detectPlugin(ctx.repoRoot) !== null;
270
+ if (args.models) {
271
+ return {
272
+ ci: [buildCiWorkflowFileMulti(SYSTEM_MANIFEST_ARG, args.models, args.testsDir, ctx.installedVersion), ciManagedBodyMulti(args.models, args.testsDir, ctx.installedVersion)],
273
+ conform: [buildConformWorkflowFileMulti(SYSTEM_MANIFEST_ARG, args.models, ctx.installedVersion, usePlugin), conformManagedBodyMulti(args.models, ctx.installedVersion, usePlugin)],
274
+ };
275
+ }
276
+ return {
277
+ ci: [buildCiWorkflowFile(args.model, args.testsDir, ctx.installedVersion), ciManagedBody(args.model, args.testsDir, ctx.installedVersion)],
278
+ conform: [buildConformWorkflowFile(args.model, ctx.installedVersion, usePlugin), conformManagedBody(args.model, ctx.installedVersion, usePlugin)],
279
+ };
133
280
  }
281
+ /** Only used for the generated header's wording on a from-scratch file; `ci-block` only ever
282
+ * patches the managed block of a file that already exists, so this never reaches disk there. */
283
+ const SYSTEM_MANIFEST_ARG = "system.yaml";
134
284
  function ciBlockPlans(ctx) {
135
285
  const ciPath = ciWorkflowPath(ctx.repoRoot);
136
286
  const conformPath = conformWorkflowPath(ctx.repoRoot);
137
287
  if (!existsSync(ciPath))
138
288
  return { ciPath, ciStale: false, conformPath, conformStale: false, args: null };
139
- const args = extractCiInitArgs(readFileSync(ciPath, "utf8"));
140
- if (!args)
141
- return { ciPath, ciStale: false, conformPath, conformStale: false, args: null };
142
- const ciStatus = planCiFile(ciPath, buildCiWorkflowFile(args.model, args.testsDir, ctx.installedVersion), ciManagedBody(args.model, args.testsDir, ctx.installedVersion), CI_WORKFLOW_MARKER, false);
289
+ const extracted = extractCiInitArgs(readFileSync(ciPath, "utf8"), ctx.repoRoot);
290
+ if (!extracted.args)
291
+ return { ciPath, ciStale: false, conformPath, conformStale: false, args: null, blocked: "blocked" in extracted ? extracted.blocked : undefined };
292
+ const args = extracted.args;
293
+ const multi = args.models !== null;
294
+ const files = ciBlockFiles(args, ctx);
295
+ const ciStatus = planCiFile(ciPath, files.ci[0], files.ci[1], CI_WORKFLOW_MARKER, false, multi);
143
296
  const ciStale = ciStatus.kind === "stale";
144
297
  let conformStale = false;
145
298
  if (existsSync(conformPath)) {
146
- const conformStatus = planCiFile(conformPath, buildConformWorkflowFile(args.model, ctx.installedVersion), conformManagedBody(args.model, ctx.installedVersion), CONFORM_WORKFLOW_MARKER, false);
299
+ const conformStatus = planCiFile(conformPath, files.conform[0], files.conform[1], CONFORM_WORKFLOW_MARKER, false, multi);
147
300
  conformStale = conformStatus.kind === "stale";
148
301
  }
149
302
  return { ciPath, ciStale, conformPath, conformStale, args };
150
303
  }
151
304
  function detectCiBlock(ctx) {
152
- const { ciPath, ciStale, conformStale, args } = ciBlockPlans(ctx);
305
+ const { ciPath, ciStale, conformStale, args, blocked } = ciBlockPlans(ctx);
153
306
  if (!existsSync(ciPath))
154
307
  return { applicable: false, reason: "no .github/workflows/em-ci.yml — em upgrade never creates one" };
308
+ if (!args && blocked)
309
+ return { applicable: false, reason: blocked };
155
310
  if (!args)
156
311
  return { applicable: false, reason: "can't determine the <model>/--tests arguments em-ci.yml was generated with — run `em ci init <model>` by hand if it needs refreshing" };
157
312
  if (!ciStale && !conformStale)
@@ -162,15 +317,15 @@ function applyCiBlock(ctx) {
162
317
  const { ciPath, ciStale, conformPath, conformStale, args } = ciBlockPlans(ctx);
163
318
  if (!args || (!ciStale && !conformStale))
164
319
  return { ok: false, message: "nothing to refresh" };
320
+ const multi = args.models !== null;
321
+ const files = ciBlockFiles(args, ctx);
165
322
  const changedFiles = [];
166
323
  if (ciStale) {
167
- const status = planCiFile(ciPath, buildCiWorkflowFile(args.model, args.testsDir, ctx.installedVersion), ciManagedBody(args.model, args.testsDir, ctx.installedVersion), CI_WORKFLOW_MARKER, false);
168
- applyCiFile(ciPath, status);
324
+ applyCiFile(ciPath, planCiFile(ciPath, files.ci[0], files.ci[1], CI_WORKFLOW_MARKER, false, multi));
169
325
  changedFiles.push(ciPath);
170
326
  }
171
327
  if (conformStale) {
172
- const status = planCiFile(conformPath, buildConformWorkflowFile(args.model, ctx.installedVersion), conformManagedBody(args.model, ctx.installedVersion), CONFORM_WORKFLOW_MARKER, false);
173
- applyCiFile(conformPath, status);
328
+ applyCiFile(conformPath, planCiFile(conformPath, files.conform[0], files.conform[1], CONFORM_WORKFLOW_MARKER, false, multi));
174
329
  changedFiles.push(conformPath);
175
330
  }
176
331
  return { ok: true, changedFiles };
@@ -199,12 +354,191 @@ function applyConstitution(ctx) {
199
354
  writeFileSync(path, scaffoldConstitution(ctx.model.name));
200
355
  return { ok: true, changedFiles: [path] };
201
356
  }
357
+ // ---- Step 6: ratified-signoff (MIL-259) — `--slice-ready` now requires a recorded `ratifiedBy`,
358
+ // so a doc that reached `ready-to-implement` before sign-offs were recorded (pre-1.8) would
359
+ // suddenly read not-ready. Grandfather exactly those: `version: 1` (never reratified — a
360
+ // reratified doc has `version > 1` and is mid-sign-off, which only a human can resolve; see the
361
+ // `ready-to-implement-no-ratifiedby` human item) with no `ratifiedBy`. A doc with no `version:`
362
+ // at all is treated as version 1 (the schema's default). ----
363
+ export const GRANDFATHERED_RATIFIER = "grandfathered (unsigned before em 1.14)";
364
+ /** Pure text transform: inserts the grandfather sign-off after the `status:` line, using only
365
+ * the key lines that are missing (a stray `ratifiedOn:` is kept as-is), in the file's own EOL.
366
+ * `null` when the doc isn't a candidate (no frontmatter/status, or `ratifiedBy` already set). */
367
+ export function applyGrandfatherSignoff(raw, ratifiedOn) {
368
+ const range = locateFrontmatterInner(raw);
369
+ if (!range)
370
+ return null;
371
+ const inner = raw.slice(range.innerStart, range.innerEnd);
372
+ const statusMatch = fieldLineRegex("status").exec(inner);
373
+ if (!statusMatch)
374
+ return null;
375
+ const byMatch = fieldLineRegex("ratifiedBy").exec(inner);
376
+ if (byMatch && normalizeFieldValue(byMatch[2]) !== null)
377
+ return null;
378
+ const onMatch = fieldLineRegex("ratifiedOn").exec(inner);
379
+ const statusEnd = statusMatch.index + statusMatch[0].length;
380
+ const eol = inner.slice(statusEnd).startsWith("\r\n") ? "\r\n" : "\n";
381
+ const value = `"${GRANDFATHERED_RATIFIER}"`;
382
+ // Edits against `inner`, applied highest index first so earlier offsets stay valid. A blank
383
+ // `ratifiedBy:` line is filled in place; missing lines are inserted right after `status:`.
384
+ const missing = [];
385
+ if (!byMatch)
386
+ missing.push(`ratifiedBy: ${value}`);
387
+ if (!onMatch)
388
+ missing.push(`ratifiedOn: ${ratifiedOn}`);
389
+ const edits = [];
390
+ if (missing.length > 0)
391
+ edits.push({ index: statusEnd, oldLen: 0, next: eol + missing.join(eol) });
392
+ if (byMatch)
393
+ edits.push({ index: byMatch.index, oldLen: byMatch[0].length, next: `${byMatch[1]}${value}` });
394
+ edits.sort((a, b) => b.index - a.index);
395
+ let updated = inner;
396
+ for (const e of edits)
397
+ updated = updated.slice(0, e.index) + e.next + updated.slice(e.index + e.oldLen);
398
+ return raw.slice(0, range.innerStart) + updated + raw.slice(range.innerEnd);
399
+ }
400
+ function grandfatherCandidates(ctx) {
401
+ const seen = new Set();
402
+ const out = [];
403
+ for (const d of nonContinuationDocs(ctx)) {
404
+ if (d.status !== "ready-to-implement" || d.ratifiedBy)
405
+ continue;
406
+ if ((d.version ?? 1) !== 1)
407
+ continue;
408
+ if (seen.has(d.path))
409
+ continue;
410
+ seen.add(d.path);
411
+ out.push({ key: d.key, path: d.path });
412
+ }
413
+ return out.sort((a, b) => a.key.localeCompare(b.key));
414
+ }
415
+ function detectRatifiedSignoff(ctx) {
416
+ const keys = grandfatherCandidates(ctx).map((c) => c.key);
417
+ if (keys.length === 0)
418
+ return { applicable: false, reason: "no version-1 ready-to-implement doc is missing a ratifiedBy" };
419
+ return {
420
+ applicable: true,
421
+ reason: `${keys.length} ready-to-implement doc(s) with no recorded sign-off (--slice-ready now requires one): ${keys.join(", ")} — em upgrade will record ratifiedBy: "${GRANDFATHERED_RATIFIER}"`,
422
+ };
423
+ }
424
+ function applyRatifiedSignoff(ctx) {
425
+ const candidates = grandfatherCandidates(ctx);
426
+ if (candidates.length === 0)
427
+ return { ok: false, message: "nothing to grandfather" };
428
+ const today = localIsoDate();
429
+ const writes = [];
430
+ for (const c of candidates) {
431
+ const file = join(ctx.baseDir, c.path);
432
+ const next = applyGrandfatherSignoff(readFileSync(file, "utf8"), today);
433
+ if (next === null)
434
+ return { ok: false, message: `${c.path}: could not locate status: in the frontmatter` };
435
+ writes.push({ file, text: next });
436
+ }
437
+ for (const w of writes)
438
+ writeFileSync(w.file, w.text);
439
+ return { ok: true, changedFiles: writes.map((w) => w.file) };
440
+ }
441
+ // ---- Step 7: system-manifest (MIL-235, R3) — system.yaml 1.0 -> 2.0. ----
442
+ // The one mechanical step that writes OTHER models' files: the legacy manifest's seams become
443
+ // `consumes` clauses on each consuming translation and its owners become `owner "…"` on each
444
+ // member model's header, in the same single commit as the manifest rewrite (its `Em-Upgrade:`
445
+ // trailer is what lets MIL-240's scope gate exempt that cross-model change set). Planned by
446
+ // src/system/migrateManifest.ts over export documents; this half finds the manifest, loads the
447
+ // members, verifies every rewritten model still compiles clean, and only then writes anything.
448
+ /** The nearest `system.yaml` from `baseDir` upward, stopping at (and including) `repoRoot`. */
449
+ function findSystemManifest(ctx) {
450
+ // Real paths on both sides: git reports the repo root symlink-resolved (macOS /var -> /private/var).
451
+ const real = (p) => {
452
+ try {
453
+ return realpathSync(p);
454
+ }
455
+ catch {
456
+ return resolve(p);
457
+ }
458
+ };
459
+ const root = real(ctx.repoRoot);
460
+ let dir = real(ctx.baseDir);
461
+ for (;;) {
462
+ const candidate = join(dir, SYSTEM_MANIFEST_FILE);
463
+ if (existsSync(candidate))
464
+ return candidate;
465
+ if (dir === root)
466
+ return null;
467
+ const parent = dirname(dir);
468
+ const rel = relative(root, parent);
469
+ if (parent === dir || rel.startsWith("..") || isAbsolute(rel))
470
+ return null;
471
+ dir = parent;
472
+ }
473
+ }
474
+ function loadLegacyManifest(ctx) {
475
+ const path = findSystemManifest(ctx);
476
+ if (path === null)
477
+ return { path: null, reason: `no ${SYSTEM_MANIFEST_FILE} between the model and the repository root` };
478
+ const text = readFileSync(path, "utf8");
479
+ const parsed = parseManifest(text);
480
+ if (!parsed.ok)
481
+ return { path, reason: `${path} does not parse — run \`em system ${path}\` to see why` };
482
+ if (parsed.manifest.systemSchemaVersion !== LEGACY_SYSTEM_MANIFEST_SCHEMA_VERSION) {
483
+ return { path, reason: `${path} is already systemSchemaVersion "${parsed.manifest.systemSchemaVersion}"` };
484
+ }
485
+ return { path, text, manifest: parsed.manifest };
486
+ }
487
+ function detectSystemManifest(ctx) {
488
+ const loaded = loadLegacyManifest(ctx);
489
+ if (!("manifest" in loaded))
490
+ return { applicable: false, reason: loaded.reason };
491
+ const m = loaded.manifest;
492
+ return {
493
+ applicable: true,
494
+ reason: `${loaded.path} is systemSchemaVersion "1.0" — em upgrade will move its ${m.seams.length} seam(s) into \`consumes\` clauses, ` +
495
+ `its owner(s) onto the ${m.models.length} model header(s), and rewrite it to "2.0"`,
496
+ };
497
+ }
498
+ function applySystemManifest(ctx) {
499
+ const loaded = loadLegacyManifest(ctx);
500
+ if (!("manifest" in loaded))
501
+ return { ok: false, message: loaded.reason };
502
+ const baseDir = dirname(loaded.path);
503
+ const members = [];
504
+ for (const entry of loaded.manifest.models) {
505
+ const file = isAbsolute(entry.source) ? entry.source : join(baseDir, entry.source);
506
+ const source = loadSource(file);
507
+ if ("error" in source)
508
+ return { ok: false, message: `model "${entry.key}": ${source.error}` };
509
+ members.push({ key: entry.key, file, text: source.sourceKind === "em" ? readFileSync(file, "utf8") : null, doc: source.doc });
510
+ }
511
+ const plan = planManifestMigration(loaded.text, loaded.manifest, members);
512
+ if (!plan.ok)
513
+ return { ok: false, message: plan.message };
514
+ // Verify before write: every rewritten model must still compile with no errors.
515
+ for (const f of plan.files) {
516
+ try {
517
+ const { diagnostics } = compile(f.text);
518
+ if (hasErrors(diagnostics))
519
+ return { ok: false, message: `the rewrite of ${f.file} would introduce validation errors — aborting, nothing written` };
520
+ }
521
+ catch (e) {
522
+ return { ok: false, message: `the rewrite of ${f.file} does not parse (${e.message}) — aborting, nothing written` };
523
+ }
524
+ }
525
+ const reparsed = parseManifest(plan.manifestText);
526
+ if (!reparsed.ok)
527
+ return { ok: false, message: `the rewritten ${loaded.path} does not parse as 2.0 — aborting, nothing written` };
528
+ for (const f of plan.files)
529
+ writeFileSync(f.file, f.text);
530
+ writeFileSync(loaded.path, plan.manifestText);
531
+ return { ok: true, changedFiles: [...plan.files.map((f) => f.file), loaded.path] };
532
+ }
202
533
  export const UPGRADE_STEPS = [
534
+ { id: "skill-plugin", sinceVersion: "1.14.0", detect: detectSkillPlugin, apply: applySkillPlugin },
203
535
  { id: "skill-bundle", sinceVersion: "1.7.0", detect: detectSkillBundle, apply: applySkillBundle },
204
536
  { id: "reaction-shape", sinceVersion: "1.8.0", detect: detectReactionShape, apply: applyReactionShape },
205
537
  { id: "state-file", sinceVersion: "1.13.0", detect: detectStateFile, apply: applyStateFile },
206
538
  { id: "ci-block", sinceVersion: "1.9.0", detect: detectCiBlock, apply: applyCiBlock },
207
539
  { id: "constitution", sinceVersion: "1.11.0", detect: detectConstitution, apply: applyConstitution },
540
+ { id: "ratified-signoff", sinceVersion: "1.14.0", detect: detectRatifiedSignoff, apply: applyRatifiedSignoff },
541
+ { id: "system-manifest", sinceVersion: "1.14.0", detect: detectSystemManifest, apply: applySystemManifest },
208
542
  ];
209
543
  function elementRefOf(ctx) {
210
544
  return (id) => ctx.refs.refById.get(id);
@@ -220,7 +554,7 @@ function nonContinuationDocs(ctx) {
220
554
  const { doc, continuationOf } = resolveSliceDocJoin(ctx.model, ctx.refs, slice, key, ctx.baseDir, elementRefOf(ctx));
221
555
  if (continuationOf !== null)
222
556
  return;
223
- results.push({ key, status: doc.status, ratifiedBy: doc.ratifiedBy });
557
+ results.push({ key, status: doc.status, ratifiedBy: doc.ratifiedBy, version: doc.version, path: doc.path });
224
558
  });
225
559
  return results;
226
560
  }
@@ -251,14 +585,14 @@ function detectContinuationHasOwnDoc(ctx) {
251
585
  }
252
586
  function detectReadyNoRatifiedBy(ctx) {
253
587
  const keys = nonContinuationDocs(ctx)
254
- .filter((d) => d.status === "ready-to-implement" && !d.ratifiedBy)
588
+ .filter((d) => d.status === "ready-to-implement" && !d.ratifiedBy && (d.version ?? 1) > 1)
255
589
  .map((d) => d.key)
256
590
  .sort();
257
591
  if (keys.length === 0)
258
592
  return null;
259
593
  return {
260
594
  id: "ready-to-implement-no-ratifiedby",
261
- reason: `${keys.length} ready-to-implement doc(s) with no ratifiedBy: ${keys.join(", ")} — run \`em slice ratify --by <name>\` on each`,
595
+ reason: `${keys.length} ready-to-implement doc(s) with no ratifiedBy (version > 1, reratified and awaiting a fresh sign-off): ${keys.join(", ")} — run \`em slice ratify --by <name>\` on each`,
262
596
  };
263
597
  }
264
598
  function detectCoverageScopeDefault(ctx) {
@@ -301,6 +635,36 @@ function detectPredates16(ctx) {
301
635
  reason: `${plan.refusals.length} old-shape reaction site(s) can't be auto-migrated — run \`em migrate\` by hand, resolve the refusal(s), then re-run \`em upgrade\``,
302
636
  };
303
637
  }
638
+ /** MIL-237: strict public types (briefing R7). Lists every field of a `public` element (or of a
639
+ * declared type reachable from one) whose type is not in the fixed public type table and names
640
+ * no declared type — the same set `em validate`'s `public-field-type-unresolved` errors on.
641
+ * Detect-only: choosing the right type (or dropping `public`) is a modeling decision. */
642
+ function detectPublicFieldTypesUnresolved(ctx) {
643
+ const found = findUnresolvedPublicFieldTypes(ctx.model);
644
+ if (found.length === 0)
645
+ return null;
646
+ const items = found.map((f) => `${f.owner.name}.${f.field}: ${f.type ?? "(no type)"}`);
647
+ return {
648
+ id: "public-field-types-unresolved",
649
+ reason: `${found.length} public field(s) without a public type: ${items.join(", ")} — give each a type from ` +
650
+ `${PUBLIC_SCALAR_TYPE_NAMES.join(", ")}, \`X[]\`, or a declared \`type\` (em 1.14 strict public types), or drop \`public\``,
651
+ };
652
+ }
653
+ /** MIL-232: the machine-level plugin registration is per user and cannot be done by `em upgrade`.
654
+ * Fires when the `skill-plugin` step is pending (it will declare the plugin), or when the repo
655
+ * declares the plugin at the installed version but the machine registry lacks it. */
656
+ function detectPluginInstallLocally(ctx) {
657
+ const commands = pluginInstallCommands(ctx.installedVersion);
658
+ const snippet = `run \`${commands.join(" && ")}\` on each developer machine and in CI (the generated em-ci.yml does it)`;
659
+ if (detectSkillPlugin(ctx).applicable) {
660
+ return { id: "plugin-install-locally", reason: `the em plugin will be declared in .claude/settings.json but is registered per machine — after --apply, ${snippet}` };
661
+ }
662
+ const check = checkPlugin(ctx.repoRoot, ctx.installedVersion, claudePluginsDir());
663
+ if (check && check.findings.some((f) => f.code === "plugin-not-installed-locally")) {
664
+ return { id: "plugin-install-locally", reason: `the em plugin is declared but not registered on this machine — ${snippet}` };
665
+ }
666
+ return null;
667
+ }
304
668
  const HUMAN_DETECTORS = [
305
669
  detectNoModelVersion,
306
670
  detectContinuationHasOwnDoc,
@@ -308,6 +672,8 @@ const HUMAN_DETECTORS = [
308
672
  detectCoverageScopeDefault,
309
673
  detectUnratifiedConstitution,
310
674
  detectPredates16,
675
+ detectPublicFieldTypesUnresolved,
676
+ detectPluginInstallLocally,
311
677
  ];
312
678
  export function detectHumanItems(ctx) {
313
679
  return HUMAN_DETECTORS.map((d) => d(ctx)).filter((x) => x !== null);
@@ -360,11 +726,17 @@ export function hasGitIdentity(repoRoot, runGit = realGit) {
360
726
  const email = runGit(["-C", repoRoot, "config", "user.email"]);
361
727
  return name.status === 0 && name.stdout.trim().length > 0 && email.status === 0 && email.stdout.trim().length > 0;
362
728
  }
363
- function gitCommitAll(repoRoot, message, runGit) {
729
+ /** The commit trailer key every `em upgrade --apply` commit carries (MIL-235, R2):
730
+ * `Em-Upgrade: <step-id>` (or `em-version` for the final stamp commit). Readers use
731
+ * `git log --format=%(trailers:key=Em-Upgrade,valueonly)` — MIL-240's scope gate exempts files
732
+ * changed only by such commits, since a mechanical migration may legitimately span models. */
733
+ export const EM_UPGRADE_TRAILER = "Em-Upgrade";
734
+ function gitCommitAll(repoRoot, message, trailerValue, runGit) {
364
735
  const add = runGit(["-C", repoRoot, "add", "-A"]);
365
736
  if (add.status !== 0)
366
737
  return { ok: false, message: `git add failed: ${(add.stderr || "").trim() || "unknown error"}` };
367
- const commit = runGit(["-C", repoRoot, "commit", "-m", message]);
738
+ // Second `-m` = its own paragraph, which git reads as the trailer block.
739
+ const commit = runGit(["-C", repoRoot, "commit", "-m", message, "-m", `${EM_UPGRADE_TRAILER}: ${trailerValue}`]);
368
740
  if (commit.status !== 0)
369
741
  return { ok: false, message: `git commit failed: ${(commit.stderr || "").trim() || "unknown error"}` };
370
742
  return { ok: true };
@@ -383,14 +755,14 @@ function gitDiscardAll(repoRoot, runGit) {
383
755
  export function detectUpgrade(ctx) {
384
756
  const loaded = loadStateFile(ctx.baseDir);
385
757
  let recorded = null;
758
+ // MIL-257: an ABSENT state file is no longer an error — the `state-file` step scaffolds it — so
759
+ // `stateFileError` is `null` for it (the pending step's own reason carries the detail). It is
760
+ // set only for a file that exists but doesn't parse, and now names the file.
386
761
  let stateFileError = null;
387
- if (!loaded.ok) {
388
- stateFileError = loaded.message;
389
- }
390
- else {
762
+ if (loaded.ok) {
391
763
  const parsed = parseState(loaded.text);
392
764
  if (!parsed.ok)
393
- stateFileError = parsed.message;
765
+ stateFileError = describeStateFileFailure(loaded.path, parsed.message);
394
766
  else
395
767
  recorded = parsed.state.emVersion;
396
768
  }
@@ -404,7 +776,7 @@ export function detectUpgrade(ctx) {
404
776
  }
405
777
  /**
406
778
  * `--apply`: detect, then apply every applicable mechanical step in fixed order, one commit
407
- * each (`em upgrade: <step-id> (<from> → <to>)`), stopping at the first failure with every
779
+ * each (`em upgrade: <step-id> (<from> → <to>)`, trailer `Em-Upgrade: <step-id>`), stopping at the first failure with every
408
780
  * prior commit intact and the failed step's own partial writes discarded. On full success,
409
781
  * writes `Em version: <to>` as one final commit — always, even when every step above was a
410
782
  * no-op, UNLESS the bullet already reads `<to>` (idempotent second run makes zero commits).
@@ -440,7 +812,7 @@ export function applyUpgrade(ctx, runGit = realGit) {
440
812
  return { ok: false, message: `em upgrade: step "${step.id}" failed — ${result.message}`, report, applied };
441
813
  }
442
814
  const message = `em upgrade: ${step.id} (${report.from.version} → ${report.to})`;
443
- const commitResult = gitCommitAll(ctx.repoRoot, message, runGit);
815
+ const commitResult = gitCommitAll(ctx.repoRoot, message, step.id, runGit);
444
816
  if (!commitResult.ok) {
445
817
  return { ok: false, message: `em upgrade: step "${step.id}" applied but failed to commit — ${commitResult.message}`, report, applied };
446
818
  }
@@ -454,7 +826,7 @@ export function applyUpgrade(ctx, runGit = realGit) {
454
826
  if (stamped.ok && stamped.text !== loaded.text) {
455
827
  writeFileSync(loaded.path, stamped.text);
456
828
  const message = `em upgrade: em-version (${report.from.version} → ${report.to})`;
457
- const commitResult = gitCommitAll(ctx.repoRoot, message, runGit);
829
+ const commitResult = gitCommitAll(ctx.repoRoot, message, "em-version", runGit);
458
830
  if (!commitResult.ok) {
459
831
  return { ok: false, message: `em upgrade: writing Em version: failed to commit — ${commitResult.message}`, report, applied };
460
832
  }
@@ -463,14 +835,26 @@ export function applyUpgrade(ctx, runGit = realGit) {
463
835
  }
464
836
  return { ok: true, report, applied, emVersionCommit };
465
837
  }
838
+ /** MIL-257: every cause that makes `--check` exit 1, each as one sentence, so the final line
839
+ * always says why. Empty when there is no hard incompatibility. */
840
+ export function hardIncompatibilityReasons(report) {
841
+ const reasons = [];
842
+ if (report.stateFileError !== null)
843
+ reasons.push(report.stateFileError);
844
+ for (const h of report.human)
845
+ if (h.id === "predates-1.6")
846
+ reasons.push(`predates-1.6: ${h.reason}`);
847
+ return reasons;
848
+ }
466
849
  /** `--check` (what CI runs): exit-worthiness only — no writes. Hard incompatibilities are the
467
- * ONLY thing that makes this fail: an unparseable/missing state file, or a `predates-1.6` human
468
- * item (an old reaction shape `em migrate` itself refuses to touch). Every other human item —
469
- * including the ordinary "some steps are applicable" case — is advisory, same as running
470
- * `em upgrade` without `--apply` at all. */
850
+ * ONLY thing that makes this fail: a state file that exists but is unparseable (a MISSING one is
851
+ * scaffolded by the `state-file` step, MIL-257, so it is a pending step, not a failure), or a
852
+ * `predates-1.6` human item (an old reaction shape `em migrate` itself refuses to touch). Every
853
+ * other human item — including the ordinary "some steps are applicable" case — is advisory, same
854
+ * as running `em upgrade` without `--apply` at all. `reasons` (MIL-257) is why `ok` is false. */
471
855
  export function checkUpgrade(ctx) {
472
856
  const report = detectUpgrade(ctx);
473
- const hardIncompatibility = report.stateFileError !== null || report.human.some((h) => h.id === "predates-1.6");
474
- return { ok: !hardIncompatibility, report };
857
+ const reasons = hardIncompatibilityReasons(report);
858
+ return { ok: reasons.length === 0, report, reasons };
475
859
  }
476
860
  //# sourceMappingURL=upgrade.js.map