@smartmemory/compose 0.3.6-beta → 0.3.8

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 (269) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/.compose-deps.json +1 -13
  3. package/README.md +72 -5
  4. package/bin/compose.js +754 -347
  5. package/bin/git-hooks/pre-push.template +29 -0
  6. package/bin/judgment-import.js +7 -0
  7. package/bin/judgment-migrate.js +387 -0
  8. package/contracts/comp-obs-contract.schema.json +9 -3
  9. package/contracts/feature-json.schema.json +5 -0
  10. package/contracts/fluid-record.schema.json +209 -0
  11. package/contracts/judgment-record.schema.json +425 -4
  12. package/contracts/lifecycle-backfill.schema.json +322 -0
  13. package/dist/assets/App-Z4MU-H_F.js +916 -0
  14. package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
  15. package/dist/assets/arc-DY26UIVo.js +1 -0
  16. package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
  17. package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
  18. package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
  19. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  20. package/dist/assets/channel-SnZzzh7k.js +1 -0
  21. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  22. package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
  23. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  24. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  26. package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  28. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  29. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  30. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  31. package/dist/assets/clone-DgklGjHm.js +1 -0
  32. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  33. package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
  34. package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
  35. package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
  36. package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
  37. package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
  38. package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
  39. package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
  40. package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
  41. package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
  42. package/dist/assets/graph-BNzKGvoy.js +1 -0
  43. package/dist/assets/graph-CI_1htl0.js +331 -0
  44. package/dist/assets/index-BEfrNBp8.js +123 -0
  45. package/dist/assets/index-yyrA5OZd.css +1 -0
  46. package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
  47. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
  48. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  49. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
  50. package/dist/assets/katex-DkKDou_j.js +257 -0
  51. package/dist/assets/layout-BI8cXFPI.js +1 -0
  52. package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
  53. package/dist/assets/min-vPHfnXcC.js +1 -0
  54. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
  55. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  56. package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
  57. package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
  58. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
  59. package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
  60. package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
  61. package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
  62. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  63. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
  64. package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
  65. package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
  66. package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
  67. package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +7 -5
  70. package/lib/append-integrity.js +81 -0
  71. package/lib/backfill-evidence.js +109 -0
  72. package/lib/bug-escalation.js +39 -4
  73. package/lib/build-stream-schema.js +3 -1
  74. package/lib/build-stream-writer.js +25 -0
  75. package/lib/build.js +1624 -195
  76. package/lib/canon-guard.js +245 -0
  77. package/lib/canon-override.js +196 -0
  78. package/lib/canon-registry.js +291 -0
  79. package/lib/cli-commands.js +144 -0
  80. package/lib/codex-preflight.js +50 -15
  81. package/lib/colleague/context.js +215 -0
  82. package/lib/colleague/writeback.js +95 -0
  83. package/lib/completion-gate.js +1421 -0
  84. package/lib/completion-writer.js +47 -47
  85. package/lib/consumer-fanout.js +105 -11
  86. package/lib/coverage-gate.js +200 -0
  87. package/lib/dir-lock.js +170 -0
  88. package/lib/dispatch-ledger.js +301 -0
  89. package/lib/dispatch-metrics.js +236 -0
  90. package/lib/experiment-judge.js +6 -1
  91. package/lib/feature-json.js +1 -1
  92. package/lib/feature-reconciler.js +8 -0
  93. package/lib/feature-validator.js +64 -1
  94. package/lib/feature-writer.js +66 -2
  95. package/lib/fluid/factory.js +167 -0
  96. package/lib/fluid/ideabox-dates.js +73 -0
  97. package/lib/fluid/ideabox-migrate.js +154 -0
  98. package/lib/fluid/ideabox-ops.js +585 -0
  99. package/lib/fluid/ideabox-view.js +146 -0
  100. package/lib/fluid/import-ideabox.js +186 -0
  101. package/lib/fluid/local-provider.js +606 -0
  102. package/lib/fluid/provider.js +684 -0
  103. package/lib/fluid/record-shape.js +214 -0
  104. package/lib/fluid/record-store.js +328 -0
  105. package/lib/fluid/render-ideabox.js +261 -0
  106. package/lib/fluid/schema.js +40 -0
  107. package/lib/fluid/smartmemory-provider.js +1695 -0
  108. package/lib/gsd.js +63 -14
  109. package/lib/guard-cli.js +175 -0
  110. package/lib/guard-custody.js +141 -0
  111. package/lib/guard-descriptors.js +530 -0
  112. package/lib/guard-enrol.js +254 -0
  113. package/lib/health-score.js +1 -1
  114. package/lib/hooks-status.js +32 -3
  115. package/lib/ideabox-cli.js +315 -0
  116. package/lib/ideabox.js +121 -21
  117. package/lib/judgment/store/index.js +166 -0
  118. package/lib/judgment/store/records.js +184 -25
  119. package/lib/judgment/trace.js +380 -0
  120. package/lib/judgment-attest.js +259 -0
  121. package/lib/judgment-decision-write.js +277 -0
  122. package/lib/judgment-decisions.js +466 -0
  123. package/lib/judgment-gen.js +375 -22
  124. package/lib/judgment-verify.js +153 -0
  125. package/lib/judgment-writer.js +2842 -262
  126. package/lib/lane-gate.js +2 -0
  127. package/lib/lifecycle-modes.js +4 -4
  128. package/lib/lineage.js +400 -0
  129. package/lib/local-claude-connector.js +250 -54
  130. package/lib/maya-client.js +302 -0
  131. package/lib/maya-config.js +53 -0
  132. package/lib/maya-identity.js +283 -0
  133. package/lib/mcp-enforcement.js +21 -35
  134. package/lib/migrate-anon.js +5 -0
  135. package/lib/migrate-roadmap.js +15 -0
  136. package/lib/new.js +13 -1
  137. package/lib/pipeline-compat.js +104 -0
  138. package/lib/policy-catalog.js +295 -0
  139. package/lib/policy-check.js +0 -0
  140. package/lib/process-termination.js +98 -0
  141. package/lib/resolve-workspace.js +5 -1
  142. package/lib/result-normalizer.js +428 -153
  143. package/lib/review-normalize.js +4 -0
  144. package/lib/roadmap-errors.js +65 -0
  145. package/lib/roadmap-preservers.js +24 -4
  146. package/lib/roadmap-residue.js +299 -0
  147. package/lib/smartmemory-client.js +614 -78
  148. package/lib/smartmemory-config.js +54 -0
  149. package/lib/smartmemory-ingest.js +19 -2
  150. package/lib/step-prompt.js +7 -6
  151. package/lib/stratum-engine.js +53 -4
  152. package/lib/stratum-mcp-client.js +391 -31
  153. package/lib/test-bootstrap.js +31 -0
  154. package/lib/tool-inventory.js +122 -0
  155. package/lib/version-check.js +91 -19
  156. package/lib/vision-writer.js +88 -1
  157. package/package.json +7 -6
  158. package/pipelines/bug-fix.stratum.yaml +205 -211
  159. package/pipelines/build-quick.profiles.json +12 -0
  160. package/pipelines/build-quick.stratum.yaml +263 -350
  161. package/pipelines/content.stratum.yaml +81 -77
  162. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  163. package/pipelines/plan.stratum.yaml +76 -86
  164. package/pipelines/refactor.stratum.yaml +125 -125
  165. package/pipelines/research.stratum.yaml +56 -58
  166. package/pipelines/review-fix.profiles.json +6 -0
  167. package/pipelines/review-fix.stratum.yaml +110 -83
  168. package/presets/team-feature.profiles.json +6 -0
  169. package/presets/team-feature.stratum.yaml +93 -66
  170. package/presets/team-research.profiles.json +6 -0
  171. package/presets/team-research.stratum.yaml +89 -80
  172. package/presets/team-review.profiles.json +8 -0
  173. package/presets/team-review.stratum.yaml +98 -80
  174. package/scripts/cost-census.mjs +70 -0
  175. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  176. package/server/agent-health.js +22 -0
  177. package/server/agent-hooks.js +14 -1
  178. package/server/agent-server.js +5 -248
  179. package/server/agent-spawn.js +3 -4
  180. package/server/agent-workspace.js +294 -0
  181. package/server/build-routes.js +6 -5
  182. package/server/build-stream-bridge.js +53 -0
  183. package/server/cc-session-watcher.js +4 -1
  184. package/server/coalescing-buffer.js +7 -1
  185. package/server/completion-projection.js +228 -0
  186. package/server/compose-mcp-tools.js +124 -24
  187. package/server/compose-mcp.js +91 -790
  188. package/server/decision-event-emit.js +41 -2
  189. package/server/decision-event-id.js +17 -0
  190. package/server/decision-events-snapshot.js +3 -0
  191. package/server/design-routes.js +14 -8
  192. package/server/feature-scan.js +76 -2
  193. package/server/file-watcher.js +170 -21
  194. package/server/ideabox-routes.js +166 -224
  195. package/server/index.js +70 -100
  196. package/server/lifecycle-guard.js +240 -10
  197. package/server/lifecycle-phase-history.js +276 -0
  198. package/server/maya-routes.js +507 -0
  199. package/server/mcp-tool-defs.js +940 -0
  200. package/server/mcp-tool-policy.js +35 -3
  201. package/server/model-tiers.js +22 -5
  202. package/server/pipeline-routes.js +21 -11
  203. package/server/project-root.js +58 -19
  204. package/server/remote-utils.js +3 -1
  205. package/server/schema-validator.js +7 -1
  206. package/server/session-manager.js +5 -6
  207. package/server/session-routes.js +3 -1
  208. package/server/stratum-client.js +57 -10
  209. package/server/stratum-sync.js +6 -3
  210. package/server/summarizer.js +3 -4
  211. package/server/supervisor.js +0 -1
  212. package/server/vision-routes.js +208 -98
  213. package/server/vision-server.js +86 -23
  214. package/server/vision-store.js +60 -6
  215. package/server/vision-utils.js +3 -4
  216. package/server/workspace-activity.js +18 -0
  217. package/server/workspace-middleware.js +2 -2
  218. package/server/workspace-runtime.js +243 -0
  219. package/server/worktree-gc.js +1 -0
  220. package/dist/assets/App-BG3ngu8H.js +0 -896
  221. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  222. package/dist/assets/arc-_v4hR_uD.js +0 -1
  223. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  224. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  225. package/dist/assets/channel-B3U5wFAT.js +0 -1
  226. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  227. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  228. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  229. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  230. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  231. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  232. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  233. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  234. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  235. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  236. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  237. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  238. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  239. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  240. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  241. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  242. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  243. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  244. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  245. package/dist/assets/graph-BXPQrYYB.js +0 -331
  246. package/dist/assets/graph-C9eacEi8.js +0 -1
  247. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  248. package/dist/assets/index-LIwREYgH.css +0 -1
  249. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  250. package/dist/assets/katex-C5jXJg4s.js +0 -257
  251. package/dist/assets/layout-DEXfKzaS.js +0 -1
  252. package/dist/assets/map-Czzmt4hB.js +0 -1
  253. package/dist/assets/mobile-CaoXUwAr.js +0 -17
  254. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  255. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  256. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  257. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  258. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  259. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  260. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  261. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  262. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  263. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  264. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  265. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  266. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  267. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
  268. package/lib/staleness.js +0 -87
  269. package/server/ideabox-cache.js +0 -77
