@smartmemory/compose 0.2.52-beta → 0.2.53-beta

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 (138) hide show
  1. package/.claude/skills/compose/SKILL.md +0 -8
  2. package/README.md +11 -0
  3. package/bin/compose.js +335 -3
  4. package/dist/assets/App-Bu9KMtTa.js +891 -0
  5. package/dist/assets/abnfDiagram-VRR7QNED-6zp3w9rx.js +1 -0
  6. package/dist/assets/arc-CnLxxuah.js +1 -0
  7. package/dist/assets/architectureDiagram-ZJ3FMSHR-COM46S9k.js +36 -0
  8. package/dist/assets/blockDiagram-677ZJIJ3-wKzgOwF8.js +132 -0
  9. package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
  10. package/dist/assets/{c4Diagram-AHTNJAMY-DtDSNlOX.js → c4Diagram-LMCZKHZV-DIXC_mR6.js} +1 -1
  11. package/dist/assets/channel-DQJPWkb_.js +1 -0
  12. package/dist/assets/{chunk-QZHKN3VN-BMWFLZ8t.js → chunk-2Q5K7J3B-BqLqZX6m.js} +1 -1
  13. package/dist/assets/{chunk-YZCP3GAM-CqjNXooj.js → chunk-32BRIVSS-CarSmVlX.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-BAa7Edp6.js → chunk-5VM5RSS4-D-f_Jn3G.js} +1 -1
  15. package/dist/assets/chunk-EX3LRPZG-DT42xo8E.js +231 -0
  16. package/dist/assets/{chunk-4BX2VUAB-oXU2npoL.js → chunk-JWPE2WC7-CwY1aZc4.js} +1 -1
  17. package/dist/assets/chunk-MOJQB5TN-DBbZU_KX.js +88 -0
  18. package/dist/assets/chunk-RYQCIY6F-DqxtcLJ3.js +1 -0
  19. package/dist/assets/chunk-V7JOEXUC-Cy4Ixww6.js +206 -0
  20. package/dist/assets/{chunk-EDXVE4YY-Bse2kdRh.js → chunk-VR4S4FIN-CFexfU_a.js} +1 -1
  21. package/dist/assets/{chunk-55IACEB6-BEEk2FdH.js → chunk-XXDRQBXY-uE-zN_vc.js} +1 -1
  22. package/dist/assets/classDiagram-OUVF2IWQ-CTLbAiUK.js +1 -0
  23. package/dist/assets/classDiagram-v2-EOCWNBFH-CTLbAiUK.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-NYnH3mVW.js → cose-bilkent-JH36ORCC-DIjpekos.js} +1 -1
  25. package/dist/assets/cynefin-VYW2F7L2-Ba2gbQow.js +178 -0
  26. package/dist/assets/cynefinDiagram-TSTJHNR4-njGzb7Tg.js +62 -0
  27. package/dist/assets/dagre-VKFMJZFB-DuyZREZ3.js +4 -0
  28. package/dist/assets/diagram-FQU43EPY-Npq-5o3a.js +3 -0
  29. package/dist/assets/diagram-G47NLZAW-fm4k3axC.js +24 -0
  30. package/dist/assets/diagram-NH7WQ7WH-B8EFGrTG.js +24 -0
  31. package/dist/assets/diagram-OA4YK3LP-BC7UHx9Q.js +30 -0
  32. package/dist/assets/diagram-WEI45ONY-BqjHLcCX.js +41 -0
  33. package/dist/assets/ebnfDiagram-CCIWWBDH-D5zXd4Qg.js +1 -0
  34. package/dist/assets/erDiagram-Q63AITRT-DC2FMcra.js +85 -0
  35. package/dist/assets/flowDiagram-23GEKE2U-D7Dx7JU4.js +156 -0
  36. package/dist/assets/ganttDiagram-NO4QXBWP-8sQH2y5K.js +292 -0
  37. package/dist/assets/gitGraphDiagram-IHSO6WYX-Ci_rhxur.js +106 -0
  38. package/dist/assets/graph-C9eacEi8.js +1 -0
  39. package/dist/assets/graph-xkel59g2.js +331 -0
  40. package/dist/assets/index-B7-HQenC.js +119 -0
  41. package/dist/assets/infoDiagram-FWYZ7A6U-B9FGQ0Cb.js +2 -0
  42. package/dist/assets/{ishikawaDiagram-UXIWVN3A-CK2IFFAP.js → ishikawaDiagram-FXEZZL3T-BEKLyH6A.js} +5 -5
  43. package/dist/assets/{journeyDiagram-VCZTEJTY-DAH5Stkf.js → journeyDiagram-5HDEW3XC-BM-IDVRo.js} +1 -1
  44. package/dist/assets/{kanban-definition-6JOO6SKY-Bi0aCqkv.js → kanban-definition-HUTT4EX6-Beb2k3pt.js} +7 -7
  45. package/dist/assets/katex-C5jXJg4s.js +257 -0
  46. package/dist/assets/layout-DEXfKzaS.js +1 -0
  47. package/dist/assets/{linear-ClGEGlS2.js → linear-Dwg7dTpz.js} +1 -1
  48. package/dist/assets/map-Czzmt4hB.js +1 -0
  49. package/dist/assets/{mindmap-definition-QFDTVHPH-Wjr-PgC6.js → mindmap-definition-LN4V7U3C-CeaynfcE.js} +7 -7
  50. package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
  51. package/dist/assets/pegDiagram-2B236MQR-7SVdAVqj.js +1 -0
  52. package/dist/assets/pieDiagram-ENE6RG2P-CzqQ4TaE.js +39 -0
  53. package/dist/assets/quadrantDiagram-ABIIQ3AL-B9B6W80N.js +7 -0
  54. package/dist/assets/railroadDiagram-RFXS5EU6-BRNLawsr.js +1 -0
  55. package/dist/assets/{requirementDiagram-MS252O5E-CU9Rlyq5.js → requirementDiagram-TGXJPOKE-CmASubeG.js} +3 -3
  56. package/dist/assets/sankeyDiagram-HTMAVEWB-DiReR0OB.js +40 -0
  57. package/dist/assets/sequenceDiagram-DBY2YBRQ-DVn5iZKR.js +162 -0
  58. package/dist/assets/sizeCapture-X5ZJPWSS-C8CuDOdp.js +1 -0
  59. package/dist/assets/stateDiagram-2N3HPSRC-BIdkHglY.js +1 -0
  60. package/dist/assets/stateDiagram-v2-6OUMAXLB-B9yje3Er.js +1 -0
  61. package/dist/assets/swimlanes-5IMT3BWC-BlVsYyyN.js +2 -0
  62. package/dist/assets/swimlanesDiagram-G3AALYLV-BTLpU45n.js +8 -0
  63. package/dist/assets/{timeline-definition-GMOUNBTQ-C8SZmaLp.js → timeline-definition-FHXFAJF6-CnTqJmQ2.js} +3 -3
  64. package/dist/assets/vennDiagram-L72KCM5P-D_qZxRhe.js +34 -0
  65. package/dist/assets/wardleyDiagram-EHGQE667-CT-WHFEO.js +78 -0
  66. package/dist/assets/xychartDiagram-FW5EYKEG-CVRFHwUQ.js +7 -0
  67. package/dist/index.html +2 -2
  68. package/lib/agent-string.js +34 -0
  69. package/lib/build.js +542 -148
  70. package/lib/experiment-judge.js +145 -0
  71. package/lib/experiment-metrics.js +265 -0
  72. package/lib/experiment-pricing.js +60 -0
  73. package/lib/experiment-report.js +306 -0
  74. package/lib/experiment-sandbox.js +147 -0
  75. package/lib/experiment.js +539 -0
  76. package/lib/feature-json.js +1 -1
  77. package/lib/feature-writer.js +30 -0
  78. package/lib/flow-state.js +36 -0
  79. package/lib/gate-prompt.js +66 -6
  80. package/lib/lifecycle-modes.js +213 -0
  81. package/lib/new.js +6 -2
  82. package/lib/roadmap-graph/index.js +26 -28
  83. package/lib/roadmap-graph/vision-adapter.js +126 -0
  84. package/lib/stratum-mcp-client.js +9 -0
  85. package/lib/triage.js +7 -1
  86. package/lib/vision-writer.js +30 -6
  87. package/package.json +1 -1
  88. package/pipelines/plan.stratum.yaml +161 -0
  89. package/server/artifact-manager.js +30 -3
  90. package/server/compose-mcp-tools.js +11 -9
  91. package/server/compose-mcp.js +4 -0
  92. package/server/feature-scan.js +87 -173
  93. package/server/graph-export.js +0 -0
  94. package/server/index.js +3 -5
  95. package/server/lifecycle-guard.js +62 -25
  96. package/server/roadmap-graph-vision.js +138 -0
  97. package/server/status-snapshot.js +8 -7
  98. package/server/vision-routes.js +64 -33
  99. package/server/vision-store.js +13 -4
  100. package/.claude/skills/compose/references/hermes-tools.md +0 -80
  101. package/dist/assets/App-Ddhfx18X.js +0 -889
  102. package/dist/assets/_baseUniq-CoEVVYyC.js +0 -1
  103. package/dist/assets/arc-DFnGxuCB.js +0 -1
  104. package/dist/assets/architectureDiagram-Q4EWVU46-66rx01Tn.js +0 -36
  105. package/dist/assets/blockDiagram-DXYQGD6D-AE5PKSak.js +0 -132
  106. package/dist/assets/channel-CScyWprq.js +0 -1
  107. package/dist/assets/chunk-4TB4RGXK-5w2lZAjW.js +0 -206
  108. package/dist/assets/chunk-OYMX7WX6-CGr_YiAc.js +0 -231
  109. package/dist/assets/classDiagram-6PBFFD2Q-DibVYYdr.js +0 -1
  110. package/dist/assets/classDiagram-v2-HSJHXN6E-DibVYYdr.js +0 -1
  111. package/dist/assets/clone-ZMALIFmw.js +0 -1
  112. package/dist/assets/dagre-KV5264BT--mrfaTHR.js +0 -4
  113. package/dist/assets/diagram-5BDNPKRD-DjXvyFzh.js +0 -10
  114. package/dist/assets/diagram-G4DWMVQ6-BrrexEvm.js +0 -24
  115. package/dist/assets/diagram-MMDJMWI5-D3Q0OT2M.js +0 -43
  116. package/dist/assets/diagram-TYMM5635-DKTntmxE.js +0 -24
  117. package/dist/assets/erDiagram-SMLLAGMA-BWbR0dPj.js +0 -85
  118. package/dist/assets/flowDiagram-DWJPFMVM-Dj5M5XaY.js +0 -162
  119. package/dist/assets/ganttDiagram-T4ZO3ILL-DKppFh_5.js +0 -292
  120. package/dist/assets/gitGraphDiagram-UUTBAWPF-9xQ9O25V.js +0 -106
  121. package/dist/assets/graph-Cgmhvu1T.js +0 -1
  122. package/dist/assets/graph-DZe55uk8.js +0 -331
  123. package/dist/assets/index-D8uDfn8y.js +0 -123
  124. package/dist/assets/infoDiagram-42DDH7IO-DQYWVzVN.js +0 -2
  125. package/dist/assets/katex-DkKDou_j.js +0 -257
  126. package/dist/assets/layout-DN13RkA1.js +0 -1
  127. package/dist/assets/min-CXr-fUFS.js +0 -1
  128. package/dist/assets/pieDiagram-DEJITSTG-CVipbIlr.js +0 -30
  129. package/dist/assets/quadrantDiagram-34T5L4WZ-CHJFyhm-.js +0 -7
  130. package/dist/assets/sankeyDiagram-XADWPNL6-BSULeqST.js +0 -10
  131. package/dist/assets/sequenceDiagram-FGHM5R23-CCOKXvjj.js +0 -157
  132. package/dist/assets/stateDiagram-FHFEXIEX-B91gKA7Q.js +0 -1
  133. package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dc8ur9ta.js +0 -1
  134. package/dist/assets/vennDiagram-DHZGUBPP-YvEPDlsM.js +0 -34
  135. package/dist/assets/wardley-RL74JXVD-TMBUq_w1.js +0 -162
  136. package/dist/assets/wardleyDiagram-NUSXRM2D-DzEkPrri.js +0 -20
  137. package/dist/assets/xychartDiagram-5P7HB3ND-BIiCeiYk.js +0 -7
  138. package/lib/roadmap-graph/collect.js +0 -178
