@milehimikey/em 1.14.2 → 1.16.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 (125) hide show
  1. package/.claude/agents/em-implementer.md +12 -2
  2. package/.claude/agents/em-validator.md +2 -1
  3. package/.claude/skills/event-modeling/SKILL.md +3 -3
  4. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  5. package/.claude/skills/event-modeling-design/SKILL.md +29 -18
  6. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  7. package/.claude/skills/event-modeling-engagement/SKILL.md +60 -56
  8. package/.claude/skills/event-modeling-engagement/reference/engagement.md +13 -19
  9. package/.claude/skills/event-modeling-implement/SKILL.md +3 -3
  10. package/.claude/skills/event-modeling-implement/reference/implement.md +40 -27
  11. package/.claude/skills/event-modeling-review/SKILL.md +13 -3
  12. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +234 -228
  13. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +3 -3
  14. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +69 -27
  15. package/.claude/skills/event-modeling-shared/templates/constitution.md +5 -7
  16. package/.claude-plugin/marketplace.json +2 -2
  17. package/dist/catalog/deferQueue.d.ts +20 -0
  18. package/dist/catalog/deferQueue.js +63 -0
  19. package/dist/catalog/deferQueue.js.map +1 -0
  20. package/dist/catalog/docJoin.d.ts +22 -1
  21. package/dist/catalog/docJoin.js +13 -4
  22. package/dist/catalog/docJoin.js.map +1 -1
  23. package/dist/catalog/docModelConsistencyValidate.js +4 -8
  24. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  25. package/dist/catalog/driftSignal.d.ts +16 -12
  26. package/dist/catalog/driftSignal.js +14 -9
  27. package/dist/catalog/driftSignal.js.map +1 -1
  28. package/dist/catalog/sliceDoc.d.ts +51 -0
  29. package/dist/catalog/sliceDoc.js +47 -0
  30. package/dist/catalog/sliceDoc.js.map +1 -1
  31. package/dist/catalog/sliceReadyValidate.js +19 -1
  32. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  33. package/dist/catalog/sliceSections.d.ts +5 -0
  34. package/dist/catalog/sliceSections.js +11 -2
  35. package/dist/catalog/sliceSections.js.map +1 -1
  36. package/dist/cli/conformScope.d.ts +4 -0
  37. package/dist/cli/conformScope.js +2 -2
  38. package/dist/cli/conformScope.js.map +1 -1
  39. package/dist/cli/coverage.js +7 -3
  40. package/dist/cli/coverage.js.map +1 -1
  41. package/dist/cli/defer.d.ts +106 -2
  42. package/dist/cli/defer.js +357 -24
  43. package/dist/cli/defer.js.map +1 -1
  44. package/dist/cli/engagement.d.ts +26 -5
  45. package/dist/cli/engagement.js +55 -28
  46. package/dist/cli/engagement.js.map +1 -1
  47. package/dist/cli/engagementFile.d.ts +2 -0
  48. package/dist/cli/engagementFile.js +5 -3
  49. package/dist/cli/engagementFile.js.map +1 -1
  50. package/dist/cli/findings.d.ts +2 -2
  51. package/dist/cli/findings.js +2 -2
  52. package/dist/cli/frontmatterSurgery.d.ts +3 -3
  53. package/dist/cli/frontmatterSurgery.js +3 -3
  54. package/dist/cli/frontmatterSurgery.js.map +1 -1
  55. package/dist/cli/markImplemented.d.ts +23 -18
  56. package/dist/cli/markImplemented.js +99 -15
  57. package/dist/cli/markImplemented.js.map +1 -1
  58. package/dist/cli/ratify.d.ts +36 -6
  59. package/dist/cli/ratify.js +130 -35
  60. package/dist/cli/ratify.js.map +1 -1
  61. package/dist/cli/review.js +3 -3
  62. package/dist/cli/review.js.map +1 -1
  63. package/dist/cli/revise.d.ts +67 -0
  64. package/dist/cli/revise.js +229 -0
  65. package/dist/cli/revise.js.map +1 -0
  66. package/dist/cli/sliceConform.d.ts +4 -3
  67. package/dist/cli/sliceConform.js +29 -12
  68. package/dist/cli/sliceConform.js.map +1 -1
  69. package/dist/cli/status.d.ts +30 -6
  70. package/dist/cli/status.js +59 -19
  71. package/dist/cli/status.js.map +1 -1
  72. package/dist/cli/upgrade.js +17 -12
  73. package/dist/cli/upgrade.js.map +1 -1
  74. package/dist/cli.js +173 -85
  75. package/dist/cli.js.map +1 -1
  76. package/dist/emit/engagementJson.d.ts +7 -4
  77. package/dist/emit/engagementJson.js +11 -4
  78. package/dist/emit/engagementJson.js.map +1 -1
  79. package/dist/emit/json.d.ts +1 -1
  80. package/dist/emit/json.js +18 -5
  81. package/dist/emit/json.js.map +1 -1
  82. package/dist/emit/statusJson.d.ts +1 -1
  83. package/dist/emit/statusJson.js +6 -1
  84. package/dist/emit/statusJson.js.map +1 -1
  85. package/dist/mcp/server.js +5 -5
  86. package/dist/mcp/server.js.map +1 -1
  87. package/dist/model/continuation.d.ts +11 -1
  88. package/dist/model/continuation.js +29 -0
  89. package/dist/model/continuation.js.map +1 -1
  90. package/dist/model/resolveSlice.d.ts +19 -0
  91. package/dist/model/resolveSlice.js +69 -0
  92. package/dist/model/resolveSlice.js.map +1 -0
  93. package/dist/model/rules.d.ts +8 -0
  94. package/dist/model/rules.js +8 -0
  95. package/dist/model/rules.js.map +1 -1
  96. package/dist/model/sliceDocIndex.d.ts +5 -0
  97. package/dist/model/sliceDocIndex.js +6 -4
  98. package/dist/model/sliceDocIndex.js.map +1 -1
  99. package/dist/model/validate.js +5 -2
  100. package/dist/model/validate.js.map +1 -1
  101. package/dist/parser/parser.js +55 -1
  102. package/dist/parser/parser.js.map +1 -1
  103. package/dist/templates.d.ts +1 -1
  104. package/dist/templates.js +5 -7
  105. package/dist/templates.js.map +1 -1
  106. package/package.json +1 -1
  107. package/plugin/.claude-plugin/plugin.json +2 -2
  108. package/plugin/agents/em-implementer.md +12 -2
  109. package/plugin/agents/em-validator.md +2 -1
  110. package/plugin/shared/reference/em-dsl.md +234 -228
  111. package/plugin/shared/reference/operating-principles.md +3 -3
  112. package/plugin/shared/reference/slice-doc-schema.md +69 -27
  113. package/plugin/shared/templates/constitution.md +5 -7
  114. package/plugin/skills/conform/SKILL.md +1 -1
  115. package/plugin/skills/design/SKILL.md +29 -18
  116. package/plugin/skills/discover/SKILL.md +1 -1
  117. package/plugin/skills/engagement/SKILL.md +60 -56
  118. package/plugin/skills/engagement/reference/engagement.md +13 -19
  119. package/plugin/skills/event-modeling/SKILL.md +3 -3
  120. package/plugin/skills/implement/SKILL.md +3 -3
  121. package/plugin/skills/implement/reference/implement.md +40 -27
  122. package/plugin/skills/review/SKILL.md +13 -3
  123. package/dist/cli/reratify.d.ts +0 -58
  124. package/dist/cli/reratify.js +0 -238
  125. package/dist/cli/reratify.js.map +0 -1
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { ParseError } from "./parser/parser.js";
13
13
  import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
