@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
@@ -23,10 +23,41 @@ export const SETUP_TOOLS = new Set([
23
23
  'set_workspace', 'get_workspace', 'bind_session', 'get_current_session',
24
24
  ]);
25
25
 
26
- /** Management/approval/completion tools an implementer context must not wield. */
26
+ /**
27
+ * Management/approval/completion tools an implementer context must not wield.
28
+ *
29
+ * COMP-COVERAGE-GATE (2026-08-24) added the last two, found by
30
+ * `checkAuthorizationCoverage`'s C4 check — mutating tools this list had never
31
+ * been asked about. Both postdate COMP-MCP-ENFORCE-1's charter ("cannot
32
+ * self-approve, self-complete, or mutate roadmap status") and no design ever
33
+ * ruled that an implementer may call them:
34
+ *
35
+ * - `canon_override_grant` — the escape hatch FROM canon enforcement was
36
+ * callable by the profile SUBJECT to it. COMP-CANON-OVERRIDE already reasoned
37
+ * that the override must not be grantable for its own governance state
38
+ * (`overrideEligible: false`); this is the same argument one level up, at the
39
+ * caller instead of the target. An implementer that hits a canon block must
40
+ * escalate, not self-authorise.
41
+ * - `roadmap_xref_push` — writes EXTERNAL trackers (github issues, sibling
42
+ * repos). An implementer session should not be reaching outside the repo.
43
+ *
44
+ * The eight `judgment_*` writers were the other eight C4 findings and are
45
+ * deliberately NOT here: COMP-JUDGMENT-WRITER/design.md:137 rules that they
46
+ * "stay implementer/orchestrator-only", so an implementer writing judgment is a
47
+ * recorded decision, not an omission. That ruling is now recorded machine-side
48
+ * in `C4_EXCEPTIONS` (lib/coverage-gate.js) so the gate stops re-raising it.
49
+ *
50
+ * Verified before adding the two: no pipeline spec, prompt template or server
51
+ * flow invokes either from an implementer-profile session.
52
+ */
27
53
  const IMPLEMENTER_DENY = [
28
54
  'approve_gate', 'complete_feature', 'kill_feature',
29
55
  'set_feature_status', 'add_roadmap_entry', 'record_completion', 'propose_followup',
56
+ // COMP-LIFECYCLE-BACKFILL: a completion is a management act, backfilled or not.
57
+ 'backfill_completion',
58
+ // ── added by COMP-COVERAGE-GATE C4 ──
59
+ 'canon_override_grant',
60
+ 'roadmap_xref_push',
30
61
  ];
31
62
 
32
63
  /** Reviewer (read-only) may call only these. */
@@ -36,7 +67,8 @@ const REVIEWER_ALLOW = [
36
67
  'get_pending_gates', 'get_changelog_entries', 'get_journal_entries', 'get_completions',
37
68
  'validate_feature', 'validate_project', 'roadmap_diff', 'get_roadmap', 'roadmap_graph_check', 'assess_feature_artifacts',
38
69
  'set_workspace', 'get_workspace', 'bind_session',
39
- 'get_judgment_state', // COMP-JUDGMENT-WRITER: read-only; the five judgment write tools stay reviewer-denied
70
+ 'get_judgment_state', // COMP-JUDGMENT-WRITER: read-only; the eight judgment write tools stay reviewer-denied
71
+ 'get_judgment_trace', // COMP-JUDGMENT-PRECEDENT: read-only ancestry walk
40
72
  ];
41
73
 