@@ -0,0 +1,291 @@
1
+ /**
2
+ * canon-registry.js — COMP-CANON-GUARD S1.
3
+ *
4
+ * The single declaration of what is canon: path pattern → writer → typed tools
5
+ * → enforcement points. Pure, no I/O (shape template: server/mcp-tool-policy.js).
6
+ *
7
+ * Consumed by BOTH:
8
+ * - lib/mcp-enforcement.js (the ship-time scan — the 'ship' point)
9
+ * - .claude/hooks/canon-guard.mjs (the write-time PreToolUse hook — 'hook')
10
+ *
11
+ * The load-bearing invariant (design Decision 1, corrected in blueprint-s1-s4):
12
+ * ONE registry does NOT imply ONE coverage. Each entry declares `enforcedBy`,
13
+ * and every enforcement point consumes only the subset that names it. This is
14
+ * what keeps the shared registry from locking out paths a point cannot yet
15
+ * legally guard:
16
+ * - docs/judgment/** is 100% tool-covered (S3 shipped the 8 judgment_* tools
17
+ * + regen), so the write-time hook can guard it with no lockout → ['hook'].
18
+ * - ROADMAP.md / feature.json have legal mutations NO tool covers yet
19
+ * (open a preserved section; edit a feature description — Decision 2), so
20
+ * the always-deny hook would lock them out. They stay ['ship'] (their
21
+ * existing build-event correlation) until update_feature_fields /
22
+ * open_preserved_section + the override land.
23
+ *
24
+ * Adding a path here turns on real enforcement — register a path for 'hook'
25
+ * ONLY once every legal mutation of it has a tool or an override.
26
+ */
27
+
28
+ // ── Tool sets ────────────────────────────────────────────────────────────────
29
+ // Moved verbatim from the pre-refactor mcp-enforcement.js literal sets. The
30
+ // contract test pins these against the legacy values.
31
+ const TOOLS_FOR_ROADMAP = ['add_roadmap_entry', 'set_feature_status', 'propose_followup'];
32
+ const TOOLS_FOR_CHANGELOG = ['add_changelog_entry'];
33
+ /** The override's governance state is written only by the grant tool itself. */
34
+ const TOOLS_FOR_OVERRIDE = ['canon_override_grant'];
35
+ const TOOLS_FOR_FEATURE_JSON = [
36
+ 'add_roadmap_entry',
37
+ 'set_feature_status',
38
+ 'link_artifact',
39
+ 'link_features',
40
+ 'record_completion',
41
+ // COMP-LIFECYCLE-BACKFILL S3-2: writes feature.json through the completion gate via HTTP.
42
+ 'backfill_completion',
43
+ 'propose_followup',
44
+ // COMP-COVERAGE-GATE C2 (2026-08-24): both write feature.json server-side via
45
+ // _postLifecycle (server/vision-routes.js:366 status write-back, :451
46
+ // kill→KILLED) and were missing here. `entry.tools` feeds only the
47
+ // canon-guard deny message ("use one of these tools instead") — no allow/deny
48
+ // decision reads it — so this widens no enforcement; it stops the rejection
49
+ // message from omitting two legitimate alternatives.
50
+ 'complete_feature',
51
+ 'kill_feature',
52
+ ];
53
+
54
+ /**
55
+ * The eight judgment WRITE tools (COMP-JUDGMENT-WRITER, shipped @751cc96a).
56
+ * get_judgment_state is read-only and is deliberately NOT here. Every mutation
57
+ * of docs/judgment/** — records under records/ and the generated projections
58
+ * (REGISTER/LEDGER/OBJECTIVE/SITUATION/index.md, people/*.md, positions/*.md) —
59
+ * goes through one of these, which regenerate the projections atomically.
60
+ */
61
+ export const JUDGMENT_WRITE_TOOLS = [
62
+ 'judgment_position_create',
63
+ 'judgment_position_amend',
64
+ 'judgment_joint_add',
65
+ 'judgment_transition',
66
+ 'judgment_ledger_append',
67
+ 'judgment_person_write',
68
+ 'judgment_situation_write',
69
+ 'judgment_goal_write',
70
+ ];
71
+
72
+ // ── Matchers ─────────────────────────────────────────────────────────────────
73
+ // A matcher is (path, { featuresDir }) => boolean. featuresDir is relative and
74
+ // project-configurable (loadFeaturesDir), so the feature.json matcher is
75
+ // parameterized; fixed-name and docs/judgment matchers ignore it.
76
+
77
+ function matchExact(name) {
78
+ return (path) => path === name;
79
+ }
80
+
81
+ /** startsWith(<featuresDir>/) && endsWith(/feature.json) — mirrors the legacy
82
+ * isGuardedPath exactly (does NOT require a single-segment middle; that
83
+ * constraint lives only in code-correlation, below). */
84
+ function matchFeatureJson(path, featuresDir) {
85
+ if (typeof path !== 'string' || !featuresDir) return false;
86
+ const prefix = featuresDir.replace(/\/$/, '') + '/';
87
+ if (!path.startsWith(prefix)) return false;
88
+ return path.endsWith('/feature.json');
89
+ }
90
+
91
+ /** Anything under docs/judgment/ (records or projections). Fixed root — matches
92
+ * lib/judgment-gen.js, which hardcodes docs/judgment/. */
93
+ function matchJudgment(path) {
94
+ return typeof path === 'string' && path.startsWith('docs/judgment/');
95
+ }
96
+
97
+ /**
98
+ * Prefix match on a directory, requiring the separator so that a sibling with
99
+ * a longer name (`canon-grants-backup/`) cannot masquerade as a child.
100
+ */
101
+ function matchUnder(dir) {
102
+ return (path) => typeof path === 'string' && path.startsWith(`${dir}/`);
103
+ }
104
+
105
+ // ── The registry ─────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * @typedef {object} CanonEntry
109
+ * @property {string} id — stable identifier
110
+ * @property {string} display — human-readable path pattern, for status/help output
111
+ * @property {string} writer — the module that legitimately produces this path
112
+ * @property {string[]} tools — typed tools authorised to write it
113
+ * @property {Array<'ship'|'hook'|'pre-commit'>} enforcedBy — points that guard it
114
+ * @property {boolean} [overrideEligible] — may a canon override be granted FOR
115
+ * this path? Absent means yes. Set false for the override's own governance
116
+ * state, which must be guarded without being grantable.
117
+ * @property {(path:string, featuresDir:string)=>boolean} matches
118
+ */
119
+
120
+ /** @type {CanonEntry[]} */
121
+ const REGISTRY = [
122
+ {
123
+ id: 'roadmap',
124
+ display: 'ROADMAP.md',
125
+ writer: 'lib/roadmap-gen.js',
126
+ tools: TOOLS_FOR_ROADMAP,
127
+ enforcedBy: ['ship'],
128
+ matches: matchExact('ROADMAP.md'),
129
+ },
130
+ {
131
+ id: 'changelog',
132
+ display: 'CHANGELOG.md',
133
+ writer: 'lib/changelog-writer.js',
134
+ tools: TOOLS_FOR_CHANGELOG,
135
+ enforcedBy: ['ship'],
136
+ matches: matchExact('CHANGELOG.md'),
137
+ },
138
+ {
139
+ id: 'feature-json',
140
+ display: '<features>/*/feature.json',
141
+ writer: 'lib/feature-writer.js',
142
+ tools: TOOLS_FOR_FEATURE_JSON,
143
+ enforcedBy: ['ship'],
144
+ matches: (path, featuresDir) => matchFeatureJson(path, featuresDir),
145
+ },
146
+ {
147
+ id: 'judgment',
148
+ display: 'docs/judgment/**',
149
+ writer: 'lib/judgment-writer.js',
150
+ tools: JUDGMENT_WRITE_TOOLS,
151
+ enforcedBy: ['hook'],
152
+ matches: (path) => matchJudgment(path),
153
+ },
154
+
155
+ // ── Governance class (COMP-CANON-OVERRIDE S1) ──────────────────────────────
156
+ // The override's own state. Guarded like any canon, and additionally
157
+ // `overrideEligible: false` so the override cannot be turned on itself:
158
+ // without this, "hook-registered" and "grantable" are the same set, and an
159
+ // agent could grant a bypass FOR the bypass ledger and then rewrite it
160
+ // (gate round 2, finding 2). The grant directory is governance state for the
161
+ // same reason — unregistered, a raw-written token would be consumable with
162
+ // no ledger row at all (finding 1).
163
+ //
164
+ // Runtime-scoped, like every hook guarantee: `Bash` never reaches this.
165
+ {
166
+ id: 'override-ledger',
167
+ display: '.compose/canon-overrides.jsonl',
168
+ writer: 'lib/canon-override.js',
169
+ tools: TOOLS_FOR_OVERRIDE,
170
+ enforcedBy: ['hook'],
171
+ overrideEligible: false,
172
+ matches: matchExact('.compose/canon-overrides.jsonl'),
173
+ },
174
+ {
175
+ id: 'override-attest',
176
+ display: '.compose/canon-overrides-attest.json',
177
+ writer: 'lib/canon-override.js',
178
+ tools: TOOLS_FOR_OVERRIDE,
179
+ enforcedBy: ['hook'],
180
+ overrideEligible: false,
181
+ matches: matchExact('.compose/canon-overrides-attest.json'),
182
+ },
183
+ {
184
+ id: 'override-grants',
185
+ display: '.compose/data/canon-grants/**',
186
+ writer: 'lib/canon-override.js',
187
+ tools: TOOLS_FOR_OVERRIDE,
188
+ enforcedBy: ['hook'],
189
+ overrideEligible: false,
190
+ matches: matchUnder('.compose/data/canon-grants'),
191
+ },
192
+ ];
193
+
194
+ // ── Public API ───────────────────────────────────────────────────────────────
195
+
196
+ /**
197
+ * Resolve the registry entry guarding `path` at enforcement `point`, or null.
198
+ * Only entries whose `enforcedBy` includes the point are considered — this is
199
+ * the per-point-subset invariant.
200
+ *
201
+ * @param {string} path
202
+ * @param {{ featuresDir?: string, point: 'ship'|'hook'|'pre-commit' }} opts
203
+ * @returns {CanonEntry|null}
204
+ */
205
+ export function matchEntry(path, { featuresDir, point }) {
206
+ for (const entry of REGISTRY) {
207
+ if (!entry.enforcedBy.includes(point)) continue;
208
+ if (entry.matches(path, featuresDir)) return entry;
209
+ }
210
+ return null;
211
+ }
212
+
213
+ /** True if `path` is guarded at `point`. */
214
+ export function isGuarded(path, opts) {
215
+ return matchEntry(path, opts) !== null;
216
+ }
217
+
218
+ /**
219
+ * True if a canon override may be granted for `path` at `point`.
220
+ *
221
+ * Deliberately NOT the same predicate as `isGuarded`. Two paths are guarded
222
+ * but ungrantable:
223
+ * - governance state (`overrideEligible: false`) — else the override could
224
+ * authorise rewriting its own audit trail;
225
+ * - anything not guarded at THIS point — a grant for a ship-only path is
226
+ * meaningless, because the hook already allows it, and would write a
227
+ * misleading bypass row.
228
+ * An unguarded path is likewise ineligible: nothing is blocking it, so there
229
+ * is nothing to override (the lockout invariant).
230
+ */
231
+ /**
232
+ * Human-readable path patterns guarded at `point`, for status and help output.
233
+ * Derived rather than hand-written: a hardcoded string in `compose guard
234
+ * status` silently under-reported the guarded set the moment S1 widened it.
235
+ */
236
+ export function guardedDisplaysFor(point) {
237
+ return REGISTRY.filter((e) => e.enforcedBy.includes(point)).map((e) => e.display);
238
+ }
239
+
240
+ export function isOverrideEligible(path, opts) {
241
+ const entry = matchEntry(path, opts);
242
+ return entry !== null && entry.overrideEligible !== false;
243
+ }
244
+
245
+ /** The typed tools authorised to write `path` at `point`, or [] if unguarded. */
246
+ export function toolsForPath(path, opts) {
247
+ const entry = matchEntry(path, opts);
248
+ return entry ? [...entry.tools] : [];
249
+ }
250
+
251
+ /**
252
+ * The feature code for a feature.json path (single-segment middle), else null.
253
+ * Used for ship-scan code correlation so an event for feature A cannot bless a
254
+ * dirty edit to feature B's feature.json.
255
+ *
256
+ * @param {string} path
257
+ * @param {{ featuresDir: string }} opts
258
+ * @returns {string|null}
259
+ */
260
+ export function featureCodeForPath(path, { featuresDir }) {
261
+ if (typeof path !== 'string' || !featuresDir) return null;
262
+ const prefix = featuresDir.replace(/\/$/, '') + '/';
263
+ if (!path.startsWith(prefix) || !path.endsWith('/feature.json')) return null;
264
+ const middle = path.slice(prefix.length, -'/feature.json'.length);
265
+ if (!middle || middle.includes('/')) return null;
266
+ return middle;
267
+ }
268
+
269
+ /**
270
+ * The registry as plain data — `{ id, display, tools }` per entry, deep-copied.
271
+ *
272
+ * Exists so COMP-COVERAGE-GATE can cross-check `entry.tools` against the tool
273
+ * inventory without reaching into `_internals` (which is test-only) and without
274
+ * being able to mutate the live registry.
275
+ */
276
+ export function canonEntries() {
277
+ return REGISTRY.map((e) => ({ id: e.id, display: e.display, tools: [...e.tools] }));
278
+ }
279
+
280
+ /** The entry ids guarded at `point` (for the contract test + introspection). */
281
+ export function guardedPatternIdsFor(point) {
282
+ return REGISTRY.filter((e) => e.enforcedBy.includes(point)).map((e) => e.id);
283
+ }
284
+
285
+ export const _internals = {
286
+ REGISTRY,
287
+ TOOLS_FOR_ROADMAP,
288
+ TOOLS_FOR_CHANGELOG,
289
+ TOOLS_FOR_FEATURE_JSON,
290
+ JUDGMENT_WRITE_TOOLS,
291
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * cli-commands.js — COMP-AUDIT-13: the single source of truth for compose's
3
+ * top-level command surface.
4
+ *
5
+ * `compose --help` (bin/compose.js) and the Command Index in docs/cli.md are
6
+ * both rendered from COMMANDS here, so installed capability can never again be
7
+ * invisible unless a command is missing from this table — and a drift-guard test
8
+ * (test/cli-commands.test.js) asserts this table matches the real dispatch
9
+ * branches in bin/compose.js in both directions.
10
+ *
11
+ * To add a command: add its `cmd === '<name>'` branch in bin/compose.js AND a
12
+ * row here. The test fails until both exist.
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} Command
17
+ * @property {string} name Canonical command token (matches `cmd === '<name>'`)
18
+ * @property {string[]} aliases Alternate tokens that dispatch the same command
19
+ * @property {string} group Display group (must be one of COMMAND_GROUPS)
20
+ * @property {string} summary One-line description (help + docs index)
21
+ */
22
+
23
+ /** Ordered display groups. */
24
+ export const COMMAND_GROUPS = [
25
+ 'Getting started',
26
+ 'Features & roadmap',
27
+ 'Build & implement',
28
+ 'Lifecycle, gates & review',
29
+ 'Vision, ideas & tracking',
30
+ 'App, integrations & runtime',
31
+ 'Maintenance & info',
32
+ ];
33
+
34
+ /** @type {Command[]} */
35
+ export const COMMANDS = [
36
+ // Getting started
37
+ { name: 'init', aliases: [], group: 'Getting started', summary: 'Initialize Compose in the current project' },
38
+ { name: 'setup', aliases: ['sync'], group: 'Getting started', summary: 'Install/sync global Compose skills' },
39
+ { name: 'install', aliases: [], group: 'Getting started', summary: 'Legacy bootstrap — runs init + setup' },
40
+ { name: 'import', aliases: [], group: 'Getting started', summary: 'Scan an existing project and generate a structured analysis' },
41
+ { name: 'doctor', aliases: [], group: 'Getting started', summary: 'Check external skill dependencies' },
42
+ { name: 'update', aliases: ['upgrade'], group: 'Getting started', summary: 'Pull latest compose, reinstall deps, refresh global skill' },
43
+
44
+ // Features & roadmap
45
+ { name: 'new', aliases: [], group: 'Features & roadmap', summary: 'Kickoff a product (research, brainstorm, roadmap, scaffold)' },
46
+ { name: 'feature', aliases: [], group: 'Features & roadmap', summary: 'Add a single feature (folder, design seed, ROADMAP entry)' },
47
+ { name: 'roadmap', aliases: [], group: 'Features & roadmap', summary: 'Show roadmap status; generate/migrate/check ROADMAP.md' },
48
+ { name: 'triage', aliases: [], group: 'Features & roadmap', summary: 'Analyze a feature and recommend a build profile' },
49
+ { name: 'qa-scope', aliases: [], group: 'Features & roadmap', summary: "Show affected routes from a feature's changed files" },
50
+
51
+ // Build & implement
52
+ { name: 'build', aliases: [], group: 'Build & implement', summary: 'Run a feature through the headless lifecycle' },
53
+ { name: 'fix', aliases: [], group: 'Build & implement', summary: 'Run a bug through the headless bug-fix lifecycle' },
54
+ { name: 'plan', aliases: [], group: 'Build & implement', summary: 'Plan work into a structured roadmap from a prompt' },
55
+ { name: 'gsd', aliases: [], group: 'Build & implement', summary: 'Per-task fresh-context dispatch from a blueprint + Boundary Map' },
56
+ { name: 'pipeline', aliases: [], group: 'Build & implement', summary: 'View and edit the build pipeline' },
57
+ { name: 'experiment', aliases: [], group: 'Build & implement', summary: 'Run an A/B model experiment from a spec' },
58
+
59
+ // Lifecycle, gates & review
60
+ { name: 'gates', aliases: ['gate'], group: 'Lifecycle, gates & review', summary: 'List and resolve pending gates' },
61
+ { name: 'loops', aliases: [], group: 'Lifecycle, gates & review', summary: 'Manage iteration loops for a feature' },
62
+ { name: 'guard', aliases: [], group: 'Lifecycle, gates & review', summary: 'Manage the canon guard and drift detection' },
63
+ { name: 'validate', aliases: [], group: 'Lifecycle, gates & review', summary: 'Validate feature/project artifacts against contracts' },
64
+ { name: 'record-completion', aliases: [], group: 'Lifecycle, gates & review', summary: 'Record a completion bound to a commit SHA (flips status to COMPLETE)' },
65
+ { name: 'lineage', aliases: [], group: 'Lifecycle, gates & review', summary: 'PROV-O artifact lineage: stamp | stale | show' },
66
+ { name: 'context', aliases: [], group: 'Lifecycle, gates & review', summary: 'Show the build decision log' },
67
+
68
+ // Vision, ideas & tracking
69
+ { name: 'items', aliases: [], group: 'Vision, ideas & tracking', summary: 'List vision items from local state (no server)' },
70
+ { name: 'ideabox', aliases: [], group: 'Vision, ideas & tracking', summary: 'Capture, review, and promote product ideas' },
71
+ { name: 'judgment', aliases: [], group: 'Vision, ideas & tracking', summary: "Judgment records: trace a position's causal ancestry" },
72
+ { name: 'metrics', aliases: [], group: 'Vision, ideas & tracking', summary: 'Report dispatch, settlement, and triage metrics' },
73
+ { name: 'tracker', aliases: [], group: 'Vision, ideas & tracking', summary: 'Tracker provider status and op-log sync' },
74
+
75
+ // App, integrations & runtime
76
+ { name: 'start', aliases: [], group: 'App, integrations & runtime', summary: 'Start the compose app (UI + API) for this project' },
77
+ { name: 'remote', aliases: [], group: 'App, integrations & runtime', summary: 'Manage remote access: pair, list, revoke, status' },
78
+ { name: 'smartmemory', aliases: [], group: 'App, integrations & runtime', summary: 'Sync feature-events/journal/artifacts into SmartMemory' },
79
+
80
+ // Maintenance & info
81
+ { name: 'migrate-state', aliases: [], group: 'Maintenance & info', summary: 'Run pending feature.json state migrations' },
82
+ { name: 'migrate-anon', aliases: [], group: 'Maintenance & info', summary: 'Promote anonymous ROADMAP rows to typed features (interactive)' },
83
+ { name: 'hooks', aliases: [], group: 'Maintenance & info', summary: 'Manage Claude Code hooks (install | uninstall | status)' },
84
+ { name: 'version', aliases: ['--version', '-V'], group: 'Maintenance & info', summary: 'Print compose version, git SHA, and install root' },
85
+ ];
86
+
87
+ /** Every token (names + aliases) that should dispatch to a command. */
88
+ export function allCommandTokens() {
89
+ return COMMANDS.flatMap((c) => [c.name, ...c.aliases]);
90
+ }
91
+
92
+ /** A command's display label: `name` plus any aliases, e.g. `setup, sync`. */
93
+ function labelOf(cmd) {
94
+ return cmd.aliases.length ? `${cmd.name}, ${cmd.aliases.join(', ')}` : cmd.name;
95
+ }
96
+
97
+ /**
98
+ * Render the grouped `compose --help` body from COMMANDS.
99
+ * @returns {string}
100
+ */
101
+ export function renderHelp() {
102
+ const lines = ['Usage: compose <command>', ''];
103
+ const width = Math.max(...COMMANDS.map((c) => labelOf(c).length));
104
+ for (const group of COMMAND_GROUPS) {
105
+ const inGroup = COMMANDS.filter((c) => c.group === group);
106
+ if (!inGroup.length) continue;
107
+ lines.push(`${group}:`);
108
+ for (const cmd of inGroup) {
109
+ lines.push(` ${labelOf(cmd).padEnd(width)} ${cmd.summary}`);
110
+ }
111
+ lines.push('');
112
+ }
113
+ lines.push('Run `compose <command> --help` (or see docs/cli.md) for command details.');
114
+ return lines.join('\n');
115
+ }
116
+
117
+ /**
118
+ * Render the docs/cli.md Command Index — a complete, grouped table generated
119
+ * from COMMANDS. The completeness test in test/cli-commands.test.js asserts
120
+ * every command appears in cli.md, so this table cannot silently omit one.
121
+ * @returns {string}
122
+ */
123
+ export function renderCommandIndex() {
124
+ const out = [
125
+ '## Command Index',
126
+ '',
127
+ '<!-- Generated from lib/cli-commands.js (COMP-AUDIT-13). Every shipped command appears here. -->',
128
+ '',
129
+ ];
130
+ for (const group of COMMAND_GROUPS) {
131
+ const inGroup = COMMANDS.filter((c) => c.group === group);
132
+ if (!inGroup.length) continue;
133
+ out.push(`### ${group}`, '', '| Command | Summary |', '|---|---|');
134
+ for (const cmd of inGroup) {
135
+ const label = cmd.aliases.length
136
+ ? `\`compose ${cmd.name}\` (alias: ${cmd.aliases.map((a) => `\`${a}\``).join(', ')})`
137
+ : `\`compose ${cmd.name}\``;
138
+ // Escape pipes so a summary like "stamp | stale | show" can't break the table.
139
+ out.push(`| ${label} | ${cmd.summary.replace(/\|/g, '\\|')} |`);
140
+ }
141
+ out.push('');
142
+ }
143
+ return out.join('\n');
144
+ }
@@ -21,10 +21,12 @@
21
21
  import { execSync } from 'node:child_process';