14
14
  import { buildSliceDiagram } from "./render/sliceDiagram.js";
15
15
  import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
16
+ import { resolveSliceKey } from "./model/resolveSlice.js";
16
17
  import { serializeBuilds, watchFile } from "./render/watch.js";
17
18
  import { startLiveServer } from "./render/serve.js";
18
19
  import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
@@ -75,8 +76,8 @@ import { wireSliceNote } from "./cli/sliceLink.js";
75
76
  import { isStubStatus, runStubAll, STUB_STATUSES } from "./cli/sliceStubAll.js";
76
77
  import { listModelCommits } from "./cli/changelog-git.js";
77
78
  import { buildChangelogDoc } from "./cli/changelogBuild.js";
78
- import { runReratify } from "./cli/reratify.js";
79
- import { runDefer } from "./cli/defer.js";
79
+ import { runRevise } from "./cli/revise.js";
80
+ import { runDefer, runDeferFromIssue } from "./cli/defer.js";
80
81
  import { runSliceConform } from "./cli/sliceConform.js";
81
82
  import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
82
83
  import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
@@ -246,7 +247,7 @@ program
246
247
  // "don't gate on breakage elsewhere" call `--slice-ready` already made (see its own
247
248
  // scoping comment above). Only an error that concerns THIS slice (bare key, or an
248
249
  // element ref prefixed `<key>/`) refuses.
249
- const key = opts.slice;
250
+ const key = resolveSliceOrExit("em export --slice", model, refs, opts.slice);
250
251
  const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
251
252
  if (scopedErrors.length > 0) {
252
253
  console.error(`not exporting: slice "${key}" has errors — fix them first`);
@@ -571,6 +572,8 @@ slice
571
572
  process.exit(1);
572
573
  }