@@ -16,14 +16,6 @@ Both modes share Phase 6 (plan), Phase 7 (TDD execute + E2E + review loop + cove
16
16
 
17
17
  Before any phase begins, read project state via the `compose` MCP server (get_vision_items, get_phase_summary, get_blocked_items). Every feature OR bug maps to a vision item — check it exists, create it if not.
18
18
 
19
- ## Platform Adaptation
20
-
21
- This SKILL.md uses **Claude Code** tool names (Skill, Agent/Task, EnterPlanMode, TodoWrite). On other harnesses, resolve each action to that harness's equivalent:
22
-
23
- - **Nous Hermes Agent** → `references/hermes-tools.md` (skills, `delegate_task` for subagents, MCP servers, AGENTS.md; Phase-6 plan mode degrades to a flat gate).
24
-
25
- The headless `compose build`/`compose fix` CLI is harness-agnostic — it runs from any shell and delegates agent execution to Stratum (Claude/Codex) regardless of the launching harness. The mapping files only matter for running the *interactive* lifecycle under a non-Claude-Code harness.
26
-
27
19
  ## Mode Selection
28
20
 
29
21
  `/compose` with no verb defaults to `build` for back-compat. Explicit verbs:
package/README.md CHANGED
@@ -30,6 +30,17 @@ compose build TODO-1
30
30
  -> done: feature implemented, reviewed, tested, documented
31
31
  ```
32
32
 
33
+ Starting from a fuzzy goal instead of a known feature? Use the planning lifecycle:
34
+
35
+ ```bash
36
+ compose plan "a tool that summarizes my team's standups"
37
+ -> frame + research + ideate (ideas go to the ideabox) -> [gate]
38
+ -> converge + estimate -> writes build-ready feature.json + design.md -> [gate]
39
+ -> handoff: each feature ready for `compose build <CODE>`
40
+ ```
41
+
42
+ `compose build` then picks up a plan-authored feature and ratifies its design rather than rewriting it.
43
+
33
44
  ## Quick install
34
45
 
35
46
  Prerequisites: Node.js 18+ and `stratum-mcp` on PATH (`pip install stratum-mcp`, requires Python 3.11+). Codex steps additionally need the OpenAI `codex` CLI. Full prereqs in [docs/install.md](docs/install.md).
package/bin/compose.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * compose start — start the compose app (supervisor.js)
9
9
  * compose build — headless feature lifecycle runner
10
10
  * compose fix — headless bug-fix lifecycle runner (pipelines/bug-fix.stratum.yaml)
11
+ * compose plan — product-planning lifecycle runner (pipelines/plan.stratum.yaml)
11
12
  */
12
13
  import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'fs'
13
14
  import { resolve, join, basename, dirname, sep } from 'path'
@@ -19,6 +20,7 @@ import { resolveWorkspace, getWorkspaceFlag } from '../lib/resolve-workspace.js'
19
20
  import { resolvePort } from '../lib/resolve-port.js'
20
21
  import { resolveRoadmapPath, resolveFeaturesPath, resolveContextPathFromConfig, resolveFeaturesPathFromConfig, resolveRoadmapPathFromConfig } from '../lib/project-paths.js'
21
22
  import { installAgentDefs } from '../lib/install-agent-defs.js'
23
+ import { validateAgentString } from '../lib/agent-string.js'
22
24
 
23
25
  const __dirname = dirname(fileURLToPath(import.meta.url))
24
26
  const PACKAGE_ROOT = resolve(__dirname, '..')
@@ -565,7 +567,7 @@ async function runInit(flags, cwdOverride) {
565
567
  // 8. Copy default pipeline specs if absent
566
568
  const pipelinesDir = join(cwd, 'pipelines')
567
569
  mkdirSync(pipelinesDir, { recursive: true })
568
- for (const specName of ['build.stratum.yaml', 'build-quick.stratum.yaml', 'new.stratum.yaml']) {
570
+ for (const specName of ['build.stratum.yaml', 'build-quick.stratum.yaml', 'new.stratum.yaml', 'plan.stratum.yaml']) {
569
571
  const dest = join(pipelinesDir, specName)
570
572
  if (!existsSync(dest)) {
571
573
  const src = join(PACKAGE_ROOT, 'pipelines', specName)
@@ -1180,6 +1182,125 @@ if (cmd === 'roadmap') {
1180
1182
  process.exit(0)
1181
1183
  }
1182
1184
 
1185
+ // compose roadmap add <CODE> — write a (build-ready) feature via the canonical
1186
+ // typed writer (lib/feature-writer.js addRoadmapEntry). This is the CLI mirror of
1187
+ // the add_roadmap_entry MCP tool, so an agent of ANY runtime (claude, codex, ...)
1188
+ // can emit a build-ready feature.json without the compose MCP. The plan
1189
+ // lifecycle's spec step uses it to write build-ready features (COMP-ROADMAP-PLAN).
1190
+ if (subcmd === 'add') {
1191
+ const { addRoadmapEntry } = await import('../lib/feature-writer.js')
1192
+ const { root: cwd } = resolveCwdWithWorkspace(args)
1193
+ const VALUE_FLAGS = ['--description', '--phase', '--status', '--complexity', '--profile', '--triage-timestamp', '--planned-by', '--impact', '--cwd', '--workspace', '--team']
1194
+ // flagVal returns the value only when it exists and is not itself another flag,
1195
+ // so `--description --phase X` does not silently capture "--phase" as the value.
1196
+ const flagVal = (name) => {
1197
+ const i = args.indexOf(name)
1198
+ if (i === -1) return undefined
1199
+ const v = args[i + 1]
1200
+ return (v === undefined || v.startsWith('-')) ? undefined : v
1201
+ }
1202
+ // The positional <CODE> is the first non-flag token that is NOT a value-taking
1203
+ // flag's argument (so `--planned-by PLAN-X REAL-1` resolves the code to REAL-1).
1204
+ const valueFlagSet = new Set(VALUE_FLAGS)
1205
+ const valueIdx = new Set()
1206
+ for (let i = 1; i < args.length; i++) { if (valueFlagSet.has(args[i])) valueIdx.add(i + 1) }
1207
+ let code
1208
+ for (let i = 1; i < args.length; i++) {
1209
+ if (!args[i].startsWith('-') && !valueIdx.has(i)) { code = args[i]; break }
1210
+ }
1211
+ const description = flagVal('--description')
1212
+ const phase = flagVal('--phase')
1213
+ if (!code || !description || !phase) {
1214
+ console.error('Usage: compose roadmap add <CODE> --description "..." --phase "..." \\')
1215
+ console.error(' [--status PLANNED] [--complexity S|M|L|XL] [--profile <JSON>] \\')
1216
+ console.error(' [--triage-timestamp <ISO>] [--planned-by <CODE>] [--impact low|medium|high]')
1217
+ console.error('')
1218
+ console.error('Writes feature.json via the canonical writer and regenerates ROADMAP.md.')
1219
+ process.exit(1)
1220
+ }
1221
+ const entry = { code, description, phase, status: flagVal('--status') ?? 'PLANNED' }
1222
+ const complexity = flagVal('--complexity'); if (complexity) entry.complexity = complexity
1223
+ const plannedBy = flagVal('--planned-by'); if (plannedBy) entry.plannedBy = plannedBy
1224
+ // --planned-by present but valueless must NOT silently fall through to the
1225
+ // non-handshake path (it would write a non-build-ready feature).
1226
+ if (args.includes('--planned-by') && !plannedBy) {
1227
+ console.error('--planned-by requires a value (the originating plan session code)')
1228
+ process.exit(1)
1229
+ }
1230
+ const impact = flagVal('--impact'); if (impact) entry.impact = impact
1231
+ const profileRaw = flagVal('--profile')
1232
+ if (profileRaw) {
1233
+ let parsed
1234
+ try { parsed = JSON.parse(profileRaw) }
1235
+ catch (e) { console.error(`--profile must be valid JSON: ${e.message}`); process.exit(1) }
1236
+ // A build-ready triage profile must carry the four needs_* booleans build uses
1237
+ // to gate skippable phases — otherwise build skips fresh triage on a cache with
1238
+ // no gating fields (COMP-ROADMAP-PLAN; profile contract in lib/triage.js).
1239
+ const NEEDS = ['needs_prd', 'needs_architecture', 'needs_verification', 'needs_report']
1240
+ const missing = NEEDS.filter((k) => typeof parsed?.[k] !== 'boolean')
1241
+ if (missing.length) {
1242
+ console.error(`--profile must include boolean fields: ${missing.join(', ')} (build triage gating)`)
1243
+ process.exit(1)
1244
+ }
1245
+ entry.profile = parsed
1246
+ }
1247
+ // A plan handshake (--planned-by) must be FULLY build-ready or build won't consume
1248
+ // it as one: profile + complexity + PLANNED status + an existing design.md (build
1249
+ // ratifies it). Enforce the whole contract here (build reuses cached triage only
1250
+ // when feature.profile exists and the cache is fresh — lib/build.js, lib/triage.js).
1251
+ let designMtime = 0
1252
+ if (plannedBy) {
1253
+ if (!entry.profile) {
1254
+ console.error('--planned-by requires --profile (a build-ready plan handshake must carry the triage profile)')
1255
+ process.exit(1)
1256
+ }
1257
+ if (!entry.complexity) {
1258
+ console.error('--planned-by requires --complexity (S|M|L|XL) for a build-ready handshake')
1259
+ process.exit(1)
1260
+ }
1261
+ if (entry.status !== 'PLANNED') {
1262
+ console.error(`--planned-by must be PLANNED (got ${entry.status}) — a plan handshake produces a build-ready PLANNED feature`)
1263
+ process.exit(1)
1264
+ }
1265
+ const { resolveFeaturesPath } = await import('../lib/project-paths.js')
1266
+ // alias the fs import — statSync is re-bound later in this roadmap block
1267
+ const { statSync: statFile } = await import('fs')
1268
+ const designPath = join(resolveFeaturesPath(cwd), code, 'design.md')
1269
+ if (!existsSync(designPath)) {
1270
+ console.error(`--planned-by: ${code}/design.md not found (${designPath}). Write the plan-approved design.md BEFORE creating the feature — build ratifies it.`)
1271
+ process.exit(1)
1272
+ }
1273
+ designMtime = statFile(designPath).mtime.getTime()
1274
+ }
1275
+ // triageTimestamp: explicit (validated, and for a handshake not older than design.md
1276
+ // or build re-triages), else default to now for a plan handshake.
1277
+ const ts = flagVal('--triage-timestamp')
1278
+ if (ts) {
1279
+ const tsMs = new Date(ts).getTime()
1280
+ if (Number.isNaN(tsMs)) {
1281
+ console.error(`--triage-timestamp must be a valid ISO 8601 date (got "${ts}")`)
1282
+ process.exit(1)
1283
+ }
1284
+ if (plannedBy && tsMs < designMtime) {
1285
+ console.error('--triage-timestamp is older than design.md — build would re-triage. Omit it to stamp now, or pass a time at/after the design write.')
1286
+ process.exit(1)
1287
+ }
1288
+ entry.triageTimestamp = ts
1289
+ } else if (plannedBy) {
1290
+ // Stamp now, but never older than design.md (guards a future-dated design.md /
1291
+ // clock skew, which would otherwise read as stale and force a re-triage).
1292
+ entry.triageTimestamp = new Date(Math.max(Date.now(), designMtime)).toISOString()
1293
+ }
1294
+ try {
1295
+ const r = await addRoadmapEntry(cwd, entry)
1296
+ console.log(`Wrote ${r.code} (${entry.status}) and regenerated ROADMAP.md`)
1297
+ } catch (e) {
1298
+ console.error(`roadmap add failed: ${e.message}`)
1299
+ process.exit(1)
1300
+ }
1301
+ process.exit(0)
1302
+ }
1303
+
1183
1304
  // compose roadmap migrate — extract ROADMAP.md entries into feature.json files
1184
1305
  if (subcmd === 'migrate') {
1185
1306
  const { migrateRoadmap } = await import('../lib/migrate-roadmap.js')
@@ -1286,9 +1407,11 @@ if (cmd === 'roadmap') {
1286
1407
  }
1287
1408
 
1288
1409
  // compose roadmap graph — generate a self-contained dependency-graph HTML
1289
- // from feature.json + deps.yaml + frontmatter (COMP-ROADMAP-GRAPH-1).
1410
+ // from the canonical vision projection (COMP-ROADMAP-GRAPH-2: the vision
1411
+ // model is the single source, deterministically seeded from feature.json +
1412
+ // deps.yaml; renders through the one shared renderer).
1290
1413
  if (subcmd === 'graph') {
1291
- const { generateRoadmapGraph, checkRoadmapGraph } = await import('../lib/roadmap-graph/index.js')
1414
+ const { generateRoadmapGraph, checkRoadmapGraph } = await import('../server/roadmap-graph-vision.js')
1292
1415
  let cwd
1293
1416
  const projIdx = args.indexOf('--project')
1294
1417
  if (projIdx !== -1 && args[projIdx + 1]) {
@@ -1952,6 +2075,34 @@ if (cmd === 'pipeline') {
1952
2075
  process.exit(0)
1953
2076
  }
1954
2077
 
2078
+ // COMP-MODEL-AB: compose experiment <spec.json> [--prune-workspaces]
2079
+ if (cmd === 'experiment') {
2080
+ const specArg = args[0]
2081
+ if (!specArg || specArg.startsWith('-')) {
2082
+ console.error('Usage: compose experiment <spec.json> [--prune-workspaces]')
2083
+ console.error('')
2084
+ console.error('Runs a sandboxed A/B experiment across model configs.')
2085
+ console.error('Writes results.json + report.md next to the spec file.')
2086
+ process.exit(1)
2087
+ }
2088
+ const pruneWorkspaces = args.includes('--prune-workspaces')
2089
+ const specPath = resolve(specArg)
2090
+ if (!existsSync(specPath)) {
2091
+ console.error(`Error: spec file not found: ${specPath}`)
2092
+ process.exit(1)
2093
+ }
2094
+ const { runExperiment } = await import('../lib/experiment.js')
2095
+ try {
2096
+ const result = await runExperiment(specPath, { pruneWorkspaces })
2097
+ console.log(`Results: ${result.resultsPath}`)
2098
+ console.log(`Report: ${result.reportPath}`)
2099
+ } catch (err) {
2100
+ console.error(`Error: ${err.message}`)
2101
+ process.exit(1)
2102
+ }
2103
+ process.exit(0)
2104
+ }
2105
+
1955
2106
  if (cmd === 'build') {
1956
2107
  // Parse --cwd <path> for cross-repo builds
1957
2108
  let agentWorkDir = null
@@ -1999,6 +2150,8 @@ if (cmd === 'build') {
1999
2150
  const all = filteredArgs2.includes('--all')
2000
2151
  const dryRun = filteredArgs2.includes('--dry-run')
2001
2152
  const skipTriage = filteredArgs2.includes('--skip-triage')
2153
+ const resume = filteredArgs2.includes('--resume')
2154
+ const fresh = filteredArgs2.includes('--fresh')
2002
2155
  // COMP-BUILD-QUICK: --quick selects the trimmed build-quick pipeline
2003
2156
  // (design → implement → ship). Symmetric to fix mode's Quick path; the real
2004
2157
  // mirror is the existing --template mechanism (fix dispatches template:'bug-fix'),
@@ -2007,6 +2160,29 @@ if (cmd === 'build') {
2007
2160
  // COMP-CODEX-IMPL: --codex flips the implementer to Codex (Claude reviews). v1 is
2008
2161
  // full-build, single-feature only — mutually exclusive with --quick and batch.
2009
2162
  const codex = filteredArgs2.includes('--codex')
2163
+ const nonInteractiveBuild = filteredArgs2.includes('--non-interactive')
2164
+
2165
+ // COMP-MODEL-AB: --implementer=<agentStr> / --reviewer=<agentStr> override the
2166
+ // --codex-derived defaults. Format: --implementer=claude::critical (= operator).
2167
+ // Mutual exclusion applies when the explicit value conflicts with --codex's implicit
2168
+ // assignment; identical values (e.g. --codex --implementer=codex) are allowed.
2169
+ const implementerFlagMatch = filteredArgs2.find(a => a.startsWith('--implementer='))
2170
+ const implementerArg = implementerFlagMatch ? implementerFlagMatch.slice('--implementer='.length) : null
2171
+ const reviewerFlagMatch = filteredArgs2.find(a => a.startsWith('--reviewer='))
2172
+ const reviewerArg = reviewerFlagMatch ? reviewerFlagMatch.slice('--reviewer='.length) : null
2173
+
2174
+ if (implementerArg) {
2175
+ try { validateAgentString(implementerArg) } catch (err) {
2176
+ console.error(`Error: --implementer: ${err.message}`)
2177
+ process.exit(1)
2178
+ }
2179
+ }
2180
+ if (reviewerArg) {
2181
+ try { validateAgentString(reviewerArg) } catch (err) {
2182
+ console.error(`Error: --reviewer: ${err.message}`)
2183
+ process.exit(1)
2184
+ }
2185
+ }
2010
2186
 
2011
2187
  // Multiple codes: compose build FEAT-1 FEAT-2 FEAT-3
2012
2188
  const isMulti = featureCodes.length > 1
@@ -2018,6 +2194,14 @@ if (cmd === 'build') {
2018
2194
  console.error('Error: --abort and --all/prefix/multi are mutually exclusive')
2019
2195
  process.exit(1)
2020
2196
  }
2197
+ if (resume && fresh) {
2198
+ console.error('--resume and --fresh are mutually exclusive')
2199
+ process.exit(1)
2200
+ }
2201
+ if ((resume || fresh) && isBatch) {
2202
+ console.error('Error: --resume/--fresh cannot be combined with --all/prefix/multi (single feature only)')
2203
+ process.exit(1)
2204
+ }
2021
2205
 
2022
2206
  // COMP-BUILD-QUICK: --quick is a single-feature shortcut for a specific
2023
2207
  // template, so it conflicts with an explicit --template and with batch builds.
@@ -2044,6 +2228,17 @@ if (cmd === 'build') {
2044
2228
  process.exit(1)
2045
2229
  }
2046
2230
 
2231
+ // COMP-MODEL-AB: mutual exclusion — conflict only when the explicit value differs
2232
+ // from what --codex would set (--codex sets implementer=codex, reviewer=claude).
2233
+ if (codex && implementerArg && implementerArg !== 'codex') {
2234
+ console.error(`Error: --implementer=${implementerArg} conflicts with --codex (--codex sets implementer=codex); omit --codex or use --implementer=codex`)
2235
+ process.exit(1)
2236
+ }
2237
+ if (codex && reviewerArg && reviewerArg !== 'claude') {
2238
+ console.error(`Error: --reviewer=${reviewerArg} conflicts with --codex (--codex sets reviewer=claude); omit --codex or use --reviewer=claude`)
2239
+ process.exit(1)
2240
+ }
2241
+
2047
2242
  if (!featureCode && !abort && !all) {
2048
2243
  console.error('Usage: compose build <feature-code> [feature-code...]')
2049
2244
  console.error(' compose build STRAT-COMP (prefix — builds all matching)')
@@ -2052,6 +2247,8 @@ if (cmd === 'build') {
2052
2247
  console.error('Options:')
2053
2248
  console.error(' --quick Trimmed lifecycle (design → implement → ship) for small additive work')
2054
2249
  console.error(' --abort Abort the active build')
2250
+ console.error(' --resume Resume the active build for <feature-code>')
2251
+ console.error(' --fresh Start a fresh build, discarding stale failed/resumable state')
2055
2252
  console.error(' --all Build all PLANNED features in dependency order')
2056
2253
  console.error(' --dry-run Print build order without executing')
2057
2254
  console.error(' --cwd <path> Agent working directory (for cross-repo features)')
@@ -2100,6 +2297,25 @@ if (cmd === 'build') {
2100
2297
  })
2101
2298
  })
2102
2299
  } else {
2300
+ let resumeFlowId = null
2301
+ if (resume && !abort && featureCode) {
2302
+ const activeBuildPath = join(buildCwd, '.compose', 'data', 'active-build.json')
2303
+ let active = null
2304
+ if (existsSync(activeBuildPath)) {
2305
+ try { active = JSON.parse(readFileSync(activeBuildPath, 'utf-8')) } catch { active = null }
2306
+ }
2307
+ if (
2308
+ !active ||
2309
+ active.featureCode !== featureCode ||
2310
+ !active.flowId ||
2311
+ ['complete', 'aborted', 'killed'].includes(active.status)
2312
+ ) {
2313
+ console.error(`Nothing to resume for ${featureCode} (no in-progress or failed build found)`)
2314
+ process.exit(1)
2315
+ }
2316
+ resumeFlowId = active.flowId
2317
+ }
2318
+
2103
2319
  import('../lib/build.js').then(({ runBuild }) => {
2104
2320
  const singleOpts = { abort }
2105
2321
  if (agentWorkDir) singleOpts.workingDirectory = agentWorkDir
@@ -2107,6 +2323,12 @@ if (cmd === 'build') {
2107
2323
  if (templateName) singleOpts.template = templateName
2108
2324
  if (quick) singleOpts.template = 'build-quick' // COMP-BUILD-QUICK
2109
2325
  if (codex) singleOpts.codex = true // COMP-CODEX-IMPL
2326
+ if (implementerArg) singleOpts.implementer = implementerArg // COMP-MODEL-AB
2327
+ if (reviewerArg) singleOpts.reviewer = reviewerArg // COMP-MODEL-AB
2328
+ if (resume) singleOpts.resume = true
2329
+ if (fresh) singleOpts.fresh = true
2330
+ if (nonInteractiveBuild) singleOpts.gateOpts = { nonInteractive: true }
2331
+ if (resumeFlowId) singleOpts.resumeFlowId = resumeFlowId
2110
2332
  runBuild(featureCode, singleOpts).then(() => {
2111
2333
  process.exit(0)
2112
2334
  }).catch((err) => {
@@ -2227,6 +2449,116 @@ if (cmd === 'build') {
2227
2449
  process.exit(1)
2228
2450
  })
2229
2451
  })
2452
+ } else if (cmd === 'plan') {
2453
+ // compose plan "<intent>" — runs the plan.stratum.yaml product-planning
2454
+ // lifecycle (explore_design → plan → ship). Thin delegation to runBuild() with
2455
+ // template='plan', mode='plan'. Derives a PLAN-<slug> code from the intent
2456
+ // (override with --code). Always passes opts.description so the dead
2457
+ // descriptionLoader:'plan' branch never runs (it would read a stray design.md).
2458
+ // Emits build-ready docs/features/<code>/{feature.json, design.md} that
2459
+ // `compose build <code>` consumes.
2460
+ let agentWorkDir = null
2461
+ const cwdIdx = args.indexOf('--cwd')
2462
+ if (cwdIdx !== -1) {
2463
+ const cwdValue = args[cwdIdx + 1]
2464
+ if (!cwdValue || cwdValue.startsWith('-')) {
2465
+ console.error('Error: --cwd requires a path argument')
2466
+ process.exit(1)
2467
+ }
2468
+ agentWorkDir = resolve(cwdValue)
2469
+ }
2470
+ const codeIdx = args.indexOf('--code')
2471
+ const explicitCode = codeIdx !== -1 ? args[codeIdx + 1] : null
2472
+ const abort = args.includes('--abort')
2473
+ const resume = args.includes('--resume')
2474
+ // Everything that isn't a flag or a value-taking flag's argument is the intent.
2475
+ // Skip the values of every value-taking flag so e.g. `--workspace foo` or
2476
+ // `--team t` never pollute the derived intent/slug.
2477
+ const skip = new Set()
2478
+ for (const flag of ['--cwd', '--code', '--workspace', '--team']) {
2479
+ const fi = args.indexOf(flag)
2480
+ if (fi !== -1) { skip.add(fi); skip.add(fi + 1) }
2481
+ }
2482
+ const intent = args.filter((a, i) => !a.startsWith('-') && !skip.has(i)).join(' ')
2483
+
2484
+ if (!intent && !abort && !resume) {
2485
+ console.error('Usage: compose plan "<what you want to build>" [--code PLAN-X] [--resume] [--abort]')
2486
+ console.error('')
2487
+ console.error('Runs the product-planning lifecycle (frame → research → ideate → converge → handoff).')
2488
+ console.error('Emits build-ready docs/features/<code>/{feature.json, design.md} for `compose build`.')
2489
+ console.error('')
2490
+ console.error('Options:')
2491
+ console.error(' --code <CODE> Use an explicit plan code instead of a derived PLAN-<slug>')
2492
+ console.error(' --resume Resume the active plan run')
2493
+ console.error(' --abort Abort the active plan run')
2494
+ console.error(' --cwd <path> Agent working directory')
2495
+ process.exit(1)
2496
+ }
2497
+
2498
+ const { root: planCwd } = resolveCwdWithWorkspace(args)
2499
+ if (!existsSync(join(planCwd, '.compose', 'compose.json')) || !existsSync(join(planCwd, 'pipelines', 'plan.stratum.yaml'))) {
2500
+ console.log('Running compose init...\n')
2501
+ await runInit(args.filter(a => a.startsWith('--')))
2502
+ console.log('')
2503
+ }
2504
+
2505
+ // Resolve the plan code. --resume/--abort operate on the ACTIVE build, so they
2506
+ // take the code from active-build.json (never derive a fresh one — that would
2507
+ // mint PLAN-<slug>-2 and miss the live flow). A fresh run derives a
2508
+ // deterministic PLAN-<slug> from the intent, dedup'd against existing workspaces.
2509
+ let planCode = explicitCode
2510
+ let resumeFlowId = null
2511
+ if (resume || abort) {
2512
+ const activeBuildPath = join(planCwd, '.compose', 'data', 'active-build.json')
2513
+ let active = null
2514
+ if (existsSync(activeBuildPath)) {
2515
+ try { active = JSON.parse(readFileSync(activeBuildPath, 'utf-8')) } catch { active = null }
2516
+ }
2517
+ if (!planCode) planCode = active?.featureCode ?? null
2518
+ if (resume) {
2519
+ if (!active || !active.flowId || (planCode && active.featureCode !== planCode)) {
2520
+ console.error(`No active plan build to resume${planCode ? ` for ${planCode}` : ''}.`)
2521
+ process.exit(1)
2522
+ }
2523
+ if (active.mode && active.mode !== 'plan') {
2524
+ console.error(`Cannot --resume: active build for ${active.featureCode} is in ${active.mode} mode, not plan mode.`)
2525
+ process.exit(1)
2526
+ }
2527
+ planCode = active.featureCode
2528
+ resumeFlowId = active.flowId
2529
+ }
2530
+ // --abort: only abort an active PLAN build — abortBuild does not check mode,
2531
+ // so guard here or `compose plan --abort` could kill a feature/fix run.
2532
+ if (abort && active && active.mode && active.mode !== 'plan') {
2533
+ console.error(`Cannot --abort as plan: the active build for ${active.featureCode} is in ${active.mode} mode, not plan mode.`)
2534
+ process.exit(1)
2535
+ }
2536
+ // planCode is now the active build's code (or an explicit --code);
2537
+ // runBuild(planCode, {abort}) matches it against active-build.json.
2538
+ } else if (!planCode) {
2539
+ const rawSlug = intent.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24).replace(/-+$/, '')
2540
+ const slug = rawSlug || 'session'
2541
+ const base = `PLAN-${slug}`.toUpperCase()
2542
+ let candidate = base
2543
+ let n = 2
2544
+ while (existsSync(join(planCwd, 'docs', 'plans', candidate))) {
2545
+ candidate = `${base}-${n}`
2546
+ n++
2547
+ }
2548
+ planCode = candidate
2549
+ }
2550
+
2551
+ import('../lib/build.js').then(({ runBuild }) => {
2552
+ const opts = { abort, template: 'plan', mode: 'plan', description: intent }
2553
+ if (agentWorkDir) opts.workingDirectory = agentWorkDir
2554
+ if (resumeFlowId) opts.resumeFlowId = resumeFlowId
2555
+ runBuild(planCode, opts).then(() => {
2556
+ process.exit(0)
2557
+ }).catch((err) => {
2558
+ console.error(`Plan failed: ${err.message}`)
2559
+ process.exit(1)
2560
+ })
2561
+ })
2230
2562
  } else if (cmd === 'gsd') {
2231
2563
  // compose gsd <feature-code> — runs the per-task fresh-context dispatch
2232
2564
  // pipeline (pipelines/gsd.stratum.yaml). Hard-requires existing