@milehimikey/em 1.13.1 → 1.14.1
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.
- package/.claude/agents/em-critic.md +26 -0
- package/.claude/agents/em-implementer.md +34 -0
- package/.claude/agents/em-reviewer.md +31 -0
- package/.claude/agents/em-validator.md +27 -0
- package/.claude/skills/event-modeling/SKILL.md +50 -11
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +127 -18
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +38 -11
- package/.claude/skills/event-modeling-review/SKILL.md +2 -2
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +279 -189
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +116 -5
- package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
- package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
- package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +25 -11
- package/dist/catalog/apiFirst.d.ts +28 -0
- package/dist/catalog/apiFirst.js +49 -0
- package/dist/catalog/apiFirst.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +13 -0
- package/dist/catalog/docJoin.js +11 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +44 -4
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +4 -0
- package/dist/catalog/invariantsValidate.js +102 -0
- package/dist/catalog/invariantsValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +15 -0
- package/dist/catalog/scenarios.js +97 -0
- package/dist/catalog/scenarios.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +8 -0
- package/dist/catalog/sliceDoc.js +9 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +31 -4
- package/dist/catalog/sliceReadyValidate.js +66 -8
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +55 -0
- package/dist/catalog/sliceSections.js +317 -0
- package/dist/catalog/sliceSections.js.map +1 -0
- package/dist/cli/agentsMd.js +7 -1
- package/dist/cli/agentsMd.js.map +1 -1
- package/dist/cli/api.d.ts +90 -0
- package/dist/cli/api.js +318 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/ciInit.d.ts +82 -21
- package/dist/cli/ciInit.js +468 -64
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +28 -0
- package/dist/cli/conformScope.js +67 -0
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/coverage.d.ts +9 -0
- package/dist/cli/coverage.js +60 -8
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/defer.d.ts +55 -0
- package/dist/cli/defer.js +244 -0
- package/dist/cli/defer.js.map +1 -0
- package/dist/cli/engagement.d.ts +210 -0
- package/dist/cli/engagement.js +444 -0
- package/dist/cli/engagement.js.map +1 -0
- package/dist/cli/engagementFile.d.ts +86 -0
- package/dist/cli/engagementFile.js +255 -0
- package/dist/cli/engagementFile.js.map +1 -0
- package/dist/cli/metrics.d.ts +10 -0
- package/dist/cli/metrics.js +49 -1
- package/dist/cli/metrics.js.map +1 -1
- package/dist/cli/pluginPin.d.ts +55 -0
- package/dist/cli/pluginPin.js +188 -0
- package/dist/cli/pluginPin.js.map +1 -0
- package/dist/cli/ratify.d.ts +27 -2
- package/dist/cli/ratify.js +112 -13
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.d.ts +4 -2
- package/dist/cli/reratify.js +41 -16
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/scopeInputs.d.ts +48 -0
- package/dist/cli/scopeInputs.js +176 -0
- package/dist/cli/scopeInputs.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +15 -1
- package/dist/cli/skillCheck.js +22 -1
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +5 -0
- package/dist/cli/skillDirs.js +13 -1
- package/dist/cli/skillDirs.js.map +1 -1
- package/dist/cli/skillSync.d.ts +4 -0
- package/dist/cli/skillSync.js +23 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +11 -7
- package/dist/cli/sliceNew.js +91 -18
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceSync.d.ts +50 -0
- package/dist/cli/sliceSync.js +170 -0
- package/dist/cli/sliceSync.js.map +1 -0
- package/dist/cli/status.d.ts +24 -1
- package/dist/cli/status.js +20 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/systemInputs.d.ts +53 -10
- package/dist/cli/systemInputs.js +186 -11
- package/dist/cli/systemInputs.js.map +1 -1
- package/dist/cli/upgrade.d.ts +16 -3
- package/dist/cli/upgrade.js +366 -30
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/usageLog.d.ts +4 -2
- package/dist/cli/usageLog.js +4 -2
- package/dist/cli/usageLog.js.map +1 -1
- package/dist/cli.js +599 -94
- package/dist/cli.js.map +1 -1
- package/dist/emit/api.d.ts +29 -0
- package/dist/emit/api.js +220 -0
- package/dist/emit/api.js.map +1 -0
- package/dist/emit/apiCheckJson.d.ts +3 -0
- package/dist/emit/apiCheckJson.js +21 -0
- package/dist/emit/apiCheckJson.js.map +1 -0
- package/dist/emit/codeownersJson.d.ts +3 -0
- package/dist/emit/codeownersJson.js +19 -0
- package/dist/emit/codeownersJson.js.map +1 -0
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +8 -0
- package/dist/emit/engagementJson.js +46 -0
- package/dist/emit/engagementJson.js.map +1 -0
- package/dist/emit/glossaryJson.d.ts +1 -1
- package/dist/emit/glossaryJson.js +4 -1
- package/dist/emit/glossaryJson.js.map +1 -1
- package/dist/emit/json.d.ts +17 -1
- package/dist/emit/json.js +30 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/metricsJson.d.ts +1 -1
- package/dist/emit/metricsJson.js +4 -1
- package/dist/emit/metricsJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +7 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/scopeJson.d.ts +12 -0
- package/dist/emit/scopeJson.js +26 -0
- package/dist/emit/scopeJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +3 -2
- package/dist/emit/skillCheckJson.js +10 -2
- package/dist/emit/skillCheckJson.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +15 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/systemJson.d.ts +15 -8
- package/dist/emit/systemJson.js +28 -13
- package/dist/emit/systemJson.js.map +1 -1
- package/dist/emit/validateJson.d.ts +3 -3
- package/dist/emit/validateJson.js +10 -4
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +163 -31
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/diff.d.ts +4 -1
- package/dist/model/diff.js +44 -8
- package/dist/model/diff.js.map +1 -1
- package/dist/model/glossary.d.ts +2 -0
- package/dist/model/glossary.js +1 -0
- package/dist/model/glossary.js.map +1 -1
- package/dist/model/model.d.ts +55 -2
- package/dist/model/model.js +57 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/qualifiedRef.d.ts +18 -0
- package/dist/model/qualifiedRef.js +26 -0
- package/dist/model/qualifiedRef.js.map +1 -1
- package/dist/model/queryIndex.d.ts +9 -1
- package/dist/model/queryIndex.js +18 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/rules.d.ts +143 -2
- package/dist/model/rules.js +162 -3
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.d.ts +40 -0
- package/dist/model/validate.js +147 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +31 -3
- package/dist/parser/parser.js +148 -14
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -1
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +6 -0
- package/dist/query/verbs.js +6 -2
- package/dist/query/verbs.js.map +1 -1
- package/dist/system/adaptation.d.ts +14 -0
- package/dist/system/adaptation.js +49 -0
- package/dist/system/adaptation.js.map +1 -0
- package/dist/system/codeowners.d.ts +77 -0
- package/dist/system/codeowners.js +242 -0
- package/dist/system/codeowners.js.map +1 -0
- package/dist/system/manifest.d.ts +10 -5
- package/dist/system/manifest.js +54 -26
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/migrateManifest.d.ts +28 -0
- package/dist/system/migrateManifest.js +155 -0
- package/dist/system/migrateManifest.js.map +1 -0
- package/dist/system/scope.d.ts +46 -0
- package/dist/system/scope.js +132 -0
- package/dist/system/scope.js.map +1 -0
- package/dist/system/verify.d.ts +50 -13
- package/dist/system/verify.js +179 -58
- package/dist/system/verify.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +23 -2
- package/dist/templates.js.map +1 -1
- package/package.json +5 -2
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/agents/em-critic.md +27 -0
- package/plugin/agents/em-implementer.md +35 -0
- package/plugin/agents/em-reviewer.md +32 -0
- package/plugin/agents/em-validator.md +28 -0
- package/plugin/shared/reference/em-dsl.md +871 -0
- package/plugin/shared/reference/methodology.md +329 -0
- package/plugin/shared/reference/operating-principles.md +222 -0
- package/plugin/shared/reference/slice-doc-schema.md +574 -0
- package/plugin/shared/templates/conformance-report.md +85 -0
- package/plugin/shared/templates/constitution.md +136 -0
- package/plugin/shared/templates/model-readme.md +46 -0
- package/plugin/shared/templates/slice.md +251 -0
- package/plugin/shared/templates/state.md +77 -0
- package/plugin/skills/conform/SKILL.md +93 -0
- package/plugin/skills/conform/reference/conform.md +428 -0
- package/plugin/skills/design/SKILL.md +298 -0
- package/plugin/skills/discover/SKILL.md +90 -0
- package/plugin/skills/discover/reference/extract.md +147 -0
- package/plugin/skills/engagement/SKILL.md +193 -0
- package/plugin/skills/engagement/reference/engagement.md +144 -0
- package/plugin/skills/event-modeling/SKILL.md +132 -0
- package/plugin/skills/implement/SKILL.md +61 -0
- package/plugin/skills/implement/reference/implement.md +439 -0
- package/plugin/skills/review/SKILL.md +74 -0
- package/dist/emit/typespec.d.ts +0 -26
- package/dist/emit/typespec.js +0 -356
- 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 {
|
|
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 {
|
|
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,23 +64,26 @@ 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";
|
|
67
77
|
import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
68
78
|
import { runReratify } from "./cli/reratify.js";
|
|
79
|
+
import { runDefer } from "./cli/defer.js";
|
|
69
80
|
import { runSliceConform } from "./cli/sliceConform.js";
|
|
70
81
|
import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
|
|
71
82
|
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
|
|
72
83
|
import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
|
|
73
84
|
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
|
|
74
85
|
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";
|
|
86
|
+
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
87
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
77
88
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
78
89
|
import { isOlder } from "./util/semver.js";
|
|
@@ -84,7 +95,9 @@ import { buildQuerySystem } from "./query/system.js";
|
|
|
84
95
|
import { queryConsumers, queryProducers, queryDownstream, queryUpstream, querySlices, queryInvariant, queryField, queryPath, } from "./query/verbs.js";
|
|
85
96
|
import { formatConsumers, formatProducers, formatClosure, formatSlices, formatInvariant, formatField, formatPath, } from "./query/format.js";
|
|
86
97
|
import { buildQueryJson } from "./emit/queryJson.js";
|
|
87
|
-
import { loadSystem } from "./cli/systemInputs.js";
|
|
98
|
+
import { gitProducerCommit, loadSystem, statusSystemBlock, SYSTEM_MANIFEST_FILE } from "./cli/systemInputs.js";
|
|
99
|
+
import { runCodeowners } from "./system/codeowners.js";
|
|
100
|
+
import { buildCodeownersJson } from "./emit/codeownersJson.js";
|
|
88
101
|
import { verifySystem } from "./system/verify.js";
|
|
89
102
|
import { buildSystemJson } from "./emit/systemJson.js";
|
|
90
103
|
const program = new Command();
|
|
@@ -95,7 +108,10 @@ const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.me
|
|
|
95
108
|
program
|
|
96
109
|
.name("em")
|
|
97
110
|
.description("Event Modeling CLI — slice-first DSL rendered as a strict Graphviz grid")
|
|
98
|
-
.version(PKG_VERSION)
|
|
111
|
+
.version(PKG_VERSION)
|
|
112
|
+
// Program options (`--version`) only before the subcommand name: `em system codeowners --json`
|
|
113
|
+
// must reach the subcommand (MIL-234), and `em system` has its own `--json`.
|
|
114
|
+
.enablePositionalOptions();
|
|
99
115
|
program
|
|
100
116
|
.command("init")
|
|
101
117
|
.description("scaffold a starter .em model")
|
|
@@ -265,30 +281,67 @@ program
|
|
|
265
281
|
process.stdout.write(exported.text + "\n");
|
|
266
282
|
}
|
|
267
283
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
276
|
-
printDiagnostics(diagnostics);
|
|
277
|
-
if (hasErrors(diagnostics)) {
|
|
284
|
+
/** `em api generate` (and its deprecated `em typespec` alias): compile, refuse on errors,
|
|
285
|
+
* write `<modelDir>/contracts/<modelKey>.tsp` (R6) — or `-o`, or stdout. */
|
|
286
|
+
function runApiGenerate(file, opts) {
|
|
287
|
+
const compiled = compileFile(file);
|
|
288
|
+
const { source } = compiled;
|
|
289
|
+
printDiagnostics(compiled.diagnostics);
|
|
290
|
+
if (hasErrors(compiled.diagnostics)) {
|
|
278
291
|
console.error("not generating: fix the errors above");
|
|
279
292
|
process.exit(1);
|
|
280
293
|
}
|
|
281
|
-
const generated =
|
|
282
|
-
printDiagnostics(newDiagnostics(generated.diagnostics, diagnostics));
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
writeFileSync(opts.out, generated.text + "\n");
|
|
287
|
-
console.log(`wrote ${opts.out}`);
|
|
294
|
+
const generated = generateContract(file, source, compiled, opts.out);
|
|
295
|
+
printDiagnostics(newDiagnostics(generated.diagnostics, compiled.diagnostics));
|
|
296
|
+
if (opts.stdout) {
|
|
297
|
+
process.stdout.write(generated.text);
|
|
298
|
+
return;
|
|
288
299
|
}
|
|
289
|
-
|
|
290
|
-
|
|
300
|
+
mkdirSync(dirname(generated.contractPath), { recursive: true });
|
|
301
|
+
writeFileSync(generated.contractPath, generated.text);
|
|
302
|
+
console.log(`wrote ${generated.contractPath}`);
|
|
303
|
+
}
|
|
304
|
+
const api = program
|
|
305
|
+
.command("api")
|
|
306
|
+
.description("the model-owned TypeSpec contract for its `public` surface (MIL-237, see docs/cli.md)");
|
|
307
|
+
api
|
|
308
|
+
.command("generate")
|
|
309
|
+
.description("write the TypeSpec contract for the model's public commands, events and views to " +
|
|
310
|
+
"<model dir>/contracts/<model key>.tsp")
|
|
311
|
+
.argument("<file>", "input .em file")
|
|
312
|
+
.option("-o, --out <path>", "write to this path instead of <model dir>/contracts/<model key>.tsp")
|
|
313
|
+
.option("--stdout", "print the contract instead of writing it")
|
|
314
|
+
.action((file, opts) => runApiGenerate(file, opts));
|
|
315
|
+
api
|
|
316
|
+
.command("check")
|
|
317
|
+
.description("is the committed contract current? With --base, annotate each public-surface change since " +
|
|
318
|
+
"that revision additive or breaking (annotation only — exits 1 only when stale)")
|
|
319
|
+
.argument("<file>", "input .em file")
|
|
320
|
+
.option("--base <rev>", "git revision to diff the public surface against")
|
|
321
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
322
|
+
.action((file, opts) => {
|
|
323
|
+
const outcome = runApiCheck(file, opts.base);
|
|
324
|
+
printDiagnostics(outcome.diagnostics);
|
|
325
|
+
if (!outcome.ok) {
|
|
326
|
+
console.error(outcome.message);
|
|
327
|
+
process.exit(1);
|
|
291
328
|
}
|
|
329
|
+
if (opts.json)
|
|
330
|
+
process.stdout.write(buildApiCheckJson(outcome.report) + "\n");
|
|
331
|
+
else
|
|
332
|
+
for (const line of formatApiCheckText(outcome.report))
|
|
333
|
+
console.log(line);
|
|
334
|
+
if (!outcome.report.current)
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
});
|
|
337
|
+
program
|
|
338
|
+
.command("typespec")
|
|
339
|
+
.description("DEPRECATED alias of `em api generate --stdout` (use em api generate)")
|
|
340
|
+
.argument("<file>", "input .em file")
|
|
341
|
+
.option("-o, --out <path>", "write to a file instead of stdout")
|
|
342
|
+
.action((file, opts) => {
|
|
343
|
+
console.error("warn: em typespec is deprecated — use em api generate");
|
|
344
|
+
runApiGenerate(file, opts.out ? { out: opts.out } : { stdout: true });
|
|
292
345
|
});
|
|
293
346
|
program
|
|
294
347
|
.command("diff")
|
|
@@ -437,9 +490,11 @@ program
|
|
|
437
490
|
const slice = program.command("slice").description("author and maintain slice docs");
|
|
438
491
|
slice
|
|
439
492
|
.command("new")
|
|
440
|
-
.description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft
|
|
441
|
-
"
|
|
442
|
-
"
|
|
493
|
+
.description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft`, " +
|
|
494
|
+
"the `# Slice:` heading and diagram-image stub, then every template section: the generated " +
|
|
495
|
+
"regions (command/event/view field tables, Invariants) filled from the model with --wire " +
|
|
496
|
+
"(template placeholders otherwise — `em slice sync` fills them), the authored sections " +
|
|
497
|
+
"(Intent, Scenarios, Open Questions, ...) with placeholder bullets (see " +
|
|
443
498
|
".claude/skills/event-modeling-shared/reference/slice-doc-schema.md, templates/slice.md)")
|
|
444
499
|
.argument("<name>", "slice display name (e.g. \"Request Payment\") — kebab-cased for the filename")
|
|
445
500
|
.requiredOption("--pattern <pattern>", `slice pattern: ${SLICE_PATTERNS.join(" | ")}`)
|
|
@@ -466,6 +521,9 @@ slice
|
|
|
466
521
|
// (no matching slice, ambiguous primary element, already wired) leaves nothing written at
|
|
467
522
|
// all rather than a doc with no wiring and a non-zero exit.
|
|
468
523
|
let wired = null;
|
|
524
|
+
// MIL-266: with a model in hand (--wire), the doc's generated regions are filled from it;
|
|
525
|
+
// without one they carry the template's placeholders until `em slice sync` fills them.
|
|
526
|
+
let regions;
|
|
469
527
|
if (opts.wire) {
|
|
470
528
|
const { model, refs, diagnostics, source } = compileFile(opts.wire);
|
|
471
529
|
printDiagnostics(diagnostics);
|
|
@@ -479,11 +537,12 @@ slice
|
|
|
479
537
|
process.exit(1);
|
|
480
538
|
}
|
|
481
539
|
wired = result;
|
|
540
|
+
regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
|
|
482
541
|
}
|
|
483
542
|
mkdirSync(dir, { recursive: true });
|
|
484
543
|
const content = opts.stub
|
|
485
544
|
? buildStubDocContent(name, opts.pattern, opts.swimlane)
|
|
486
|
-
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane);
|
|
545
|
+
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions);
|
|
487
546
|
writeFileSync(path, content);
|
|
488
547
|
console.log(`wrote ${path}`);
|
|
489
548
|
if (wired) {
|
|
@@ -495,6 +554,39 @@ slice
|
|
|
495
554
|
console.log(` note "${path}"`);
|
|
496
555
|
}
|
|
497
556
|
});
|
|
557
|
+
slice
|
|
558
|
+
.command("sync")
|
|
559
|
+
.description("regenerate the GENERATED regions (command/event/view field tables, the Invariants list) of " +
|
|
560
|
+
"existing slice docs in place from the model, authored sections untouched (MIL-266); docs " +
|
|
561
|
+
"without regions are skipped with a note. --check never writes and exits 1 when a doc is stale")
|
|
562
|
+
.argument("<model-file>", "the .em model whose bound slice docs to sync")
|
|
563
|
+
.argument("[key]", "only the doc this slice's export key resolves to")
|
|
564
|
+
.option("--check", "report ok / stale / no-regions per doc without writing; exit 1 when any is stale")
|
|
565
|
+
.option("--json", "print the machine-readable report (sliceSyncSchemaVersion 1.0)")
|
|
566
|
+
.action((file, key, opts) => {
|
|
567
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
568
|
+
printDiagnostics(diagnostics.filter((d) => d.severity === "error"));
|
|
569
|
+
if (hasErrors(diagnostics)) {
|
|
570
|
+
console.error(`em slice sync: not syncing — fix the errors in ${file} above`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
const check = opts.check === true;
|
|
574
|
+
const outcome = runSliceSync(model, refs, dirname(file), key, check);
|
|
575
|
+
if (!outcome.ok) {
|
|
576
|
+
console.error(`em slice sync: ${outcome.message}`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
const { out, err } = formatSliceSync(outcome.docs, check);
|
|
580
|
+
for (const line of err)
|
|
581
|
+
console.error(line);
|
|
582
|
+
if (opts.json)
|
|
583
|
+
process.stdout.write(buildSliceSyncJson(file, outcome.docs) + "\n");
|
|
584
|
+
else
|
|
585
|
+
for (const line of out)
|
|
586
|
+
console.log(line);
|
|
587
|
+
if (sliceSyncFailed(outcome.docs, check))
|
|
588
|
+
process.exit(1);
|
|
589
|
+
});
|
|
498
590
|
slice
|
|
499
591
|
.command("stub-all")
|
|
500
592
|
.description("scaffold + wire a near-free stub (`em slice new --stub`'s content, MIL-184) for every " +
|
|
@@ -642,6 +734,10 @@ slice
|
|
|
642
734
|
? `reviewed: ${result.path} (reviewedBy: ${opts.by}, reviewedOn: ${reviewedOn})`
|
|
643
735
|
: `already reviewed (no-op): ${result.path}`);
|
|
644
736
|
});
|
|
737
|
+
/** MIL-238: `, meaningConfirmed: true` / `, contractChange: "<why>"` for a success line, or "". */
|
|
738
|
+
function confirmationSuffix(c) {
|
|
739
|
+
return c ? `, ${meaningConfirmationLine(c)}` : "";
|
|
740
|
+
}
|
|
645
741
|
slice
|
|
646
742
|
.command("ratify")
|
|
647
743
|
.description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
|
|
@@ -659,7 +755,16 @@ slice
|
|
|
659
755
|
.requiredOption("--by <name>", "the ratifier's name")
|
|
660
756
|
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
661
757
|
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
|
|
758
|
+
.option("--meaning-unchanged", "API-first sign-off (MIL-238): this version does not change what the public contract means — " +
|
|
759
|
+
"records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
|
|
760
|
+
.option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read this change differently, and why — records " +
|
|
761
|
+
"`contractChange: \"<why>\"`")
|
|
662
762
|
.action((file, sliceKey, opts) => {
|
|
763
|
+
const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
|
|
764
|
+
if (!flags.ok) {
|
|
765
|
+
console.error(`em slice ratify: ${flags.message}`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
663
768
|
const { model, refs, diagnostics, source } = compileFile(file);
|
|
664
769
|
printDiagnostics(diagnostics);
|
|
665
770
|
// Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
|
|
@@ -675,7 +780,7 @@ slice
|
|
|
675
780
|
console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
676
781
|
process.exit(1);
|
|
677
782
|
}
|
|
678
|
-
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true);
|
|
783
|
+
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true, flags.confirmation);
|
|
679
784
|
if (!result.ok) {
|
|
680
785
|
console.error(`em slice ratify: ${result.message}`);
|
|
681
786
|
process.exit(1);
|
|
@@ -693,7 +798,7 @@ slice
|
|
|
693
798
|
console.error(`warn: ratifying "${sliceKey}" ahead of upstream slice "${upstream.sliceKey}" (status: ${upstream.status})`);
|
|
694
799
|
}
|
|
695
800
|
console.log(result.changed
|
|
696
|
-
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
|
|
801
|
+
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn}${confirmationSuffix(flags.confirmation)})`
|
|
697
802
|
: `already ratified (no-op): ${result.path}`);
|
|
698
803
|
warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
|
|
699
804
|
});
|
|
@@ -710,7 +815,16 @@ slice
|
|
|
710
815
|
"fresh review; never touches `implementedIn:` or the doc body")
|
|
711
816
|
.argument("<file>", "input .em file")
|
|
712
817
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
713
|
-
.
|
|
818
|
+
.option("--meaning-unchanged", "API-first sign-off (MIL-238): the new version does not change what the public contract means — " +
|
|
819
|
+
"records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
|
|
820
|
+
.option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read the new version differently, and why — records " +
|
|
821
|
+
"`contractChange: \"<why>\"`")
|
|
822
|
+
.action((file, sliceKey, opts) => {
|
|
823
|
+
const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
|
|
824
|
+
if (!flags.ok) {
|
|
825
|
+
console.error(`em slice reratify: ${flags.message}`);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
714
828
|
const { model, refs, diagnostics, source } = compileFile(file);
|
|
715
829
|
printDiagnostics(diagnostics);
|
|
716
830
|
// Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
|
|
@@ -720,7 +834,7 @@ slice
|
|
|
720
834
|
console.error(`em slice reratify: slice "${sliceKey}" has errors — fix them first`);
|
|
721
835
|
process.exit(1);
|
|
722
836
|
}
|
|
723
|
-
const result = runReratify(model, refs, dirname(file), sliceKey);
|
|
837
|
+
const result = runReratify(model, refs, dirname(file), sliceKey, flags.confirmation);
|
|
724
838
|
if (!result.ok) {
|
|
725
839
|
console.error(`em slice reratify: ${result.message}`);
|
|
726
840
|
process.exit(1);
|
|
@@ -736,13 +850,58 @@ slice
|
|
|
736
850
|
if (result.kind === "unshipped") {
|
|
737
851
|
// MIL-258: nothing was flipped — say what DID happen, and that the new version is unsigned.
|
|
738
852
|
console.log(`reratified: ${result.path} (version: ${result.newVersion - 1} -> ${result.newVersion}, sign-off cleared, ` +
|
|
739
|
-
`status unchanged: ready-to-implement) — not ratified until
|
|
853
|
+
`status unchanged: ready-to-implement${confirmationSuffix(flags.confirmation)}) — not ratified until ` +
|
|
854
|
+
`\`em slice ratify --by <name>\` records the new sign-off`);
|
|
740
855
|
}
|
|
741
856
|
else {
|
|
742
|
-
console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
|
|
857
|
+
console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement${confirmationSuffix(flags.confirmation)})`);
|
|
743
858
|
}
|
|
859
|
+
if (result.reopened > 0)
|
|
860
|
+
console.log(`re-opened ${result.reopened} deferred question(s)`);
|
|
744
861
|
warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
|
|
745
862
|
});
|
|
863
|
+
slice
|
|
864
|
+
.command("defer")
|
|
865
|
+
.description("record a question this version will not answer as deferred to a later version (MIL-275): " +
|
|
866
|
+
"rewrites the single unchecked `## Open Questions` item containing <question> as `- [x] <item> — " +
|
|
867
|
+
"v<current>: <decision>; deferred to v<n> (<date>[, <by>])` so `--slice-ready` passes without a " +
|
|
868
|
+
"guess, and mirrors it into `.event-modeling.md` (the parking lot and a dated Decisions-log " +
|
|
869
|
+
"bullet; the file must exist, it is never created). Idempotent; `em slice reratify` re-opens " +
|
|
870
|
+
"the item when it bumps to v<n>")
|
|
871
|
+
.argument("<file>", "input .em file")
|
|
872
|
+
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
873
|
+
.argument("<question>", "text identifying the unchecked Open Question (case-sensitive substring)")
|
|
874
|
+
.requiredOption("--until <version>", "the version that must answer it, v<n> (greater than the doc's version)")
|
|
875
|
+
.requiredOption("--decision <text>", "what this version does instead")
|
|
876
|
+
.option("--by <name>", "who deferred it")
|
|
877
|
+
.option("--on <date>", "deferral date, YYYY-MM-DD (default: today, local date)")
|
|
878
|
+
.action((file, sliceKey, question, opts) => {
|
|
879
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
880
|
+
console.error(`em slice defer: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
884
|
+
printDiagnostics(diagnostics);
|
|
885
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
886
|
+
if (scopedErrors.length > 0) {
|
|
887
|
+
console.error(`em slice defer: slice "${sliceKey}" has errors — fix them first`);
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
const result = runDefer(model, refs, dirname(file), sliceKey, {
|
|
891
|
+
question,
|
|
892
|
+
until: opts.until,
|
|
893
|
+
decision: opts.decision,
|
|
894
|
+
by: opts.by,
|
|
895
|
+
on: opts.on ?? localIsoDate(),
|
|
896
|
+
});
|
|
897
|
+
if (!result.ok) {
|
|
898
|
+
console.error(`em slice defer: ${result.message}`);
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
console.log(result.changed
|
|
902
|
+
? `deferred: ${result.path} (${question} → ${opts.until})`
|
|
903
|
+
: `already deferred (no-op): ${result.path}`);
|
|
904
|
+
});
|
|
746
905
|
slice
|
|
747
906
|
.command("conform")
|
|
748
907
|
.description("record per-slice-per-version conformance certification: sets `conformedVersion:`/" +
|
|
@@ -1323,10 +1482,12 @@ program
|
|
|
1323
1482
|
.allowExcessArguments(false)
|
|
1324
1483
|
.option("--list-issues", "print only open `issue` diagnostics (slice, element, line, text)")
|
|
1325
1484
|
.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")
|
|
1485
|
+
.option("--list-public", "print only commands, events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build")
|
|
1327
1486
|
.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
1487
|
.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
|
|
1488
|
+
"note binding, zero unchecked Open Questions, a recorded ratifiedBy, and — for a slice " +
|
|
1489
|
+
"touching a `public` element — a current contracts/<model key>.tsp; exits non-zero if not " +
|
|
1490
|
+
"ready (MIL-87, MIL-259, MIL-238)")
|
|
1330
1491
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
1331
1492
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
1332
1493
|
.action((file, opts) => {
|
|
@@ -1347,17 +1508,18 @@ program
|
|
|
1347
1508
|
// diagnostics printed — on an unrelated slice's breakage; scoping to this slice alone
|
|
1348
1509
|
// matches the ticket's own scenario (check one slice while the rest of a large model is
|
|
1349
1510
|
// still WIP) and this module's own "single-slice" framing.
|
|
1350
|
-
|
|
1351
|
-
|
|
1511
|
+
// MIL-238: the API-first gate needs the model path as given (the contract path is built
|
|
1512
|
+
// from it, as `em api generate` does), its source, and the compile diagnostics.
|
|
1513
|
+
// R34 (MIL-268): one shared verdict helper for this flag, the MCP `slice_ready` tool, and
|
|
1514
|
+
// `em engagement plan`.
|
|
1515
|
+
const contractInput = { file, source, diagnostics };
|
|
1352
1516
|
const key = opts.sliceReady;
|
|
1353
|
-
const scoped =
|
|
1517
|
+
const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics);
|
|
1354
1518
|
printDiagnostics(scoped);
|
|
1355
|
-
const ready = scoped.length === 0;
|
|
1356
1519
|
if (opts.json) {
|
|
1357
|
-
// MIL-128: the
|
|
1520
|
+
// MIL-128: the named gates (see computeSliceReadyGates) plus the same `scoped`
|
|
1358
1521
|
// diagnostics and `ready` verdict driving the exit code below — replaces both the
|
|
1359
1522
|
// scraped warning prose and the two hand-parsed English sentences.
|
|
1360
|
-
const result = computeSliceReadyGates(model, refs, dirname(file), key);
|
|
1361
1523
|
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null) + "\n");
|
|
1362
1524
|
}
|
|
1363
1525
|
else {
|
|
@@ -1719,7 +1881,9 @@ program
|
|
|
1719
1881
|
const emVersion = compiled
|
|
1720
1882
|
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
1721
1883
|
.filter((e) => e !== null);
|
|
1722
|
-
|
|
1884
|
+
// MIL-239 (R11): the nearest system.yaml above the first input file, consumer-adaptation counted.
|
|
1885
|
+
const system = statusSystemBlock(files[0], realGit, (m) => console.error(`warn: em status: ${m}`));
|
|
1886
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files));
|
|
1723
1887
|
let output;
|
|
1724
1888
|
if (opts.json)
|
|
1725
1889
|
output = buildStatusJson(report);
|
|
@@ -1951,37 +2115,180 @@ query
|
|
|
1951
2115
|
.action((files, opts) => {
|
|
1952
2116
|
runQueryVerb(files, "path", { from: opts.from, to: opts.to }, opts.json, (s) => queryPath(s, opts.from, opts.to), formatPath);
|
|
1953
2117
|
});
|
|
1954
|
-
program
|
|
2118
|
+
const engagement = program
|
|
2119
|
+
.command("engagement")
|
|
2120
|
+
.description("a named chunk of the model built as one stack of slice PRs: its levelled build plan and its " +
|
|
2121
|
+
"in-flight Ledger (MIL-268, see docs/cli.md and docs/engagement-schema.md)");
|
|
2122
|
+
engagement
|
|
2123
|
+
.command("new")
|
|
2124
|
+
.description("write <model dir>/engagements/<slug>.md — frontmatter (slices, parallel ceiling) plus the " +
|
|
2125
|
+
"generated Ledger table; selection by --slices, --context, or --downstream-of (exactly one)")
|
|
2126
|
+
.argument("<file>", "input .em file")
|
|
2127
|
+
.argument("<slug>", "engagement slug (kebab-case; the file name)")
|
|
2128
|
+
.option("--slices <keys>", "comma-separated slice export keys")
|
|
2129
|
+
.option("--context <name>", "every slice with an event in this context")
|
|
2130
|
+
.option("--downstream-of <ref>", "a slice key, element ref, or display name: it and everything downstream of it (loops-to excluded)")
|
|
2131
|
+
.option("--parallel <n>", "how many slices of one level may be in flight at once", "3")
|
|
2132
|
+
.option("--by <name>", "who created the engagement (createdBy)")
|
|
2133
|
+
.option("--force", "overwrite an existing engagement file with this slug")
|
|
2134
|
+
.action((file, slug, opts) => {
|
|
2135
|
+
const fail = (m) => {
|
|
2136
|
+
console.error(`em engagement new: ${m}`);
|
|
2137
|
+
process.exit(1);
|
|
2138
|
+
};
|
|
2139
|
+
const given = [opts.slices, opts.context, opts.downstreamOf].filter((v) => v !== undefined).length;
|
|
2140
|
+
if (given !== 1)
|
|
2141
|
+
fail("pass exactly one of --slices, --context, --downstream-of");
|
|
2142
|
+
const selector = opts.slices !== undefined
|
|
2143
|
+
? { kind: "slices", keys: opts.slices.split(",").map((k) => k.trim()).filter((k) => k !== "") }
|
|
2144
|
+
: opts.context !== undefined
|
|
2145
|
+
? { kind: "context", context: opts.context }
|
|
2146
|
+
: { kind: "downstream-of", ref: opts.downstreamOf };
|
|
2147
|
+
const parallel = /^\d+$/.test(opts.parallel) ? Number(opts.parallel) : NaN;
|
|
2148
|
+
const { model, refs } = compileFile(file);
|
|
2149
|
+
const path = engagementPath(file, slug);
|
|
2150
|
+
const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, created: localIsoDate() });
|
|
2151
|
+
if (!result.ok)
|
|
2152
|
+
return fail(result.message);
|
|
2153
|
+
if (existsSync(path) && !opts.force)
|
|
2154
|
+
fail(`engagement "${slug}" already exists (${path}) — pass --force to overwrite`);
|
|
2155
|
+
if (result.warning)
|
|
2156
|
+
console.error(result.warning);
|
|
2157
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2158
|
+
writeFileSync(path, result.text);
|
|
2159
|
+
console.log(`wrote ${path} (${result.units.length} slice${result.units.length === 1 ? "" : "s"}: ${result.units.join(", ")})`);
|
|
2160
|
+
});
|
|
2161
|
+
/** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
|
|
2162
|
+
* each slice's `--slice-ready` verdict (R34). */
|
|
2163
|
+
function engagementModelInput(file) {
|
|
2164
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2165
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
2166
|
+
return { file, model, refs, baseDir: dirname(file), contract: { file, source, diagnostics }, allDiagnostics };
|
|
2167
|
+
}
|
|
2168
|
+
engagement
|
|
2169
|
+
.command("plan")
|
|
2170
|
+
.description("the engagement's build plan: dependency levels over model.edges (loops-to excluded), and per " +
|
|
2171
|
+
"slice its readiness, branch impl/<key>, base (main or impl/<upstream>), and any hold")
|
|
2172
|
+
.argument("<file>", "input .em file")
|
|
2173
|
+
.argument("<slug>", "engagement slug")
|
|
2174
|
+
.option("--json", "print a JSON document instead of text (same document as the MCP engagement_plan tool)")
|
|
2175
|
+
.action((file, slug, opts) => {
|
|
2176
|
+
const result = engagementPlanFor(engagementModelInput(file), slug);
|
|
2177
|
+
if (!result.ok) {
|
|
2178
|
+
console.error(`em engagement plan: ${result.message}`);
|
|
2179
|
+
process.exit(1);
|
|
2180
|
+
}
|
|
2181
|
+
process.stdout.write((opts.json ? buildEngagementPlanJson(result.plan) : formatEngagementPlanText(result.plan)) + "\n");
|
|
2182
|
+
});
|
|
2183
|
+
engagement
|
|
2184
|
+
.command("set")
|
|
2185
|
+
.description("the only write path to the Ledger: set one slice's state (and optionally branch/base/pr); " +
|
|
2186
|
+
"idempotent; merged is terminal; --state held records a human hold")
|
|
2187
|
+
.argument("<file>", "input .em file")
|
|
2188
|
+
.argument("<slug>", "engagement slug")
|
|
2189
|
+
.argument("<key>", "slice export key")
|
|
2190
|
+
.requiredOption("--state <state>", `one of: ${LEDGER_STATES.join(" | ")}`)
|
|
2191
|
+
.option("--branch <name>", "the slice's branch")
|
|
2192
|
+
.option("--base <name>", "the branch the slice's PR targets")
|
|
2193
|
+
.option("--pr <url>", "the slice's PR URL")
|
|
2194
|
+
.action((file, slug, key, opts) => {
|
|
2195
|
+
const fail = (m) => {
|
|
2196
|
+
console.error(`em engagement set: ${m}`);
|
|
2197
|
+
process.exit(1);
|
|
2198
|
+
};
|
|
2199
|
+
const loaded = loadEngagement(file, slug);
|
|
2200
|
+
if (!loaded.ok)
|
|
2201
|
+
return fail(loaded.message);
|
|
2202
|
+
const { model, refs } = compileFile(file);
|
|
2203
|
+
const result = applyEngagementSet({ file, model, refs, baseDir: dirname(file) }, slug, loaded.text, loaded.file, { key, ...opts });
|
|
2204
|
+
if (!result.ok)
|
|
2205
|
+
return fail(result.message);
|
|
2206
|
+
if (!result.changed) {
|
|
2207
|
+
console.log(`no change: ${key} is already ${result.entry.state} (${loaded.path})`);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
writeFileSync(loaded.path, result.text);
|
|
2211
|
+
console.log(`set ${key}: ${result.entry.state} (${loaded.path})`);
|
|
2212
|
+
});
|
|
2213
|
+
engagement
|
|
2214
|
+
.command("status")
|
|
2215
|
+
.description("the Ledger joined with each slice doc's current status, plus whether the engagement is closable")
|
|
2216
|
+
.argument("<file>", "input .em file")
|
|
2217
|
+
.argument("<slug>", "engagement slug")
|
|
2218
|
+
.option("--json", "print a JSON document instead of text (same document as the MCP engagement_status tool)")
|
|
2219
|
+
.action((file, slug, opts) => {
|
|
2220
|
+
const { model, refs } = compileFile(file);
|
|
2221
|
+
const result = engagementStatusFor({ file, model, refs, baseDir: dirname(file) }, slug);
|
|
2222
|
+
if (!result.ok) {
|
|
2223
|
+
console.error(`em engagement status: ${result.message}`);
|
|
2224
|
+
process.exit(1);
|
|
2225
|
+
}
|
|
2226
|
+
process.stdout.write((opts.json ? buildEngagementStatusJson(result.status) : formatEngagementStatusText(result.status)) + "\n");
|
|
2227
|
+
});
|
|
2228
|
+
engagement
|
|
2229
|
+
.command("close")
|
|
2230
|
+
.description("set status: closed — refuses unless every slice is merged (recorded or inferred) or gap")
|
|
2231
|
+
.argument("<file>", "input .em file")
|
|
2232
|
+
.argument("<slug>", "engagement slug")
|
|
2233
|
+
.action((file, slug) => {
|
|
2234
|
+
const fail = (m) => {
|
|
2235
|
+
console.error(`em engagement close: ${m}`);
|
|
2236
|
+
process.exit(1);
|
|
2237
|
+
};
|
|
2238
|
+
const { model, refs } = compileFile(file);
|
|
2239
|
+
const status = engagementStatusFor({ file, model, refs, baseDir: dirname(file) }, slug);
|
|
2240
|
+
if (!status.ok)
|
|
2241
|
+
return fail(status.message);
|
|
2242
|
+
const loaded = loadEngagement(file, slug);
|
|
2243
|
+
if (!loaded.ok)
|
|
2244
|
+
return fail(loaded.message);
|
|
2245
|
+
const result = applyEngagementClose(slug, loaded.text, status.status);
|
|
2246
|
+
if (!result.ok)
|
|
2247
|
+
return fail(result.message);
|
|
2248
|
+
if (!result.changed) {
|
|
2249
|
+
console.log(`no change: engagement "${slug}" is already closed (${loaded.path})`);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
writeFileSync(loaded.path, result.text);
|
|
2253
|
+
console.log(`closed engagement "${slug}" (${loaded.path})`);
|
|
2254
|
+
});
|
|
2255
|
+
const systemCommand = program
|
|
1955
2256
|
.command("system")
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
.
|
|
2257
|
+
// `--json` exists on both `em system` and `em system codeowners`: parent options are only
|
|
2258
|
+
// recognized before the subcommand name, so the subcommand owns the one that follows it.
|
|
2259
|
+
.enablePositionalOptions()
|
|
2260
|
+
.description("verify a system — every model's `consumes` refs resolved against the other models' `public` " +
|
|
2261
|
+
"events/views, the cross-model half of \"both ends of a flow\" (MIL-194/MIL-235, see docs/cli.md). " +
|
|
2262
|
+
"Reads system.yaml when given (or found in the directory), else discovers every *.em in the repo")
|
|
2263
|
+
.argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
|
|
1959
2264
|
// Same reasoning as `em validate`'s allowExcessArguments(false) (MIL-123): a second positional
|
|
1960
|
-
// would be silently dropped, and "one
|
|
2265
|
+
// would be silently dropped, and "one system per run" is the whole contract here.
|
|
1961
2266
|
.allowExcessArguments(false)
|
|
1962
2267
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1963
|
-
.action((
|
|
1964
|
-
const
|
|
2268
|
+
.action((target, opts) => {
|
|
2269
|
+
const shown = target ?? ".";
|
|
2270
|
+
const loaded = loadSystem(shown);
|
|
1965
2271
|
if (!loaded.ok) {
|
|
1966
2272
|
// Same refusal posture as `em export`/`em status`: a manifest that can't be parsed, or a
|
|
1967
2273
|
// source that can't be read/compiled, is not a system to verify — no document, exit 1.
|
|
1968
2274
|
printSystemDiagnostics(loaded.diagnostics);
|
|
1969
|
-
console.error(`em system: not verifying — ${
|
|
2275
|
+
console.error(`em system: not verifying — ${shown} could not be loaded; fix the above first`);
|
|
1970
2276
|
process.exit(1);
|
|
1971
2277
|
}
|
|
1972
|
-
const report = verifySystem(loaded.manifest, loaded.models,
|
|
2278
|
+
const report = verifySystem(loaded.manifest, loaded.models, loaded.manifestPath, loaded.diagnostics, { producerCommit: gitProducerCommit() });
|
|
1973
2279
|
if (opts.json) {
|
|
1974
2280
|
// `em validate --json`'s convention: diagnostics still go to stderr in the human format,
|
|
1975
2281
|
// the document (which carries the same diagnostics) to stdout; exit code unchanged.
|
|
1976
2282
|
printSystemDiagnostics(report.diagnostics);
|
|
1977
|
-
process.stdout.write(buildSystemJson(
|
|
2283
|
+
process.stdout.write(buildSystemJson(loaded, report) + "\n");
|
|
1978
2284
|
}
|
|
1979
2285
|
else {
|
|
1980
2286
|
const verified = report.seams.filter((s) => s.status === "verified").length;
|
|
1981
2287
|
const failing = report.seams.length - verified;
|
|
1982
2288
|
const label = report.name === null ? "system" : `system "${report.name}"`;
|
|
2289
|
+
const via = loaded.discovery ? ` (discovered under ${loaded.discovery.root} — no ${SYSTEM_MANIFEST_FILE})` : "";
|
|
1983
2290
|
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)`);
|
|
2291
|
+
`${report.seams.length} seam${report.seams.length === 1 ? "" : "s"} (${verified} verified, ${failing} failing)${via}`);
|
|
1985
2292
|
printSystemDiagnostics(report.diagnostics);
|
|
1986
2293
|
if (report.diagnostics.length === 0)
|
|
1987
2294
|
console.log("ok — no issues");
|
|
@@ -1991,6 +2298,96 @@ program
|
|
|
1991
2298
|
if (hasErrors(report.diagnostics))
|
|
1992
2299
|
process.exitCode = 1;
|
|
1993
2300
|
});
|
|
2301
|
+
systemCommand
|
|
2302
|
+
.command("codeowners")
|
|
2303
|
+
.description("generate (or --check) the managed CODEOWNERS block that routes review: each model's team on its " +
|
|
2304
|
+
"design directory, and every team that `consumes` a model's public surface on that model's " +
|
|
2305
|
+
"contract file (MIL-234, see docs/ci.md and docs/cli.md)")
|
|
2306
|
+
.argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
|
|
2307
|
+
.allowExcessArguments(false)
|
|
2308
|
+
.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)")
|
|
2309
|
+
.option("--check", "verify the committed file already carries the generated block; never writes, exit 1 on drift (CI)")
|
|
2310
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
2311
|
+
.action((target, opts) => {
|
|
2312
|
+
const shown = target ?? ".";
|
|
2313
|
+
const result = runCodeowners(shown, { output: opts.output, write: !opts.check });
|
|
2314
|
+
if (!result.ok) {
|
|
2315
|
+
console.error(`em system codeowners: ${result.message}`);
|
|
2316
|
+
process.exitCode = 1;
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
for (const note of result.plan.notes)
|
|
2320
|
+
console.error(`note: ${note}`);
|
|
2321
|
+
if (opts.json) {
|
|
2322
|
+
process.stdout.write(buildCodeownersJson(result.file, result.plan.entries, result.status) + "\n");
|
|
2323
|
+
}
|
|
2324
|
+
else if (opts.check) {
|
|
2325
|
+
const hint = `run \`em system codeowners${target ? ` ${target}` : ""}${opts.output ? ` -o ${opts.output}` : ""}\` and commit the result`;
|
|
2326
|
+
if (result.status === "ok")
|
|
2327
|
+
console.log(`ok: ${result.file} carries the generated block`);
|
|
2328
|
+
else if (result.status === "missing")
|
|
2329
|
+
console.log(`missing: ${result.file} does not exist — ${hint}`);
|
|
2330
|
+
else if (result.status === "no-markers")
|
|
2331
|
+
console.log(`no-markers: ${result.file} has no GENERATED:em-codeowners block — ${hint}`);
|
|
2332
|
+
else
|
|
2333
|
+
console.log(`stale: ${result.file} differs from the generated block — ${hint}`);
|
|
2334
|
+
}
|
|
2335
|
+
else {
|
|
2336
|
+
const verb = result.status === "created" ? "installed" : result.status === "updated" ? "updated" : "already up to date:";
|
|
2337
|
+
console.log(`${verb} ${result.file} (${result.plan.entries.length} entr${result.plan.entries.length === 1 ? "y" : "ies"})`);
|
|
2338
|
+
}
|
|
2339
|
+
// --check never writes and fails on anything but ok (same vocabulary as `em ci init --check`).
|
|
2340
|
+
if (opts.check && result.status !== "ok")
|
|
2341
|
+
process.exitCode = 1;
|
|
2342
|
+
});
|
|
2343
|
+
const scopeCommand = systemCommand
|
|
2344
|
+
.command("scope")
|
|
2345
|
+
.description("check a change set against the seams (MIL-240): fail only when it alters a producer's public surface " +
|
|
2346
|
+
"(or contract file) AND a consuming model's design dir together (`seam-crossing`); warn on other " +
|
|
2347
|
+
"multi-model change sets. Files changed only by `Em-Upgrade:` commits are exempt. There is no " +
|
|
2348
|
+
"override - review on the contract file is the gate (see docs/cli.md, docs/ci.md)")
|
|
2349
|
+
.argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
|
|
2350
|
+
.allowExcessArguments(false)
|
|
2351
|
+
.option("--base <rev>", "the change set committed since <rev> (merge-base form, <rev>...HEAD); CI passes the PR base")
|
|
2352
|
+
.option("--staged", "also (or only) check what is staged right now; never exempted")
|
|
2353
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
2354
|
+
.action((target, opts) => {
|
|
2355
|
+
const shown = target ?? ".";
|
|
2356
|
+
const run = runSystemScope(shown, { base: opts.base, staged: opts.staged });
|
|
2357
|
+
if (!run.ok) {
|
|
2358
|
+
printSystemDiagnostics(run.diagnostics);
|
|
2359
|
+
console.error(run.message);
|
|
2360
|
+
process.exit(1);
|
|
2361
|
+
}
|
|
2362
|
+
const { facts, report } = run;
|
|
2363
|
+
// `--json` is also an option of the parent `system` command, which swallows it before this
|
|
2364
|
+
// subcommand sees it: read the merged options.
|
|
2365
|
+
const json = scopeCommand.optsWithGlobals().json === true;
|
|
2366
|
+
const printFindings = () => {
|
|
2367
|
+
for (const d of report.diagnostics) {
|
|
2368
|
+
const line = `${d.file}: ${d.severity} [${d.code}] ${d.message}`;
|
|
2369
|
+
if (d.severity === "error")
|
|
2370
|
+
console.error(line);
|
|
2371
|
+
else
|
|
2372
|
+
console.warn(line);
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
if (json) {
|
|
2376
|
+
printFindings();
|
|
2377
|
+
process.stdout.write(buildScopeJson(facts, report) + "\n");
|
|
2378
|
+
}
|
|
2379
|
+
else {
|
|
2380
|
+
const range = [facts.base !== null ? `since ${facts.base}` : null, facts.staged ? "staged" : null].filter(Boolean).join(" + ");
|
|
2381
|
+
console.log(`scope ${range}: ${facts.changedPaths.length} changed path${facts.changedPaths.length === 1 ? "" : "s"}, ` +
|
|
2382
|
+
`${report.touched.length} model${report.touched.length === 1 ? "" : "s"} touched` +
|
|
2383
|
+
(facts.exemptPaths.length > 0 ? `, ${facts.exemptPaths.length} exempt (Em-Upgrade)` : ""));
|
|
2384
|
+
printFindings();
|
|
2385
|
+
if (report.diagnostics.length === 0)
|
|
2386
|
+
console.log("ok - no issues");
|
|
2387
|
+
}
|
|
2388
|
+
if (report.crossings > 0)
|
|
2389
|
+
process.exitCode = 1;
|
|
2390
|
+
});
|
|
1994
2391
|
// Shared by install/sync/check: the `.claude/skills/` root bundled with whatever em package is
|
|
1995
2392
|
// actually running (works whether em was installed from npm or run from a checkout, and
|
|
1996
2393
|
// regardless of a symlinked global install — see pkgDir's own resolution above for
|
|
@@ -2005,6 +2402,14 @@ function packagedSkillsRoot() {
|
|
|
2005
2402
|
function vendoredSkillsRoot(repoRoot) {
|
|
2006
2403
|
return join(resolve(repoRoot), ".claude", "skills");
|
|
2007
2404
|
}
|
|
2405
|
+
// MIL-269 (R32): the four em sub-agent definitions live in `.claude/agents/`, a directory shared
|
|
2406
|
+
// with the consumer's own agents - install/sync/check only ever handle the fixed EM_AGENT_FILES.
|
|
2407
|
+
function packagedAgentsRoot() {
|
|
2408
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "agents");
|
|
2409
|
+
}
|
|
2410
|
+
function vendoredAgentsRoot(repoRoot) {
|
|
2411
|
+
return join(resolve(repoRoot), ".claude", "agents");
|
|
2412
|
+
}
|
|
2008
2413
|
program
|
|
2009
2414
|
.command("contract")
|
|
2010
2415
|
.description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
|
|
@@ -2036,9 +2441,27 @@ function agentsMdMessage(result) {
|
|
|
2036
2441
|
return `updated the agent-contract section in ${result.path}`;
|
|
2037
2442
|
return `${result.path} already has an up-to-date agent-contract section`;
|
|
2038
2443
|
}
|
|
2444
|
+
/** MIL-231: `em skill install`/`sync` keep working but the vendored bundle is deprecated in favour of
|
|
2445
|
+
* the em plugin. One stderr line naming the two-command install; the exit code is untouched. */
|
|
2446
|
+
function warnVendoredDeprecated() {
|
|
2447
|
+
console.error(`warn: the vendored skill bundle is deprecated since em 1.14 — install the em plugin instead: ${pluginInstallCommands(PKG_VERSION).join(" && ")}`);
|
|
2448
|
+
}
|
|
2039
2449
|
const skill = program
|
|
2040
2450
|
.command("skill")
|
|
2041
2451
|
.description("manage Claude Code skills bundled with em");
|
|
2452
|
+
// MIL-269: reconcile the four `.claude/agents/em-*.md` files (create the directory if absent);
|
|
2453
|
+
// byte-exact restore, never touches any other file there.
|
|
2454
|
+
function installAgentFiles(repoRoot) {
|
|
2455
|
+
const packaged = packagedAgentsRoot();
|
|
2456
|
+
const vendored = vendoredAgentsRoot(repoRoot);
|
|
2457
|
+
const plan = planAgentSync(packaged, vendored, EM_AGENT_FILES);
|
|
2458
|
+
if (plan.changes.length === 0)
|
|
2459
|
+
return;
|
|
2460
|
+
applySkillSync(plan, packaged, vendored);
|
|
2461
|
+
for (const c of plan.changes)
|
|
2462
|
+
console.log(`${c.kind}: agents/${c.relPath}`);
|
|
2463
|
+
console.log(`installed em sub-agent definitions → ${vendored} (${plan.changes.length} file(s))`);
|
|
2464
|
+
}
|
|
2042
2465
|
skill
|
|
2043
2466
|
.command("install")
|
|
2044
2467
|
.description("copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-" +
|
|
@@ -2051,6 +2474,7 @@ skill
|
|
|
2051
2474
|
"unrelated sibling skill (MIL-180)")
|
|
2052
2475
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
2053
2476
|
.action(async (opts) => {
|
|
2477
|
+
warnVendoredDeprecated();
|
|
2054
2478
|
const srcRoot = packagedSkillsRoot();
|
|
2055
2479
|
const destRoot = vendoredSkillsRoot(process.cwd());
|
|
2056
2480
|
const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
|
|
@@ -2082,6 +2506,7 @@ skill
|
|
|
2082
2506
|
`(${removedCount} removed), ${totalUnchanged} unchanged`);
|
|
2083
2507
|
}
|
|
2084
2508
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
2509
|
+
installAgentFiles(process.cwd());
|
|
2085
2510
|
}
|
|
2086
2511
|
else {
|
|
2087
2512
|
await mkdir(destRoot, { recursive: true });
|
|
@@ -2090,6 +2515,7 @@ skill
|
|
|
2090
2515
|
}
|
|
2091
2516
|
console.log(`installed event-modeling skill bundle → ${destRoot}`);
|
|
2092
2517
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
2518
|
+
installAgentFiles(process.cwd());
|
|
2093
2519
|
}
|
|
2094
2520
|
if (opts.agentsMd !== false) {
|
|
2095
2521
|
console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
|
|
@@ -2102,20 +2528,29 @@ skill
|
|
|
2102
2528
|
.argument("[path]", "consumer repo root", ".")
|
|
2103
2529
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
2104
2530
|
.action(async (path, opts) => {
|
|
2531
|
+
warnVendoredDeprecated();
|
|
2105
2532
|
const packagedRoot = packagedSkillsRoot();
|
|
2106
2533
|
const vendoredRoot = vendoredSkillsRoot(path);
|
|
2107
2534
|
const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2535
|
+
const skillChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
|
|
2536
|
+
// MIL-269 (R32): the fixed agent-file list, never the shared .claude/agents/ directory as a whole.
|
|
2537
|
+
const packagedAgents = packagedAgentsRoot();
|
|
2538
|
+
const vendoredAgents = vendoredAgentsRoot(path);
|
|
2539
|
+
const agentPlan = planAgentSync(packagedAgents, vendoredAgents, EM_AGENT_FILES);
|
|
2540
|
+
const totalChanges = skillChanges + agentPlan.changes.length;
|
|
2541
|
+
const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0) + agentPlan.unchangedCount;
|
|
2110
2542
|
if (totalChanges === 0) {
|
|
2111
2543
|
console.log(`up to date — ${vendoredRoot} already matches the installed skill bundle (${totalUnchanged} file(s))`);
|
|
2112
2544
|
}
|
|
2113
2545
|
else {
|
|
2114
2546
|
applySkillSyncBundle(bundlePlan, packagedRoot, vendoredRoot);
|
|
2547
|
+
applySkillSync(agentPlan, packagedAgents, vendoredAgents);
|
|
2115
2548
|
for (const { dirName, plan } of bundlePlan) {
|
|
2116
2549
|
for (const c of plan.changes)
|
|
2117
2550
|
console.log(`${c.kind}: ${dirName}/${c.relPath}`);
|
|
2118
2551
|
}
|
|
2552
|
+
for (const c of agentPlan.changes)
|
|
2553
|
+
console.log(`${c.kind}: agents/${c.relPath}`);
|
|
2119
2554
|
console.log(`synced ${vendoredRoot} — ${totalChanges} file(s) changed, ${totalUnchanged} unchanged`);
|
|
2120
2555
|
}
|
|
2121
2556
|
if (opts.agentsMd !== false) {
|
|
@@ -2124,21 +2559,55 @@ skill
|
|
|
2124
2559
|
});
|
|
2125
2560
|
skill
|
|
2126
2561
|
.command("check")
|
|
2127
|
-
.description("check
|
|
2128
|
-
"
|
|
2562
|
+
.description("check [path]'s em skills for drift against the installed em package: the em Claude Code plugin's " +
|
|
2563
|
+
"pin (settings + this machine's marketplace registry) when the repo declares it, and the vendored " +
|
|
2564
|
+
".claude/skills/ bundle when present; exits non-zero on any mismatch (CI-ready, MIL-93, MIL-231)")
|
|
2129
2565
|
.argument("[path]", "consumer repo root", ".")
|
|
2130
2566
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
2567
|
+
.option("--ci", "also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); " +
|
|
2568
|
+
"without it that finding prints the install commands on stderr and exits 0")
|
|
2131
2569
|
.action((path, opts) => {
|
|
2132
2570
|
const packagedRoot = packagedSkillsRoot();
|
|
2133
2571
|
const vendoredRoot = vendoredSkillsRoot(path);
|
|
2134
|
-
const
|
|
2572
|
+
const ciMode = !!opts.ci || process.env.CI === "true";
|
|
2573
|
+
// MIL-231: the plugin signal (a pinned marketplace in .claude/settings*.json) sits beside the
|
|
2574
|
+
// vendored signal (the anchor directory). Plugin only -> only the plugin is checked; vendored
|
|
2575
|
+
// only -> exactly the pre-1.14 behavior; both -> both; neither -> skill-check-not-installed.
|
|
2576
|
+
const pluginCheck = checkPlugin(resolve(path), PKG_VERSION, claudePluginsDir());
|
|
2577
|
+
const vendoredPresent = existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR));
|
|
2578
|
+
const vendored = pluginCheck && !vendoredPresent
|
|
2579
|
+
? { findings: [], ok: true }
|
|
2580
|
+
: checkSkillSyncBundle(packagedRoot, vendoredRoot, PKG_VERSION, EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES);
|
|
2581
|
+
// MIL-269 (R32): the four agent files, only when the vendored bundle is present (a plugin-only
|
|
2582
|
+
// repo ships them via the plugin); checked by fixed name, never the shared directory.
|
|
2583
|
+
const agents = vendoredPresent
|
|
2584
|
+
? checkAgentFiles(packagedAgentsRoot(), vendoredAgentsRoot(path), EM_AGENT_FILES)
|
|
2585
|
+
: { findings: [], ok: true };
|
|
2586
|
+
const findings = [...vendored.findings, ...agents.findings, ...(pluginCheck?.findings ?? [])];
|
|
2587
|
+
const failing = findings.filter((f) => f.code !== "plugin-not-installed-locally" || ciMode);
|
|
2588
|
+
const ok = failing.length === 0;
|
|
2589
|
+
const result = { findings, ok };
|
|
2135
2590
|
if (opts.json) {
|
|
2136
|
-
process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION) + "\n");
|
|
2591
|
+
process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION, pluginCheck?.plugin ?? null) + "\n");
|
|
2137
2592
|
}
|
|
2138
2593
|
else {
|
|
2139
|
-
for (const f of
|
|
2140
|
-
|
|
2141
|
-
|
|
2594
|
+
for (const f of findings) {
|
|
2595
|
+
if (f.code === "plugin-not-installed-locally" && !ciMode)
|
|
2596
|
+
console.error(f.message);
|
|
2597
|
+
else
|
|
2598
|
+
console.log(f.message);
|
|
2599
|
+
}
|
|
2600
|
+
if (!ok)
|
|
2601
|
+
console.log(`${failing.length} mismatch(es)`);
|
|
2602
|
+
else {
|
|
2603
|
+
if (!pluginCheck || vendoredPresent)
|
|
2604
|
+
console.log(`ok — vendored skill matches em ${PKG_VERSION}`);
|
|
2605
|
+
if (pluginCheck) {
|
|
2606
|
+
const unregistered = findings.some((f) => f.code === "plugin-not-installed-locally");
|
|
2607
|
+
console.log(`ok — em plugin ${pluginCheck.plugin.name} (${pluginCheck.plugin.ref}) matches em ${PKG_VERSION}` +
|
|
2608
|
+
(unregistered ? " (not registered on this machine - see the install commands on stderr)" : ""));
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2142
2611
|
}
|
|
2143
2612
|
// MIL-219 ruling A: `em skill check` also warns when [path]'s own state file records an
|
|
2144
2613
|
// `Em version:` behind the installed em — best-effort (no state file, or an unparseable
|
|
@@ -2154,7 +2623,7 @@ skill
|
|
|
2154
2623
|
}
|
|
2155
2624
|
// Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
|
|
2156
2625
|
// a pipe (a --json document) shouldn't risk truncation.
|
|
2157
|
-
if (!
|
|
2626
|
+
if (!ok)
|
|
2158
2627
|
process.exitCode = 1;
|
|
2159
2628
|
});
|
|
2160
2629
|
const ci = program.command("ci").description("scaffold/check the CI enforcement preset (MIL-166, see docs/ci.md)");
|
|
@@ -2163,10 +2632,12 @@ ci.command("init")
|
|
|
2163
2632
|
"coverage --strict, em ledger, em skill check, em glossary --fail-on-conflicts, plus a " +
|
|
2164
2633
|
"push-triggered status-badge rebuild) and em-conform.yml (scheduled, advisory-only " +
|
|
2165
2634
|
"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
|
-
.
|
|
2635
|
+
"idempotent, --check for CI self-verification (MIL-166, see docs/ci.md). Give it a " +
|
|
2636
|
+
"system.yaml (or its directory) instead of a model to cover every model in the system: one " +
|
|
2637
|
+
"workflow, per-model jobs, a conform matrix (MIL-233)")
|
|
2638
|
+
.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
2639
|
.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
|
|
2640
|
+
.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
2641
|
.option("--check", "verify both files match the current preset; exit non-zero on drift without writing (CI)")
|
|
2171
2642
|
.action((model, opts) => {
|
|
2172
2643
|
const unsafe = findUnsafeCiInitArg(model) ?? findUnsafeCiInitArg(opts.tests);
|
|
@@ -2178,24 +2649,52 @@ ci.command("init")
|
|
|
2178
2649
|
const repoRoot = process.cwd();
|
|
2179
2650
|
const ciPath = ciWorkflowPath(repoRoot);
|
|
2180
2651
|
const conformPath = conformWorkflowPath(repoRoot);
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2652
|
+
// MIL-231: a repo that declares the em plugin gets a conform workflow that installs the plugin
|
|
2653
|
+
// and runs /em:conform; any other repo keeps the vendored install lines.
|
|
2654
|
+
const usePlugin = detectPlugin(repoRoot) !== null;
|
|
2655
|
+
// MIL-233: a system manifest (a .yaml/.yml file, or a directory) covers every model in it;
|
|
2656
|
+
// anything else is the single-model form, unchanged.
|
|
2657
|
+
const isDir = existsSync(model) && statSync(model).isDirectory();
|
|
2658
|
+
const manifestForm = isDir || /\.ya?ml$/i.test(model);
|
|
2659
|
+
let files;
|
|
2660
|
+
if (manifestForm) {
|
|
2661
|
+
const loaded = ciModelsFromManifest(model, repoRoot);
|
|
2662
|
+
if (!loaded.ok) {
|
|
2663
|
+
console.error(`em ci init: ${loaded.message}`);
|
|
2664
|
+
process.exit(1);
|
|
2665
|
+
}
|
|
2666
|
+
files = [
|
|
2667
|
+
[
|
|
2668
|
+
ciPath,
|
|
2669
|
+
planCiFile(ciPath, buildCiWorkflowFileMulti(model, loaded.models, opts.tests, PKG_VERSION), ciManagedBodyMulti(loaded.models, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force, true),
|
|
2670
|
+
],
|
|
2671
|
+
[
|
|
2672
|
+
conformPath,
|
|
2673
|
+
planCiFile(conformPath, buildConformWorkflowFileMulti(model, loaded.models, PKG_VERSION, usePlugin), conformManagedBodyMulti(loaded.models, PKG_VERSION, usePlugin), CONFORM_WORKFLOW_MARKER, !!opts.force, true),
|
|
2674
|
+
],
|
|
2675
|
+
];
|
|
2676
|
+
}
|
|
2677
|
+
else {
|
|
2678
|
+
files = [
|
|
2679
|
+
[
|
|
2680
|
+
ciPath,
|
|
2681
|
+
planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
|
|
2682
|
+
],
|
|
2683
|
+
[
|
|
2684
|
+
conformPath,
|
|
2685
|
+
planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION, usePlugin), conformManagedBody(model, PKG_VERSION, usePlugin), CONFORM_WORKFLOW_MARKER, !!opts.force),
|
|
2686
|
+
],
|
|
2687
|
+
];
|
|
2688
|
+
}
|
|
2689
|
+
// MIL-256 (#174) / MIL-233: a managed block is identified by the SET of models it names. A
|
|
2690
|
+
// disjoint set - or a single-model argument against a multi-model block - is its own
|
|
2691
|
+
// condition: never `stale`, never silently replaced.
|
|
2692
|
+
const otherModels = (status, path) => describeOtherModels(path, status);
|
|
2194
2693
|
if (opts.check) {
|
|
2195
2694
|
let drift = false;
|
|
2196
2695
|
for (const [path, status] of files) {
|
|
2197
|
-
if (status.kind === "other-
|
|
2198
|
-
console.log(`different
|
|
2696
|
+
if (status.kind === "other-models" || status.kind === "replace-models") {
|
|
2697
|
+
console.log(`different models: ${otherModels(status, path)}`);
|
|
2199
2698
|
drift = true;
|
|
2200
2699
|
}
|
|
2201
2700
|
else if (status.kind === "create") {
|
|
@@ -2207,7 +2706,11 @@ ci.command("init")
|
|
|
2207
2706
|
drift = true;
|
|
2208
2707
|
}
|
|
2209
2708
|
else if (status.kind === "stale") {
|
|
2210
|
-
|
|
2709
|
+
const marker = path === ciPath ? CI_WORKFLOW_MARKER : CONFORM_WORKFLOW_MARKER;
|
|
2710
|
+
const change = manifestForm
|
|
2711
|
+
? describeSetChange(fileBlockModels(status.current, marker), fileBlockModels(status.content, marker))
|
|
2712
|
+
: null;
|
|
2713
|
+
console.log(`stale: ${path} — run \`em ci init ${model}\` and commit the result${change ? ` (${change})` : ""}`);
|
|
2211
2714
|
drift = true;
|
|
2212
2715
|
}
|
|
2213
2716
|
else {
|
|
@@ -2220,11 +2723,11 @@ ci.command("init")
|
|
|
2220
2723
|
return;
|
|
2221
2724
|
}
|
|
2222
2725
|
// Refuse before writing anything, so a refusal on one file never leaves the other updated.
|
|
2223
|
-
const refused = files.filter(([, s]) => s.kind === "other-
|
|
2726
|
+
const refused = files.filter(([, s]) => s.kind === "other-models");
|
|
2224
2727
|
if (refused.length > 0) {
|
|
2225
2728
|
for (const [path, status] of refused) {
|
|
2226
|
-
if (status.kind === "other-
|
|
2227
|
-
console.error(`em ci init: ${
|
|
2729
|
+
if (status.kind === "other-models")
|
|
2730
|
+
console.error(`em ci init: ${otherModels(status, path)}`);
|
|
2228
2731
|
}
|
|
2229
2732
|
process.exitCode = 1;
|
|
2230
2733
|
return;
|
|
@@ -2232,8 +2735,8 @@ ci.command("init")
|
|
|
2232
2735
|
mkdirSync(dirname(ciPath), { recursive: true });
|
|
2233
2736
|
for (const [path, status] of files) {
|
|
2234
2737
|
applyCiFile(path, status);
|
|
2235
|
-
if (status.kind === "replace-
|
|
2236
|
-
console.log(`replaced ${path} (--force; was generated for ${status.previous})`);
|
|
2738
|
+
if (status.kind === "replace-models")
|
|
2739
|
+
console.log(`replaced ${path} (--force; was generated for ${status.previous.join(", ")})`);
|
|
2237
2740
|
else if (status.kind === "create")
|
|
2238
2741
|
console.log(`installed ${path}`);
|
|
2239
2742
|
else if (status.kind === "stale")
|
|
@@ -2289,6 +2792,7 @@ program
|
|
|
2289
2792
|
repoRoot: repoRootResult.repoRoot,
|
|
2290
2793
|
installedVersion: PKG_VERSION,
|
|
2291
2794
|
packagedSkillsRoot: packagedSkillsRoot(),
|
|
2795
|
+
packagedAgentsRoot: packagedAgentsRoot(),
|
|
2292
2796
|
model,
|
|
2293
2797
|
refs,
|
|
2294
2798
|
};
|
|
@@ -2407,6 +2911,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
|
2407
2911
|
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
2408
2912
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
2409
2913
|
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
2914
|
+
...validateInvariants(model, refs, dirname(file)),
|
|
2410
2915
|
];
|
|
2411
2916
|
}
|
|
2412
2917
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
@@ -2512,7 +3017,7 @@ function printDivergences(model) {
|
|
|
2512
3017
|
* cross-model `public` view's reader legitimately lives outside this file (see
|
|
2513
3018
|
* `publicViewsWithoutInModelReader`'s own header for why this stays advisory). */
|
|
2514
3019
|
function printPublicElements(model) {
|
|
2515
|
-
const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
|
|
3020
|
+
const pub = model.elements.filter((el) => el.public && (el.kind === "command" || el.kind === "event" || el.kind === "view"));
|
|
2516
3021
|
if (pub.length === 0) {
|
|
2517
3022
|
console.log("no public elements");
|
|
2518
3023
|
return;
|