573
574
  const check = opts.check === true;
575
+ if (key !== undefined)
576
+ key = resolveSliceOrExit("em slice sync", model, refs, key);
574
577
  const outcome = runSliceSync(model, refs, dirname(file), key, check);
575
578
  if (!outcome.ok) {
576
579
  console.error(`em slice sync: ${outcome.message}`);
@@ -672,14 +675,23 @@ slice
672
675
  "one edit an implementing agent makes to a ratified doc at merge (MIL-103, replaces the " +
673
676
  "em-sdd-bridge `em-sdd-mark-implemented` script; see reference/implement.md §6). " +
674
677
  "Applies only to a `ready-to-implement` doc (MIL-277: a `draft`/`reviewed` doc refuses — " +
675
- "ratify first). Idempotent on the same URL; refuses to overwrite a different one; never " +
676
- "touches `version:` or the doc body")
678
+ "ratify first). Also records the shipped version beside the link (MIL-284): `shippedVersion:`, " +
679
+ "`shippedRef:` (the doc's `ratifiedRef:`, when it has one) and `shippedOn:`, and opens the next " +
680
+ "version as a `draft` when an Open Question was deferred to it (MIL-283). Idempotent on " +
681
+ "the same URL — also after that draft opened; refuses to overwrite a different one; never touches " +
682
+ "the doc body except to re-open those deferred questions")
677
683
  .argument("<file>", "input .em file")
678
684
  .argument("<slice-key>", "slice export key (kebab-case)")
679
685
  .argument("<pr-url>", "merged PR (or commit) URL")
680
- .action((file, sliceKey, prUrl) => {
686
+ .option("--on <date>", "merge date recorded as `shippedOn:`, YYYY-MM-DD (default: today, local date; MIL-284)")
687
+ .action((file, sliceKey, prUrl, opts) => {
688
+ if (opts.on !== undefined && !isValidDateString(opts.on)) {
689
+ console.error(`em slice mark-implemented: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
690
+ process.exit(1);
691
+ }
681
692
  const { model, refs, diagnostics } = compileFile(file);
682
693
  printDiagnostics(diagnostics);
694
+ sliceKey = resolveSliceOrExit("em slice mark-implemented", model, refs, sliceKey);
683
695
  // Scoped the same way `em export --slice`/`em validate --slice-ready` are: only an error
684
696
  // concerning THIS slice (bare key, or an element ref prefixed `<key>/`) refuses — an
685
697
  // unrelated slice's breakage elsewhere in a large, still-WIP model doesn't block marking
@@ -689,7 +701,7 @@ slice
689
701
  console.error(`em slice mark-implemented: slice "${sliceKey}" has errors — fix them first`);
690
702
  process.exit(1);
691
703
  }
692
- const result = runMarkImplemented(model, refs, dirname(file), sliceKey, prUrl);
704
+ const result = runMarkImplemented(model, refs, dirname(file), sliceKey, prUrl, opts.on ?? localIsoDate());
693
705
  if (!result.ok) {
694
706
  console.error(`em slice mark-implemented: ${result.message}`);
695
707
  process.exit(1);
@@ -697,6 +709,11 @@ slice
697
709
  console.log(result.changed
698
710
  ? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
699
711
  : `already implemented (no-op): ${result.path}`);
712
+ if (result.ok && result.openedDraft) {
713
+ console.log(`opened draft v${result.openedDraft.version}: ${result.path} — re-opened ${result.openedDraft.reopened} deferred question(s)` +
714
+ (result.openedDraft.injected > 0 ? `, injected ${result.openedDraft.injected} queued question(s) (GH #236)` : "") +
715
+ `; v${result.openedDraft.version - 1} stays shipped`);
716
+ }
700
717
  });
701
718
  slice
702
719
  .command("review")
@@ -713,6 +730,7 @@ slice
713
730
  .action((file, sliceKey, opts) => {
714
731
  const { model, refs, diagnostics } = compileFile(file);
715
732
  printDiagnostics(diagnostics);
733
+ sliceKey = resolveSliceOrExit("em slice review", model, refs, sliceKey);
716
734
  // Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
717
735
  // `em validate --slice-ready` are: only an error concerning THIS slice refuses.
718
736
  const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
@@ -745,11 +763,13 @@ slice
745
763
  "`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#the-slice-lifecycle-gates) " +
746
764
  "that makes who ratified, and when, a first-class recorded fact. The review gate is a check " +
747
765
  "on the doc's current `status:`, not on whether `reviewedBy:`/`reviewedOn:` are populated: " +
748
- "a doc at `reviewed` or `ready-to-implement` passes (the latter is where `em slice " +
749
- "reratify` leaves a doc, so the follow-up ratify needs no --skip-review); any other status " +
766
+ "a doc at `reviewed` passes; a `ready-to-implement` doc only when the same signer is " +
767
+ "completing their own record (since 1.15.0, MIL-283 — a new sign-off is a new version: " +
768
+ "`em slice revise`); any other status " +
750
769
  "— a `draft` that never went through `em slice review` — is refused unless --skip-review " +
751
770
  "is passed. Idempotent on " +
752
- "the same --by/--on pair; refuses to overwrite a different one already recorded; never " +
771
+ "the same --by/--on pair; refuses to overwrite a different one already recorded, and (MIL-284) a " +
772
+ "same-pair re-run on a doc whose body moved after sign-off; never " +
753
773
  "touches `version:` or the doc body")
754
774
  .argument("<file>", "input .em file")
755
775
  .argument("<slice-key>", "slice export key (kebab-case)")
@@ -768,6 +788,7 @@ slice
768
788
  }
769
789
  const { model, refs, diagnostics, source } = compileFile(file);
770
790
  printDiagnostics(diagnostics);
791
+ sliceKey = resolveSliceOrExit("em slice ratify", model, refs, sliceKey);
771
792
  // Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
772
793
  // --slice-ready` are: only an error concerning THIS slice refuses.
773
794
  const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
@@ -803,64 +824,83 @@ slice
803
824
  : `already ratified (no-op): ${result.path}`);
804
825
  warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
805
826
  });