22
22
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
- import { homedir } from 'node:os';
24
+ import { homedir, tmpdir } from 'node:os';
25
25
 
26
26
  export const PROBE_SENTINEL = 'COMPOSE_CODEX_PROBE_OK';
27
- const WORKTREE_BASE = join(homedir(), '.stratum', 'worktrees');
27
+ const WORKTREE_BASE = process.env.NODE_TEST_CONTEXT
28
+ ? join(tmpdir(), 'compose-test-worktrees', 'codex-preflight')
29
+ : join(homedir(), '.stratum', 'worktrees');
28
30
  const PROBE_CACHE = 'codex-worktree-probe.json';
29
31
  // Bound the probe's agent run so a wedged Codex CLI can't hang the whole build
30
32
  // before the flow even starts. On timeout we reject → the finally cleanup runs.
@@ -63,13 +65,25 @@ function isGitRepo(cwd) {
63
65
  *
64
66
  * @param {object} args
65
67
  * @param {string} args.cwd repo working directory
68
+ * @param {string} args.projectCwd project root used for dispatch telemetry
69
+ * @param {string} args.buildId current Compose build identifier
70
+ * @param {string} args.featureCode current feature code
66
71
  * @param {object} args.stratum stratum client (uses runAgentText('codex', ...))
67
72
  * @param {string} args.dataDir .compose/data dir for the per-repo cache
68
73
  * @param {string} args.ts caller-supplied timestamp string (unique worktree name)
69
74
  * @param {boolean} [args.force] ignore the cache and re-probe
70
75
  * @returns {Promise<{ok: boolean, reason: string, cached?: boolean, skipped?: boolean}>}
71
76
  */
72
- export async function preflightCodexWorktreeProbe({ cwd, stratum, dataDir, ts, force = false }) {
77
+ export async function preflightCodexWorktreeProbe({
78
+ cwd,
79
+ projectCwd,
80
+ buildId,
81
+ featureCode,
82
+ stratum,
83
+ dataDir,
84
+ ts,
85
+ force = false,
86
+ }) {
73
87
  if (process.env.COMPOSE_SKIP_CODEX_PROBE) {
74
88
  return { ok: true, skipped: true, reason: 'COMPOSE_SKIP_CODEX_PROBE set — probe skipped' };
75
89
  }
@@ -101,21 +115,40 @@ export async function preflightCodexWorktreeProbe({ cwd, stratum, dataDir, ts, f
101
115
  // Unique per-run filename so a file committed in the repo (carried into the
102
116
  // detached worktree from HEAD) can never make the probe false-pass.
103
117
  const probeName = `codex-probe-${ts}.txt`;
118
+ let terminationUnconfirmed = false;
104
119
  try {
105
120
  const prompt =
106
121
  `Write a file named ${probeName} in the current working directory whose exact ` +
107
122
  `contents are the single line: ${PROBE_SENTINEL}\n` +
108
123
  `Do nothing else. This is an environment write-probe.`;
109
124
  try {
110
- // Bound the agent run a hang here would otherwise stall the build and skip
111
- // the finally cleanup. On timeout the race rejects and we fall to the catch.
112
- await Promise.race([
113
- stratum.runAgentText('codex', prompt, { cwd: wtPath }),
114
- new Promise((_, reject) =>
115
- setTimeout(() => reject(new Error(`probe timed out after ${PROBE_AGENT_TIMEOUT_MS}ms`)), PROBE_AGENT_TIMEOUT_MS).unref?.()
116
- ),
117
- ]);
125
+ // Wait for acknowledged termination before removing the worktree. A
126
+ // Promise.race timeout could delete it while the agent was still writing.
127
+ const controller = new AbortController();
128
+ const timer = setTimeout(() => controller.abort(), PROBE_AGENT_TIMEOUT_MS);
129
+ timer.unref?.();
130
+ try {
131
+ await stratum.runAgentText('codex', prompt, {
132
+ cwd: wtPath,
133
+ sandboxMode: 'workspace-write',
134
+ signal: controller.signal,
135
+ telemetry: {
136
+ site: 'preflight',
137
+ project_cwd: projectCwd,
138
+ build_id: buildId,
139
+ feature_code: featureCode,
140
+ },
141
+ });
142
+ } finally {
143
+ clearTimeout(timer);
144
+ }
118
145
  } catch (err) {
146
+ if (['CANCELLATION_UNCONFIRMED', 'CANCELLATION_TEARDOWN_TIMEOUT'].includes(err?.code)) {
147
+ terminationUnconfirmed = true;
148
+ err.worktreePath = wtPath;
149
+ err.message += `; probe worktree retained at ${wtPath}`;
150
+ throw err;
151
+ }
119
152
  const result = { ok: false, reason: `Codex agent run failed/timed out inside the worktree: ${err?.message ?? err}` };
120
153
  writeProbeCache(dataDir, result);
121
154
  return result;
@@ -131,10 +164,12 @@ export async function preflightCodexWorktreeProbe({ cwd, stratum, dataDir, ts, f
131
164
  writeProbeCache(dataDir, result);
132
165
  return result;
133
166
  } finally {
134
- try {
135
- execSync(`git worktree remove "${wtPath}" --force`, { cwd, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe' });
136
- } catch {
137
- try { execSync(`rm -rf "${wtPath}"`, { encoding: 'utf-8', timeout: 10_000 }); } catch { /* give up */ }
167
+ if (!terminationUnconfirmed) {
168
+ try {
169
+ execSync(`git worktree remove "${wtPath}" --force`, { cwd, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe' });
170
+ } catch {
171
+ try { execSync(`rm -rf "${wtPath}"`, { encoding: 'utf-8', timeout: 10_000 }); } catch { /* give up */ }
172
+ }
138
173
  }
139
174
  }
140
175
  }