42
74
  export const PROFILE_POLICY = {
@@ -47,7 +79,7 @@ export const PROFILE_POLICY = {
47
79
 
48
80
  /** phase → management tools re-permitted for DENY-mode profiles in that phase. */
49
81
  export const PHASE_REFINEMENT = {
50
- ship: new Set(['complete_feature', 'record_completion']),
82
+ ship: new Set(['complete_feature', 'record_completion', 'backfill_completion']),
51
83
  };
52
84
 
53
85
  // Strictness ordering — a bind hint may only NARROW (raise strictness), never widen.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * model-tiers.js — Model tier routing for STRAT-TIER.
3
3
  *
4
- * Maps symbolic tier names to concrete Anthropic model IDs.
4
+ * Maps symbolic tier names to provider-specific model IDs.
5
5
  * Tiers let pipeline specs declare intent (critical / standard / fast)
6
6
  * without hard-coding model strings — the map here is the single source of truth.
7
7
  */
@@ -13,6 +13,21 @@ export const MODEL_TIERS = {
13
13
  fast: 'claude-haiku-4-5-20251001',
14
14
  };
15
15
 
16
+ export const CODEX_MODEL_TIERS = {
17
+ critical: 'gpt-6-astra',
18
+ standard: 'gpt-5.6-terra',
19
+ fast: 'gpt-5.3-codex-spark',
20
+ };
21
+
22
+ // C12: codex efforts follow the routing convention — `low` is for trivial
23
+ // mechanical work only, and the fast tier is a model choice, not a
24
+ // reasoning-quality choice. Fast runs the cheap model at routine effort.
25
+ const CODEX_TIER_THINKING = {
26
+ critical: { mode: null, effort: 'high' },
27
+ standard: { mode: null, effort: 'high' },
28
+ fast: { mode: null, effort: 'medium' },
29
+ };
30
+
16
31
  /**
17
32
  * Default thinking config per tier.
18
33
  * - Opus 4.7 / Sonnet 4.6 support adaptive thinking and the effort parameter.
@@ -32,9 +47,10 @@ export const TIER_THINKING = {
32
47
  * @param {string|null|undefined} tier
33
48
  * @returns {string|null} Model ID, or null if tier is unknown / not provided.
34
49
  */
35
- export function resolveTierModel(tier) {
50
+ export function resolveTierModel(tier, provider = 'claude') {
36
51
  if (!tier) return null;
37
- return MODEL_TIERS[tier] ?? null;
52
+ const models = provider === 'codex' ? CODEX_MODEL_TIERS : provider === 'claude' ? MODEL_TIERS : {};
53
+ return models[tier] ?? null;
38
54
  }
39
55
 
40
56
  /**
@@ -43,7 +59,8 @@ export function resolveTierModel(tier) {
43
59
  * @param {string|null|undefined} tier
44
60
  * @returns {{ mode: string, effort: string|null }|null}
45
61
  */
46
- export function resolveTierThinking(tier) {
62
+ export function resolveTierThinking(tier, provider = 'claude') {
47
63
  if (!tier) return null;
48
- return TIER_THINKING[tier] ?? null;
64
+ const thinking = provider === 'codex' ? CODEX_TIER_THINKING : provider === 'claude' ? TIER_THINKING : {};
65
+ return thinking[tier] ?? null;
49
66
  }
@@ -16,6 +16,7 @@ import { readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, mkdir
16
16
  import { join, basename, dirname } from 'node:path';
17
17
  import { randomUUID, createHash } from 'node:crypto';
18
18
  import YAML from 'yaml';
19
+ import { tsCompatibilityOf } from '../lib/pipeline-compat.js';
19
20
 
20
21
  /** sha-256 hex of a string (COMP-PIPE-EDIT-6 conflict-detection baseline). */
21
22
  function sha256(text) {
@@ -139,7 +140,12 @@ function extractSteps(parsed) {
139
140
  * The shipped specs store `metadata` as a leading `#` comment (or omit it), so
140
141
  * the metadata-based `loadTemplates` never surfaces them. The editor discovers
141
142
  * by filename instead.
142
- * @returns {Array<{ file, version, flows: string[] }>}
143
+ * COMP-PIPELINE-QUARANTINE: each entry also carries `tsCompatible` — false for a
144
+ * spec left on the retired v0.3 dialect, which the engine cannot run at all. The
145
+ * editor lists specs it cannot execute, so the flag is what lets the surface say
146
+ * so instead of presenting them as ordinary choices.
147
+ *
148
+ * @returns {Array<{ file, version, flows: string[], tsCompatible: boolean, tsIncompatibleReason: string|null }>}
143
149
  */
144
150
  function listSpecFiles(pipelinesDir) {
145
151
  let files;
@@ -151,14 +157,24 @@ function listSpecFiles(pipelinesDir) {
151
157
  const out = [];
152
158
  for (const file of files) {
153
159
  try {
154
- const parsed = YAML.parse(readFileSync(join(pipelinesDir, file), 'utf-8'));
160
+ const raw = readFileSync(join(pipelinesDir, file), 'utf-8');
161
+ const parsed = YAML.parse(raw);
162
+ const compat = tsCompatibilityOf(raw);
155
163
  out.push({
156
164
  file,
157
165
  version: parsed?.version ?? null,
158
166
  flows: flowNamesOf(parsed),
167
+ tsCompatible: compat.compatible,
168
+ tsIncompatibleReason: compat.reason,
159
169
  });
160
170
  } catch {
161
- out.push({ file, version: null, flows: [] });
171
+ out.push({
172
+ file,
173
+ version: null,
174
+ flows: [],
175
+ tsCompatible: false,
176
+ tsIncompatibleReason: 'spec is not parseable YAML',
177
+ });
162
178
  }
163
179
  }
164
180
  return out;
@@ -603,33 +619,27 @@ function createNewFlowNode(doc, model, name) {
603
619
  }
604
620
 
605
621
  // ---------------------------------------------------------------------------
606
- // In-memory draft state (single draft at a time)
622
+ // Workspace-local draft state (disk is authoritative)
607
623
  // ---------------------------------------------------------------------------
608
624
 
609
- let currentDraft = null;
610
-
611
625
  const DRAFT_FILE = 'pipeline-draft.json';
612
626
 
613
627
  function saveDraft(dataDir, draft) {
614
- currentDraft = draft;
615
628
  mkdirSync(dataDir, { recursive: true });
616
629
  writeFileSync(join(dataDir, DRAFT_FILE), JSON.stringify(draft, null, 2));
617
630
  }
618
631
 
619
632
  function loadDraft(dataDir) {
620
- if (currentDraft) return currentDraft;
621
633
  const p = join(dataDir, DRAFT_FILE);
622
634
  if (!existsSync(p)) return null;
623
635
  try {
624
- currentDraft = JSON.parse(readFileSync(p, 'utf-8'));
625
- return currentDraft;
636
+ return JSON.parse(readFileSync(p, 'utf-8'));
626
637
  } catch {
627
638
  return null;
628
639
  }
629
640
  }
630
641
 
631
642
  function clearDraft(dataDir) {
632
- currentDraft = null;
633
643
  const p = join(dataDir, DRAFT_FILE);
634
644
  try { if (existsSync(p)) unlinkSync(p); } catch { /* ignore */ }
635
645
  }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import path from 'node:path';
15
+ import { AsyncLocalStorage } from 'node:async_hooks';
15
16
  import fs from 'node:fs';
16
17
  import { fileURLToPath } from 'node:url';
17
18
  import { findProjectRoot } from './find-root.js';
@@ -42,15 +43,40 @@ let _targetRoot = (() => {
42
43
 
43
44
  let _dataDir = path.join(_targetRoot, '.compose', 'data');
44
45
  let _configCache = null;
46
+ const projectContext = new AsyncLocalStorage();
47
+
48
+ /** Pin a request and all of its asynchronous descendants to one workspace. */
49
+ export function withProjectContext(binding, fn) {
50
+ return projectContext.run(binding, fn);
51
+ }
52
+
53
+ /** Keep asynchronous work pinned even when its HTTP client disconnects. */
54
+ export async function trackProjectWork(operation) {
55
+ const binding = projectContext.getStore();
56
+ if (!binding) return operation();
57
+ binding.activeWork = (binding.activeWork ?? 0) + 1;
58
+ try { return await operation(); }
59
+ finally { binding.activeWork--; }
60
+ }
61
+
62
+ /** Validate a destination before changing the process default or its services. */
63
+ export function prepareProject(newRoot) {
64
+ const targetRoot = path.resolve(newRoot);
65
+ if (!fs.statSync(targetRoot).isDirectory()) throw new Error(`Project path is not a directory: ${targetRoot}`);
66
+ const dataDir = path.join(targetRoot, '.compose', 'data');
67
+ fs.mkdirSync(dataDir, { recursive: true });
68
+ const config = readProjectConfig(targetRoot, true);
69
+ return { targetRoot, dataDir, config };
70
+ }
45
71
 
46
72
  /** The target project being developed. */
47
- export function getTargetRoot() { return _targetRoot; }
73
+ export function getTargetRoot() { return projectContext.getStore()?.targetRoot ?? _targetRoot; }
48
74
 
49
75
  /** Data directory for Compose state. Lives in the target project. */
50
- export function getDataDir() { return _dataDir; }
76
+ export function getDataDir() { return projectContext.getStore()?.dataDir ?? _dataDir; }
51
77
 
52
78
  let _currentWorkspaceId = null;
53
- export function getCurrentWorkspaceId() { return _currentWorkspaceId; }
79
+ export function getCurrentWorkspaceId() { return projectContext.getStore()?.workspaceId ?? _currentWorkspaceId; }
54
80
  export function setCurrentWorkspaceId(id) { _currentWorkspaceId = id; }
55
81
 
56
82
  // ---------------------------------------------------------------------------
@@ -73,15 +99,12 @@ export function onProjectSwitch(fn) {
73
99
  * @returns {{ targetRoot: string, dataDir: string }}
74
100
  */
75
101
  export function switchProject(newRoot) {
76
- const resolved = path.resolve(newRoot);
77
- if (!fs.existsSync(resolved)) {
78
- throw new Error(`Project path does not exist: ${resolved}`);
79
- }
80
- _targetRoot = resolved;
81
- _dataDir = path.join(resolved, '.compose', 'data');
82
- _configCache = null;
83
- fs.mkdirSync(_dataDir, { recursive: true });
84
- console.log(`[project-root] Switched to: ${resolved}`);
102
+ const prepared = prepareProject(newRoot);
103
+ _targetRoot = prepared.targetRoot;
104
+ _dataDir = prepared.dataDir;
105
+ _configCache = prepared.config;
106
+ _currentWorkspaceId = null;
107
+ console.error(`[project-root] Switched to: ${_targetRoot}`);
85
108
  for (const fn of _switchListeners) {
86
109
  try { fn(_targetRoot, _dataDir); } catch (e) { console.error('[project-root] Switch listener error:', e.message); }
87
110
  }
@@ -107,18 +130,34 @@ function cloneConfig(obj) {
107
130
  return JSON.parse(JSON.stringify(obj));
108
131
  }
109
132
 
110
- export function loadProjectConfig() {
111
- if (_configCache) return cloneConfig(_configCache);
112
- const configPath = path.join(getTargetRoot(), '.compose', 'compose.json');
133
+ function readProjectConfig(root, strict = false) {
134
+ const file = path.join(root, '.compose', 'compose.json');
113
135
  try {
114
- _configCache = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
115
- return cloneConfig(_configCache);
116
- } catch {
117
- _configCache = DEFAULT_CONFIG;
136
+ const config = JSON.parse(fs.readFileSync(file, 'utf-8'));
137
+ if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('project config must be a JSON object');
138
+ return { ...config, capabilities: { ...DEFAULT_CONFIG.capabilities, ...config.capabilities } };
139
+ } catch (err) {
140
+ if (strict && err.code !== 'ENOENT') {
141
+ // C10: a malformed config is fatal on purpose — silently falling back to
142
+ // defaults switched the workspace into a configuration nobody wrote. Say
143
+ // which file is broken and what to do about it; a bare SyntaxError does
144
+ // neither. A MISSING file is still fine (defaults apply).
145
+ throw Object.assign(
146
+ new Error(`Invalid Compose config at ${file}: ${err.message}. Fix the file or delete it to fall back to defaults.`),
147
+ { code: 'InvalidProjectConfig', file, cause: err },
148
+ );
149
+ }
118
150
  return cloneConfig(DEFAULT_CONFIG);
119
151
  }
120
152
  }
121
153
 
154
+ export function loadProjectConfig() {
155
+ const context = projectContext.getStore();
156
+ if (context) return cloneConfig(context.config ?? readProjectConfig(context.targetRoot));
157
+ _configCache ??= readProjectConfig(_targetRoot);
158
+ return cloneConfig(_configCache);
159
+ }
160
+
122
161
  export function resolveProjectPath(key) {
123
162
  const config = loadProjectConfig();
124
163
  // resolvePathValue handles in-root / ../-escaping / absolute overrides.
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import http from 'node:http';
18
- import { loadProjectConfig } from './project-root.js';
18
+ import { loadProjectConfig, getTargetRoot } from './project-root.js';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
21
  // resolveComposeHost
@@ -83,6 +83,8 @@ export function attachAgentProxy(app, { agentPort }) {
83
83
  // Inject the server-side sensitive token; strip any client-sent credential
84
84
  delete headers['x-compose-token'];
85
85
  delete headers['authorization'];
86
+ // Only the resolved server workspace may choose the SDK cwd.
87
+ headers['x-compose-project-root'] = getTargetRoot();
86
88
  const apiToken = process.env.COMPOSE_API_TOKEN;
87
89
  if (apiToken) headers['x-compose-token'] = apiToken;
88
90
 
@@ -13,7 +13,13 @@ const cache = new Map();
13
13
  function load(schemaPath = DEFAULT_SCHEMA_PATH) {
14
14
  if (cache.has(schemaPath)) return cache.get(schemaPath);
15
15
  const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
16
- const ajv = new Ajv({ strict: false, allErrors: true });
16
+ // `$data: true` lets a schema pin one field to another's value at validation
17
+ // time (`{"const": {"$data": "2/policy_checksum"}}` in
18
+ // contracts/lifecycle-backfill.schema.json). Without the option Ajv treats the
19
+ // `$data` object as a LITERAL to compare against, so such a schema rejects
20
+ // every document — including the valid ones. Additive: no other schema here
21
+ // uses `$data`, and enabling it changes nothing for them.
22
+ const ajv = new Ajv({ strict: false, allErrors: true, $data: true });
17
23
  addFormats(ajv);
18
24
  ajv.addSchema(schema);
19
25
  const entry = { schema, ajv };
@@ -16,8 +16,6 @@ import { serializeSession, persistSession, readLastSession, readSessionsByFeatur
16
16
 
17
17
  import { getTargetRoot, getDataDir } from './project-root.js';
18
18
 
19
- const PROJECT_ROOT = getTargetRoot();
20
- const SESSIONS_FILE = path.join(getDataDir(), 'sessions.json');
21
19
 
22
20
  /** Tools whose events count toward the Haiku summary batch threshold */
23
21
  const SIGNIFICANT_TOOLS = new Set(['Write', 'Edit', 'Bash', 'NotebookEdit']);
@@ -26,7 +24,8 @@ const SIGNIFICANT_TOOLS = new Set(['Write', 'Edit', 'Bash', 'NotebookEdit']);
26
24
  const BATCH_SIZE = 4;
27
25
 
28
26
  export class SessionManager {
29
- constructor({ getFeaturePhase, featureRoot, sessionsFile } = {}) {
27
+ constructor({ getFeaturePhase, featureRoot, sessionsFile, projectRoot = getTargetRoot() } = {}) {
28
+ this._projectRoot = projectRoot;
30
29
  /** @type {object|null} Current active session */
31
30
  this.currentSession = null;
32
31
 
@@ -37,7 +36,7 @@ export class SessionManager {
37
36
  this._featureRoot = featureRoot || 'docs/features';
38
37
 
39
38
  /** @type {string} Path to sessions.json — injectable for tests */
40
- this._sessionsFile = sessionsFile || SESSIONS_FILE;
39
+ this._sessionsFile = sessionsFile || path.join(getDataDir(), 'sessions.json');
41
40
 
42
41
  /** @type {Array<{tool,filePath,input,itemIds,timestamp}>} Buffered significant events */
43
42
  this._pendingBatch = [];
@@ -332,8 +331,8 @@ export class SessionManager {
332
331
  const batch = this._pendingBatch.splice(0);
333
332
 
334
333
  try {
335
- const prompt = buildSummaryPrompt(batch, PROJECT_ROOT);
336
- const result = await summarize(prompt, { projectRoot: PROJECT_ROOT });
334
+ const prompt = buildSummaryPrompt(batch, this._projectRoot);
335
+ const result = await summarize(prompt, { projectRoot: this._projectRoot });
337
336
  if (result) {
338
337
  this._distributeSummary(result, batch);
339
338
  for (const fn of this._summaryListeners) {
@@ -245,7 +245,9 @@ export function attachSessionRoutes(app, { sessionManager, scheduleBroadcast, br
245
245
  session: sessionData || recentSessions[0] || null,
246
246
  lifecycle: item?.lifecycle ? {
247
247
  currentPhase: item.lifecycle.currentPhase,
248
- phaseHistory: (item.lifecycle.phaseHistory || []).map(h => ({ phase: h.phase, enteredAt: h.enteredAt, exitedAt: h.exitedAt })),
248
+ phaseHistory: (item.lifecycle.phaseHistory || []).map(h => ({
249
+ phase: h.phase, enteredAt: h.enteredAt, exitedAt: h.exitedAt, origin: h.origin,
250
+ })),
249
251
  artifacts: item.lifecycle.artifacts || {},
250
252
  pendingGate: item.lifecycle.pendingGate || null,
251
253
  } : null,
@@ -191,9 +191,12 @@ async function runMutation(args) {
191
191
  * @param {number} timeoutMs
192
192
  * @returns {Promise<{ stdout: string, stderr: string, code: number }>}
193
193
  */
194
- function spawnStratumStdin(args, inputJson, timeoutMs, bin) {
194
+ function spawnStratumStdin(args, inputJson, timeoutMs, bin, extraEnv) {
195
195
  return new Promise((resolve) => {
196
- const proc = _execFile(bin, args, { timeout: timeoutMs }, (err, out, err2) => {
196
+ const proc = _execFile(bin, args, {
197
+ timeout: timeoutMs,
198
+ ...(extraEnv ? { env: { ...process.env, ...extraEnv } } : {}),
199
+ }, (err, out, err2) => {
197
200
  resolve(_spawnResult(bin, err, out, err2));
198
201
  });
199
202
  // Same both-paths-settle-identically contract as spawnStratum.
@@ -219,8 +222,8 @@ function spawnStratumStdin(args, inputJson, timeoutMs, bin) {
219
222
  *
220
223
  * @returns {Promise<any>} parsed JSON result or { error }
221
224
  */
222
- async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS) {
223
- const result = await spawnStratumStdin(['guard', action], JSON.stringify(kwargs), timeoutMs, flowGateBin());
225
+ async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS, extraEnv) {
226
+ const result = await spawnStratumStdin(['guard', action], JSON.stringify(kwargs), timeoutMs, flowGateBin(), extraEnv);
224
227
 
225
228
  if (result.code === -1) {
226
229
  return { error: { code: 'TIMEOUT', message: 'Stratum guard timed out', detail: '' } };
@@ -230,12 +233,16 @@ async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS) {
230
233
  return { error: { code: 'SPAWN', message: result.stderr, detail: '' } };
231
234
  }
232
235
  if (result.code !== 0) {
233
- console.error('[stratum-client] guard error stderr:', result.stderr);
234
- try {
235
- return JSON.parse(result.stdout); // canonical { status:"error", ... }
236
- } catch {
237
- return { error: { code: 'UNKNOWN', message: 'Stratum guard failed', detail: '' } };
236
+ let canonical = null;
237
+ try { canonical = JSON.parse(result.stdout); } catch { /* not canonical */ }
238
+ // guard_not_found is a normal answer to a policy/history query (every
239
+ // never-registered feature returns it); logging it as an error spammed
240
+ // 363 lines from one `compose guard status` on 2026-09-06.
241
+ if (canonical?.error_type !== 'guard_not_found') {
242
+ console.error('[stratum-client] guard error stderr:', result.stderr);
238
243
  }
244
+ if (canonical) return canonical; // canonical { status:"error", ... }
245
+ return { error: { code: 'UNKNOWN', message: 'Stratum guard failed', detail: '' } };
239
246
  }
240
247
  try {
241
248
  return JSON.parse(result.stdout);
@@ -346,7 +353,7 @@ export async function guardRegister({ resourceId, graph, edgePredicates, initial
346
353
  * predicates verify server-side. A refusal is a normal result (status:"refused").
347
354
  * @returns {Promise<{status:string,verdict:object,ledger_ref:string,current_state:string}|ErrorResult>}
348
355
  */
349
- export async function guardTransition({ resourceId, fromState, toState, artifacts, modifiedFiles, idempotencyKey, resolvedBy }) {
356
+ export async function guardTransition({ resourceId, fromState, toState, artifacts, modifiedFiles, idempotencyKey, expectedPolicyChecksum, resolvedBy }) {
350
357
  return runGuard('transition', _compact({
351
358
  resource_id: resourceId,
352
359
  from_state: fromState,
@@ -354,6 +361,7 @@ export async function guardTransition({ resourceId, fromState, toState, artifact
354
361
  artifacts,
355
362
  modified_files: modifiedFiles,
356
363
  idempotency_key: idempotencyKey,
364
+ expected_policy_checksum: expectedPolicyChecksum,
357
365
  resolved_by: resolvedBy,
358
366
  }));
359
367
  }
@@ -382,3 +390,42 @@ export async function guardOverride({ resourceId, fromState, toState, overrideTo
382
390
  export async function guardHistory(resourceId) {
383
391
  return runGuard('history', { resource_id: resourceId }, QUERY_TIMEOUT_MS);
384
392
  }
393
+
394
+ /** Read an immutable guard policy for compatibility and upgrade decisions. */
395
+ export async function guardPolicy(resourceId) {
396
+ return runGuard('policy', { resource_id: resourceId }, QUERY_TIMEOUT_MS);
397
+ }
398
+
399
+ /** List registered guard resources, optionally filtered by resource_id prefix. */
400
+ export async function guardList({ prefix } = {}) {
401
+ return runGuard('list', prefix ? { prefix } : {}, QUERY_TIMEOUT_MS);
402
+ }
403
+
404
+ /** Apply a signed, server-owned guard upgrade descriptor. */
405
+ export async function guardApplyUpgrade({ resourceId, descriptorId, descriptorsPath }) {
406
+ return runGuard('apply-upgrade', {
407
+ resource_id: resourceId,
408
+ descriptor_id: descriptorId,
409
+ }, MUTATION_TIMEOUT_MS, {
410
+ STRATUM_GUARD_UPGRADE_DESCRIPTORS: descriptorsPath,
411
+ });
412
+ }
413
+
414
+ /** Inspect + verify a descriptor file through stratum's verifier (read-only). */
415
+ export async function guardDescriptors(descriptorsPath) {
416
+ return runGuard('descriptors', {}, MUTATION_TIMEOUT_MS, {
417
+ STRATUM_GUARD_UPGRADE_DESCRIPTORS: descriptorsPath,
418
+ });
419
+ }
420
+
421
+ /** Compute stratum's canonical transition payload digest without changing state. */
422
+ export async function guardDigest({ fromState, toState, artifacts, modifiedFiles, resolvedBy, policyChecksum }) {
423
+ return runGuard('digest', {
424
+ from_state: fromState,
425
+ to_state: toState,
426
+ artifacts,
427
+ modified_files: modifiedFiles,
428
+ resolved_by: resolvedBy,
429
+ policy_checksum: policyChecksum,
430
+ }, QUERY_TIMEOUT_MS);
431
+ }
@@ -77,6 +77,8 @@ export class StratumSync {
77
77
  start() {
78
78
  this.#stopped = false;
79
79
  this.#lastTickAt = Date.now();
80
+ // Idempotent: a second start() must not orphan the armed timer.
81
+ if (this.#pollTimer) return;
80
82
  this.#scheduleNext();
81
83
  }
82
84
 
@@ -241,10 +243,11 @@ export function attachStratumRoutes(app, { store, scheduleBroadcast, broadcastMe
241
243
  // eslint-disable-next-line no-unused-vars
242
244
  const { stratumViolations: _v, violatedAt: _va, ...existingEvidence } = item.evidence || {};
243
245
  const evidence = { ...existingEvidence, stratumTrace: trace, tracedAt: new Date().toISOString() };
246
+ // COMP-COMPLETION-GATE slice 3 (AC-11, path 8): an audit trace is NOT
247
+ // completion evidence. A flow reporting `complete` used to flip the item
248
+ // to complete here with no commit, no test attestation and no ledger
249
+ // entry. The trace is still stored; the status is left to the gate.
244
250
  const updates = { evidence };
245
- if (trace.status === 'complete' && item.status !== 'complete') {
246
- updates.status = 'complete';
247
- }
248
251
  const updatedItem = store.updateItem(req.params.itemId, updates);
249
252
  scheduleBroadcast();
250
253
 
@@ -10,7 +10,6 @@ import path from 'node:path';
10
10
 
11
11
  import { getTargetRoot } from './project-root.js';
12
12
 
13
- const PROJECT_ROOT = getTargetRoot();
14
13
 
15
14
  const DEFAULT_MODEL = process.env.SUMMARIZER_MODEL || 'haiku';
16
15
 
@@ -25,7 +24,7 @@ const DEFAULT_MODEL = process.env.SUMMARIZER_MODEL || 'haiku';
25
24
  * @param {string} [projectRoot]
26
25
  * @returns {string}
27
26
  */
28
- export function buildSummaryPrompt(batch, projectRoot = PROJECT_ROOT) {
27
+ export function buildSummaryPrompt(batch, projectRoot = getTargetRoot()) {
29
28
  const eventLines = batch.map(evt => {
30
29
  const itemLabel = evt.itemTitles.length > 0
31
30
  ? ` [${evt.itemTitles.join(', ')}]`
@@ -66,9 +65,9 @@ JSON schema:
66
65
  * @param {string} [opts.projectRoot]
67
66
  * @returns {Promise<object|null>}
68
67
  */
69
- export function summarize(prompt, { model = DEFAULT_MODEL, projectRoot = PROJECT_ROOT } = {}) {
68
+ export function summarize(prompt, { model = DEFAULT_MODEL, projectRoot = getTargetRoot() } = {}) {
70
69
  return new Promise((resolve) => {
71
- const cleanEnv = { ...process.env, NO_COLOR: '1' };
70
+ const cleanEnv = { ...process.env, COMPOSE_TARGET: projectRoot, NO_COLOR: '1' };
72
71
  delete cleanEnv.CLAUDECODE;
73
72
 
74
73
  const proc = spawn('claude', [
@@ -59,7 +59,6 @@ function ensureComposeApiToken() {
59
59
  // Expose the same token to Vite client code.
60
60
  process.env.VITE_COMPOSE_API_TOKEN = process.env.COMPOSE_API_TOKEN;
61
61
  // Expose AGENT_PORT so AgentStream.jsx can reach the right port
62
- process.env.VITE_AGENT_PORT = process.env.AGENT_PORT || '4002';
63
62
  }
64
63
 
65
64
  // --- Singleton enforcement ---