827
+ /** MIL-283: `em slice revise` and its deprecated alias `em slice reratify` share one action. */
828
+ function reviseAction(commandName) {
829
+ return (file, sliceKey, opts) => {
830
+ if (commandName === "reratify") {
831
+ console.error("notice: `em slice reratify` is deprecated since 1.15.0 — it is `em slice revise`, and the result is a " +
832
+ "`draft` of the next version (not an unsigned `ready-to-implement`). Review and ratify it like v1.");
833
+ }
834
+ if (opts.meaningUnchanged || opts.contractChange !== undefined) {
835
+ console.error(`notice: --meaning-unchanged/--contract-change are not taken by \`em slice ${commandName}\` since 1.15.0 — the ` +
836
+ "API-first confirmation belongs to the ratification of the new version (`em slice ratify --by ... --meaning-unchanged`)");
837
+ }
838
+ const { model, refs, diagnostics, source } = compileFile(file);
839
+ printDiagnostics(diagnostics);
840
+ sliceKey = resolveSliceOrExit(`em slice ${commandName}`, model, refs, sliceKey);
841
+ // Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
842
+ // `em validate --slice-ready` are: only an error concerning THIS slice refuses.
843
+ const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
844
+ if (scopedErrors.length > 0) {
845
+ console.error(`em slice ${commandName}: slice "${sliceKey}" has errors — fix them first`);
846
+ process.exit(1);
847
+ }
848
+ const result = runRevise(model, refs, dirname(file), sliceKey, opts.withdraw === true);
849
+ if (!result.ok) {
850
+ console.error(`em slice ${commandName}: ${result.message}`);
851
+ process.exit(1);
852
+ }
853
+ // MIL-214: advisory only, never refuses — see revise.ts's reviseAdvisory. Both warnings can
854
+ // fire together (a shipped version that was never certified AND still carries unruled findings).
855
+ if (result.advisory?.neverCertified) {
856
+ console.error(`warn: revising "${sliceKey}" whose shipped v${result.newVersion - 1} was never certified`);
857
+ }
858
+ if (result.advisory && result.advisory.unruledFindingsCount > 0) {
859
+ console.error(`warn: revising "${sliceKey}" with ${result.advisory.unruledFindingsCount} unruled conformance finding(s) on the shipped version`);
860
+ }
861
+ const what = result.kind === "shipped"
862
+ ? `v${result.newVersion - 1} stays shipped; v${result.newVersion} opened as draft`
863
+ : result.kind === "unshipped"
864
+ ? `ratification of v${result.newVersion - 1} withdrawn (never shipped); v${result.newVersion} opened as draft`
865
+ : `v${result.newVersion} was never signed; reopened as draft at the same version`;
866
+ console.log(`revised: ${result.path} (status: draft, version: ${result.newVersion} — ${what})`);
867
+ if (result.reopened > 0)
868
+ console.log(`re-opened ${result.reopened} deferred question(s)`);
869
+ if (result.injected > 0)
870
+ console.log(`injected ${result.injected} queued question(s) into Open Questions (queue cleared)`);
871
+ warnModelVersionDrift(dirname(file), model, refs, source, `revising "${sliceKey}"`);
872
+ };
873
+ }
874
+ const REVISE_DESCRIPTION = "open the next version of a slice doc as a `draft` (MIL-283): bumps `version:`, flips `status:` to " +
875
+ "`draft`, clears the prior version's review and sign-off (`reviewedBy/On`, `ratifiedBy/On/Ref/Hash`, " +
876
+ "`meaningConfirmed`/`contractChange`) and re-opens the questions deferred to the new version. The " +
877
+ "shipped version stays shipped: `implementedIn:` and the `shipped*` keys are untouched (written first on " +
878
+ "a doc shipped before 1.15). Applies to `status: implemented` (the shipped case) and to a ratified " +
879
+ "`ready-to-implement` doc (ratification withdrawn, never shipped); a `ready-to-implement` doc with no " +
880
+ "`ratifiedBy:` reopens as draft at the same version; `draft`/`reviewed` refuse — the next version is " +
881
+ "already open. The new version then takes the normal road: `em slice review --by`, `em slice ratify --by`. " +
882
+ "Questions queued by `em slice defer` for the new version (GH #236) are injected into its Open Questions. " +
883
+ "A ratified, never-shipped doc REFUSES without --withdraw (revising withdraws the only implementable version; " +
884
+ "queue a question with `em slice defer` instead)";
885
+ slice
886
+ .command("revise")
887
+ .description(REVISE_DESCRIPTION)
888
+ .argument("<file>", "input .em file")
889
+ .argument("<slice-key>", "slice export key (kebab-case)")
890
+ .option("--withdraw", "GH #236: required on a ratified, never-shipped doc — confirms withdrawing the ratification of the only implementable version")
891
+ .action(reviseAction("revise"));
806
892
  slice
807
893
  .command("reratify")
808
- .description("bump `version:` and flip a shipped slice doc's frontmatter back to `status: " +
809
- "ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice " +
810
- "mark-implemented`). Applies to a doc at `status: implemented`, or (MIL-258) to a " +
811
- "`ready-to-implement` doc that is ratified but not yet shipped — there it bumps `version:` " +
812
- "and clears the sign-off but leaves `status:` alone; a `ready-to-implement` doc with no " +
813
- "`ratifiedBy:` refuses (awaiting `em slice ratify --by`). Clears any stale " +
814
- "`ratifiedBy:`/`ratifiedOn:`/`reviewedBy:`/`reviewedOn:` (they describe the PRIOR version's " +
815
- "review and sign-off) so a follow-up `em slice ratify --by` applies cleanly — and needs no " +
816
- "fresh review; never touches `implementedIn:` or the doc body")
894
+ .description("DEPRECATED alias of `em slice revise` (MIL-283, since 1.15.0; removed in a later minor). Until 1.14 this " +
895
+ "left the doc at an unsigned `ready-to-implement`; it now opens a `draft` of the next version like `revise`. " +
896
+ "The --meaning-unchanged/--contract-change flags are accepted and ignored with a notice — the API-first " +
897
+ "confirmation is taken by `em slice ratify` on the new version")
817
898
  .argument("<file>", "input .em file")
818
899
  .argument("<slice-key>", "slice export key (kebab-case)")
819
- .option("--meaning-unchanged", "API-first sign-off (MIL-238): the new version does not change what the public contract means — " +
820
- "records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
821
- .option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read the new version differently, and why — records " +
822
- "`contractChange: \"<why>\"`")
823
- .action((file, sliceKey, opts) => {
824
- const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
825
- if (!flags.ok) {
826
- console.error(`em slice reratify: ${flags.message}`);
827
- process.exit(1);
828
- }
829
- const { model, refs, diagnostics, source } = compileFile(file);
830
- printDiagnostics(diagnostics);
831
- // Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
832
- // `em validate --slice-ready` are: only an error concerning THIS slice refuses.
833
- const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
834
- if (scopedErrors.length > 0) {
835
- console.error(`em slice reratify: slice "${sliceKey}" has errors — fix them first`);
836
- process.exit(1);
837
- }
838
- const result = runReratify(model, refs, dirname(file), sliceKey, flags.confirmation);
839
- if (!result.ok) {
840
- console.error(`em slice reratify: ${result.message}`);
841
- process.exit(1);
842
- }
843
- // MIL-214: advisory only, never refuses — see reratify.ts's reratifyAdvisory. Both warnings
844
- // can fire together (a version that was never certified AND still carries unruled findings).
845
- if (result.advisory?.neverCertified) {
846
- console.error(`warn: reratifying "${sliceKey}" whose v${result.newVersion - 1} was never certified`);
847
- }
848
- if (result.advisory && result.advisory.unruledFindingsCount > 0) {
849
- console.error(`warn: reratifying "${sliceKey}" has ${result.advisory.unruledFindingsCount} unruled conformance finding(s)`);
850
- }
851
- if (result.kind === "unshipped") {
852
- // MIL-258: nothing was flipped — say what DID happen, and that the new version is unsigned.
853
- console.log(`reratified: ${result.path} (version: ${result.newVersion - 1} -> ${result.newVersion}, sign-off cleared, ` +
854
- `status unchanged: ready-to-implement${confirmationSuffix(flags.confirmation)}) — not ratified until ` +
855
- `\`em slice ratify --by <name>\` records the new sign-off`);
856
- }
857
- else {
858
- console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement${confirmationSuffix(flags.confirmation)})`);
859
- }
860
- if (result.reopened > 0)
861
- console.log(`re-opened ${result.reopened} deferred question(s)`);
862
- warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
863
- });
900
+ .option("--meaning-unchanged", "ignored since 1.15.0 (see above)")
901
+ .option("--contract-change <why>", "ignored since 1.15.0 (see above)")
902
+ .option("--withdraw", "see `em slice revise --withdraw`")
903
+ .action(reviseAction("reratify"));
864
904
  slice
865
905
  .command("defer")
866
906
  .description("record a question this version will not answer as deferred to a later version (MIL-275): " +
@@ -868,12 +908,19 @@ slice
868
908
  "v<current>: <decision>; deferred to v<n> (<date>[, <by>])` so `--slice-ready` passes without a " +
869
909
  "guess, and mirrors it into `.event-modeling.md` (the parking lot and a dated Decisions-log " +
870
910
  "bullet; the file must exist, it is never created). Idempotent; `em slice reratify` re-opens " +
871
- "the item when it bumps to v<n>")
911
+ "the item when it bumps to v<n>. With --from-issue <text> instead of <question> (MIL-281): " +
912
+ "promotes the single open `issue \"...\"` on the slice's own elements containing <text> into " +
913
+ "the doc's Open Questions as that same deferred item, mirrors it, and removes the `issue` " +
914
+ "clause from the .em — the one-command path for a question raised in a review session. " +
915
+ "On a RATIFIED (`ready-to-implement` + `ratifiedBy:`) or shipped (`implemented`) doc (GH #236) the " +
916
+ "doc is NOT edited — its body is the signed spec — and the question is queued in `.event-modeling.md` " +
917
+ "for v<N+1>; `em slice revise` / `mark-implemented` inject it into the next draft")
872
918
  .argument("<file>", "input .em file")
873
919
  .argument("<slice-key>", "slice export key (kebab-case)")
874
- .argument("<question>", "text identifying the unchecked Open Question (case-sensitive substring)")
920
+ .argument("[question]", "text identifying the unchecked Open Question (case-sensitive substring); omit when using --from-issue")
875
921
  .requiredOption("--until <version>", "the version that must answer it, v<n> (greater than the doc's version)")
876
922
  .requiredOption("--decision <text>", "what this version does instead")
923
+ .option("--from-issue <text>", "defer a captured `issue \"...\"` on this slice instead of a doc Open Question (case-sensitive substring of its text)")
877
924
  .option("--by <name>", "who deferred it")
878
925
  .option("--on <date>", "deferral date, YYYY-MM-DD (default: today, local date)")
879
926
  .action((file, sliceKey, question, opts) => {
@@ -881,24 +928,47 @@ slice
881
928
  console.error(`em slice defer: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
882
929
  process.exit(1);
883
930
  }
931
+ if ((question === undefined) === (opts.fromIssue === undefined)) {
932
+ console.error('em slice defer: pass either "<question>" or --from-issue <text>, not both and not neither');
933
+ process.exit(1);
934
+ }
884
935
  const { model, refs, diagnostics } = compileFile(file);
885
936
  printDiagnostics(diagnostics);
937
+ sliceKey = resolveSliceOrExit("em slice defer", model, refs, sliceKey);
886
938
  const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
887
939
  if (scopedErrors.length > 0) {
888
940
  console.error(`em slice defer: slice "${sliceKey}" has errors — fix them first`);
889
941
  process.exit(1);
890
942
  }
891
- const result = runDefer(model, refs, dirname(file), sliceKey, {
892
- question,
893
- until: opts.until,
894
- decision: opts.decision,
895
- by: opts.by,
896
- on: opts.on ?? localIsoDate(),
897
- });
943
+ const common = { until: opts.until, decision: opts.decision, by: opts.by, on: opts.on ?? localIsoDate() };
944
+ if (opts.fromIssue !== undefined) {
945
+ const result = runDeferFromIssue(model, refs, dirname(file), file, sliceKey, opts.fromIssue, common);
946
+ if (!result.ok) {
947
+ console.error(`em slice defer: ${result.message}`);
948
+ process.exit(1);
949
+ }
950
+ if (result.queued) {
951
+ console.log(result.changed
952
+ ? `queued, not edited: ${result.path} stays as ratified (v${result.queued.docVersion}) — issue "${result.issue}" → ${opts.until} queued for ${sliceKey}@v${result.queued.forVersion} in .event-modeling.md; issue clause removed from ${file}:${result.modelLine}`
953
+ : `already queued (no-op): ${result.path}`);
954
+ return;
955
+ }
956
+ console.log(result.changed
957
+ ? `deferred: ${result.path} (issue "${result.issue}" → ${opts.until}; issue clause removed from ${file}:${result.modelLine})`
958
+ : `already deferred (no-op): ${result.path}`);
959
+ return;
960
+ }
961
+ const result = runDefer(model, refs, dirname(file), sliceKey, { question: question, ...common });
898
962
  if (!result.ok) {
899
963
  console.error(`em slice defer: ${result.message}`);
900
964
  process.exit(1);
901
965
  }
966
+ if (result.queued) {
967
+ console.log(result.changed
968
+ ? `queued, not edited: ${result.path} stays as ratified (v${result.queued.docVersion}) — "${question}" → ${opts.until} queued for ${sliceKey}@v${result.queued.forVersion} in .event-modeling.md; \`em slice revise\` or \`mark-implemented\` injects it into the next draft`
969
+ : `already queued (no-op): ${result.path}`);
970
+ return;
971
+ }
902
972
  console.log(result.changed
903
973
  ? `deferred: ${result.path} (${question} → ${opts.until})`
904
974
  : `already deferred (no-op): ${result.path}`);
@@ -907,7 +977,8 @@ slice
907
977
  .command("conform")
908
978
  .description("record per-slice-per-version conformance certification: sets `conformedVersion:`/" +
909
979
  "`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` " +
910
- "now depends on. Legal only for `status: implemented` with an `implementedIn:` link. " +
980
+ "now depends on. Legal only for a shipped slice (`status: implemented`, or any status with " +
981
+ "a `shippedVersion:` — MIL-283) with an `implementedIn:` link; `conformedVersion:` is the shipped version. " +
911
982
  "Idempotent on the same (version, --at) pair; a different --at simply overwrites (a " +
912
983
  "later re-certification is normal — there's no way for em to tell 'later' from 'earlier' " +
913
984
  "for an arbitrary revision string, so there's no --force to reach for). Refuses when the " +
@@ -921,7 +992,8 @@ slice
921
992
  .action((file, sliceKey, opts) => {
922
993
  const { model, refs, diagnostics } = compileFile(file);
923
994
  printDiagnostics(diagnostics);
924
- // Scoped the same way `em slice ratify`/`em slice reratify`/`em slice mark-implemented` are:
995
+ sliceKey = resolveSliceOrExit("em slice conform", model, refs, sliceKey);
996
+ // Scoped the same way `em slice ratify`/`em slice revise`/`em slice mark-implemented` are:
925
997
  // only an error concerning THIS slice refuses.
926
998
  const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
927
999
  if (scopedErrors.length > 0) {
@@ -1080,7 +1152,7 @@ state
1080
1152
  const compiled = compileFile(modelFile);
1081
1153
  if (!hasErrors(compiled.diagnostics)) {
1082
1154
  const { facts } = resolveSliceDocFacts(compiled.model, compiled.refs, modelDir);
1083
- inScope = new Set(facts.filter((f) => f.status === "implemented").map((f) => f.key));
1155
+ inScope = new Set(facts.filter((f) => f.shipped).map((f) => f.key));
1084
1156
  compiledForCertify = { model: compiled.model, refs: compiled.refs, source: compiled.source };
1085
1157
  }
1086
1158
  }
@@ -1514,7 +1586,10 @@ program
1514
1586
  // R34 (MIL-268): one shared verdict helper for this flag, the MCP `slice_ready` tool, and
1515
1587
  // `em engagement plan`.
1516
1588
  const contractInput = { file, source, diagnostics };
1517
- const key = opts.sliceReady;
1589
+ // GH #237: an unresolvable input goes through as typed so the gate reports its own
1590
+ // `slice-ready-unknown-slice` error (carrying the resolver's message).
1591
+ const resolved = resolveSliceKey(model, refs, opts.sliceReady);
1592
+ const key = resolved.ok ? resolved.key : opts.sliceReady;
1518
1593
  const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics);
1519
1594
  printDiagnostics(scoped);
1520
1595
  if (opts.json) {
@@ -1711,11 +1786,8 @@ program
1711
1786
  let onlyKey;
1712
1787
  let continuationOfKey = null;
1713
1788
  if (opts.slice !== undefined) {
1714
- const target = resolveCoverageSliceKey(model, refs, opts.slice);
1715
- if (!target) {
1716
- console.error(`em coverage --slice: no slice with export key "${opts.slice}" in this model`);
1717
- process.exit(1);
1718
- }
1789
+ const resolved = resolveSliceOrExit("em coverage --slice", model, refs, opts.slice);
1790
+ const target = resolveCoverageSliceKey(model, refs, resolved);
1719
1791
  onlyKey = target.key;
1720
1792
  continuationOfKey = target.continuationOf;
1721
1793
  }
@@ -1873,7 +1945,7 @@ program
1873
1945
  // merged `sliceFacts` above (which spans every input file when several are given).
1874
1946
  const conformance = compiled.map(({ file }) => {
1875
1947
  const facts = factsByFile.get(file) ?? [];
1876
- const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn }));
1948
+ const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn, shipped: f.shipped }));
1877
1949
  return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
1878
1950
  });
1879
1951
  // MIL-218: one model-version entry per input file.
@@ -2131,6 +2203,7 @@ engagement
2131
2203
  .option("--downstream-of <ref>", "a slice key, element ref, or display name: it and everything downstream of it (loops-to excluded)")
2132
2204
  .option("--parallel <n>", "how many slices of one level may be in flight at once", "3")
2133
2205
  .option("--by <name>", "who created the engagement (createdBy)")
2206
+ .option("--tracking <key>", "an issue-tracker key or URL the slice PRs cite (a slice doc's own tracking: wins)")
2134
2207
  .option("--force", "overwrite an existing engagement file with this slug")
2135
2208
  .action((file, slug, opts) => {
2136
2209
  const fail = (m) => {
@@ -2148,7 +2221,7 @@ engagement
2148
2221
  const parallel = /^\d+$/.test(opts.parallel) ? Number(opts.parallel) : NaN;
2149
2222
  const { model, refs } = compileFile(file);
2150
2223
  const path = engagementPath(file, slug);
2151
- const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, created: localIsoDate() });
2224
+ const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, tracking: opts.tracking ?? null, created: localIsoDate() });
2152
2225
  if (!result.ok)
2153
2226
  return fail(result.message);
2154
2227
  if (existsSync(path) && !opts.force)
@@ -2159,6 +2232,16 @@ engagement
2159
2232
  writeFileSync(path, result.text);
2160
2233
  console.log(`wrote ${path} (${result.units.length} slice${result.units.length === 1 ? "" : "s"}: ${result.units.join(", ")})`);
2161
2234
  });
2235
+ /** GH #237: resolve a `<slice-key>` argument (export key, display name, or unique prefix —
2236
+ * docs/cli.md "Slice keys") to the canonical export key, or print the resolver's error and exit 1. */
2237
+ function resolveSliceOrExit(command, model, refs, input) {
2238
+ const resolved = resolveSliceKey(model, refs, input);
2239
+ if (!resolved.ok) {
2240
+ console.error(`${command}: ${resolved.message}`);
2241
+ process.exit(1);
2242
+ }
2243
+ return resolved.key;
2244
+ }
2162
2245
  /** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
2163
2246
  * each slice's `--slice-ready` verdict (R34). */
2164
2247
  function engagementModelInput(file) {
@@ -2169,7 +2252,7 @@ function engagementModelInput(file) {
2169
2252
  engagement
2170
2253
  .command("plan")
2171
2254
  .description("the engagement's build plan: dependency levels over model.edges (loops-to excluded), and per " +
2172
- "slice its readiness, branch impl/<key>, base (main or impl/<upstream>), and any hold")
2255
+ "slice its readiness, branch impl/<key>, base (always main — MIL-280), and any hold")
2173
2256
  .argument("<file>", "input .em file")
2174
2257
  .argument("<slug>", "engagement slug")
2175
2258
  .option("--json", "print a JSON document instead of text (same document as the MCP engagement_plan tool)")
@@ -2201,6 +2284,11 @@ engagement
2201
2284
  if (!loaded.ok)
2202
2285
  return fail(loaded.message);
2203
2286
  const { model, refs } = compileFile(file);
2287
+ // GH #237: an input that resolves to no slice goes through as typed, so the engagement's own
2288
+ // "is not in engagement" refusal (which names what the Ledger holds) still speaks.
2289
+ const resolved = resolveSliceKey(model, refs, key);
2290
+ if (resolved.ok)
2291
+ key = resolved.key;
2204
2292
  const result = applyEngagementSet({ file, model, refs, baseDir: dirname(file) }, slug, loaded.text, loaded.file, { key, ...opts });
2205
2293
  if (!result.ok)
2206
2294
  return fail(result.message);