@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,294 @@
1
+ import { prepareProject, withProjectContext, getTargetRoot } from './project-root.js';
2
+ import path from 'node:path';
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
+ import express from 'express';
5
+ import { requireSensitiveToken } from './security.js';
6
+ import { hooksForProject } from './agent-hooks.js';
7
+ import { CoalescingBuffer } from './coalescing-buffer.js';
8
+ import { BuildStreamBridge } from './build-stream-bridge.js';
9
+ import { hasPersistedWork } from './workspace-activity.js';
10
+
11
+ /** One SDK session, message history and stream bridge per project. */
12
+ export function createAgentWorkspace(binding, { query }) {
13
+ const _agentBuffer = new CoalescingBuffer((flushed) => {
14
+ if (flushed.agentMessage) {
15
+ for (const msg of flushed.agentMessage) {
16
+ _trackMessage(msg);
17
+ broadcast(msg);
18
+ }
19
+ }
20
+ }, { intervalMs: 16 });
21
+ _agentBuffer.register('agentMessage', 'append');
22
+
23
+ const _recentMessages = [];
24
+ const HYDRATE_LIMIT = 50;
25
+
26
+ function _trackMessage(msg) {
27
+ _recentMessages.push(msg);
28
+ if (_recentMessages.length > HYDRATE_LIMIT) _recentMessages.shift();
29
+ }
30
+
31
+ function getAgentSnapshot() {
32
+ return _recentMessages.length > 0 ? [..._recentMessages] : null;
33
+ }
34
+
35
+ const SETTINGS_FILE = path.join(binding.dataDir, 'settings.json');
36
+
37
+ function _readModelSetting() {
38
+ try {
39
+ const raw = readFileSync(SETTINGS_FILE, 'utf-8');
40
+ return JSON.parse(raw)?.models?.interactive || null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ const app = express.Router();
47
+
48
+ // C12: /api/health is registered once, on the app in createAgentApp. A copy
49
+ // here was unreachable (the app-level route answers first) and would have
50
+ // made liveness depend on resolving a workspace.
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Session state — one active SDK session at a time
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Active session state:
58
+ * id: string|null — SDK session_id captured from init message
59
+ * queryIter: Query|null — SDK async iterator, has .interrupt() method
60
+ */
61
+ let _session = { id: null, queryIter: null };
62
+ const _queries = new Set();
63
+
64
+ /** SSE clients waiting for messages */
65
+ const _sseClients = new Set();
66
+
67
+ function broadcast(msg) {
68
+ const line = `data: ${JSON.stringify(msg)}\n\n`;
69
+ for (const client of _sseClients) {
70
+ try {
71
+ client.write(line);
72
+ } catch {
73
+ _sseClients.delete(client);
74
+ }
75
+ }
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // GET /api/agent/stream — SSE subscription
80
+ // ---------------------------------------------------------------------------
81
+
82
+ app.get('/api/agent/stream', (req, res) => {
83
+ res.setHeader('Content-Type', 'text/event-stream');
84
+ res.setHeader('Cache-Control', 'no-cache');
85
+ res.setHeader('Connection', 'keep-alive');
86
+ res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering if proxied
87
+ res.flushHeaders();
88
+
89
+ _sseClients.add(res);
90
+
91
+ const snapshot = getAgentSnapshot();
92
+ if (snapshot) {
93
+ res.write(`event: hydrate\ndata: ${JSON.stringify(snapshot)}\n\n`);
94
+ }
95
+
96
+ // Immediately tell the new client whether a session exists
97
+ if (_session.id) {
98
+ res.write(`data: ${JSON.stringify({
99
+ type: 'system', subtype: 'connected', sessionId: _session.id,
100
+ })}\n\n`);
101
+ }
102
+
103
+ req.on('close', () => _sseClients.delete(res));
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // POST /api/agent/session — create a fresh session
108
+ // ---------------------------------------------------------------------------
109
+
110
+ app.post('/api/agent/session', requireSensitiveToken, (req, res) => {
111
+ const { prompt = '' } = req.body || {};
112
+ if (!prompt.trim()) return res.status(400).json({ error: 'prompt is required' });
113
+
114
+ _killCurrentSession();
115
+
116
+ try {
117
+ const q = _startQuery(prompt, null);
118
+ _session = { id: null, queryIter: q };
119
+ _consumeStream(q);
120
+ res.json({ ok: true });
121
+ } catch (err) {
122
+ res.status(500).json({ error: err.message });
123
+ }
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // POST /api/agent/message — send a follow-up message (resumes session)
128
+ // ---------------------------------------------------------------------------
129
+
130
+ app.post('/api/agent/message', requireSensitiveToken, (req, res) => {
131
+ const { prompt } = req.body || {};
132
+ if (!prompt?.trim()) return res.status(400).json({ error: 'prompt is required' });
133
+
134
+ const resumeId = _session.id;
135
+ _killCurrentSession();
136
+
137
+ try {
138
+ const q = _startQuery(prompt, resumeId);
139
+ _session = { id: resumeId, queryIter: q };
140
+ _consumeStream(q);
141
+ res.json({ ok: true, resumeSessionId: resumeId });
142
+ } catch (err) {
143
+ res.status(500).json({ error: err.message });
144
+ }
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // POST /api/agent/interrupt — interrupt current query
149
+ // ---------------------------------------------------------------------------
150
+
151
+ app.post('/api/agent/interrupt', requireSensitiveToken, (req, res) => {
152
+ if (!_session.queryIter) {
153
+ return res.status(404).json({ error: 'No active query' });
154
+ }
155
+ try {
156
+ _session.queryIter.interrupt();
157
+
158
+ // COMP-AGT-1: Escalation — if not resolved within 5s, force-kill the session
159
+ const capturedIter = _session.queryIter;
160
+ setTimeout(() => {
161
+ if (_session.queryIter === capturedIter) {
162
+ console.log('[agent-server] Interrupt escalation: killing session after 5s timeout');
163
+ _killCurrentSession();
164
+ }
165
+ }, 5000);
166
+
167
+ res.json({ ok: true });
168
+ } catch (err) {
169
+ res.status(500).json({ error: err.message });
170
+ }
171
+ });
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // GET /api/agent/session/status
175
+ // ---------------------------------------------------------------------------
176
+
177
+ app.get('/api/agent/session/status', (_req, res) => {
178
+ res.json({
179
+ active: !!_session.queryIter,
180
+ sessionId: _session.id || null,
181
+ });
182
+ });
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Internals
186
+ // ---------------------------------------------------------------------------
187
+
188
+ function _buildOptions(prompt, resumeId) {
189
+ return {
190
+ cwd: binding.targetRoot,
191
+ env: { ...process.env, COMPOSE_TARGET: binding.targetRoot },
192
+ model: _readModelSetting() || 'claude-sonnet-4-6',
193
+ permissionMode: 'acceptEdits',
194
+ settingSources: ['project'],
195
+ tools: { type: 'preset', preset: 'claude_code' },
196
+ hooks: hooksForProject(binding),
197
+ ...(resumeId ? { resume: resumeId } : {}),
198
+ };
199
+ }
200
+
201
+ function _startQuery(prompt, resumeId) {
202
+ return query({ prompt, options: _buildOptions(prompt, resumeId) });
203
+ }
204
+
205
+ function _killCurrentSession() {
206
+ if (_session.queryIter) {
207
+ try { Promise.resolve(_session.queryIter.return()).catch(() => {}); } catch { /* ignore */ }
208
+ }
209
+ _session = { id: null, queryIter: null };
210
+ _recentMessages.length = 0;
211
+ }
212
+
213
+ async function _consumeStream(q) {
214
+ _queries.add(q);
215
+ try {
216
+ for await (const msg of q) {
217
+ // Capture session_id from the init message so we can resume
218
+ if (_session.queryIter !== q) break;
219
+ if (msg.type === 'system' && msg.subtype === 'init') {
220
+ _session.id = msg.session_id;
221
+ }
222
+ _agentBuffer.put('agentMessage', msg);
223
+ }
224
+ } catch (err) {
225
+ if (err?.name !== 'AbortError') {
226
+ broadcast({ type: 'error', message: err.message || String(err) });
227
+ }
228
+ } finally {
229
+ _queries.delete(q);
230
+ // Signal completion to clients regardless of success/error
231
+ if (_session.queryIter === q) {
232
+ _session.queryIter = null;
233
+ }
234
+ // Keep _recentMessages populated after natural session end so clients
235
+ // reloading the tab post-turn can still hydrate recent history.
236
+ // Ring is cleared only on force-kill / new-session start in _killCurrentSession.
237
+ }
238
+ }
239
+
240
+
241
+ const bridge = new BuildStreamBridge(path.join(binding.targetRoot, '.compose'), broadcast);
242
+ bridge.start();
243
+ return {
244
+ router: app,
245
+ getAgentSnapshot,
246
+ get busy() { return _queries.size > 0 || _sseClients.size > 0 || hasPersistedWork(binding.dataDir); },
247
+ close() {
248
+ _agentBuffer.stop(); bridge.stop(); _killCurrentSession();
249
+ for (const client of _sseClients) client.end();
250
+ _sseClients.clear();
251
+ },
252
+ };
253
+ }
254
+
255
+ /** Shared by the production process and executable-boundary integration probes. */
256
+ export function createAgentApp({ query, maxWorkspaces = 8 }) {
257
+ if (!Number.isInteger(maxWorkspaces) || maxWorkspaces < 1) throw new Error("maxWorkspaces must be a positive integer");
258
+ const app = express();
259
+ app.use(express.json());
260
+ app.get('/api/health', (_req, res) => res.json({ ok: true }));
261
+ const workspaces = new Map();
262
+ app.use((req, res, next) => {
263
+ const root = req.get('x-compose-project-root');
264
+ const dispatch = () => {
265
+ try {
266
+ const target = realpathSync(root || getTargetRoot());
267
+ // A token authorizes the proxy, not turning arbitrary directories into
268
+ // writable SDK workspaces. Header targets must already be configured.
269
+ if (root && target !== realpathSync(getTargetRoot())) {
270
+ const config = JSON.parse(readFileSync(path.join(target, '.compose', 'compose.json'), 'utf8'));
271
+ if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Target must be a configured Compose workspace');
272
+ }
273
+ const binding = prepareProject(target);
274
+ if (!workspaces.has(target) && workspaces.size >= maxWorkspaces) {
275
+ const idle = [...workspaces].find(([, workspace]) => !workspace.busy);
276
+ if (!idle) return res.status(409).json({ error: `Workspace capacity reached (${maxWorkspaces}); every retained workspace is busy`, code: 'WorkspaceCapacityExceeded' });
277
+ idle[1].close(); workspaces.delete(idle[0]);
278
+ }
279
+ let workspace = workspaces.get(binding.targetRoot);
280
+ if (!workspace) {
281
+ workspace = withProjectContext(binding, () => createAgentWorkspace(binding, { query }));
282
+ workspaces.set(binding.targetRoot, workspace);
283
+ }
284
+ // Map insertion order is the LRU order, including status/stream reads.
285
+ workspaces.delete(binding.targetRoot); workspaces.set(binding.targetRoot, workspace);
286
+ return withProjectContext(binding, () => workspace.router(req, res, next));
287
+ } catch (err) { return res.status(400).json({ error: err.message }); }
288
+ };
289
+ // The proxy selects cwd with a server-stamped root and sensitive credential.
290
+ if (root) return requireSensitiveToken(req, res, dispatch);
291
+ return dispatch();
292
+ });
293
+ return { app, close: () => { for (const workspace of workspaces.values()) workspace.close(); } };
294
+ }
@@ -20,7 +20,7 @@ import { runNew as defaultRunNew } from '../lib/new.js';
20
20
  import { runBuildAll as defaultRunBuildAll } from '../lib/build-all.js';
21
21
  import { runGsd as defaultRunGsd } from '../lib/gsd.js';
22
22
  import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
23
- import { getDataDir as defaultGetDataDir, getTargetRoot as defaultGetTargetRoot } from './project-root.js';
23
+ import { getDataDir as defaultGetDataDir, getTargetRoot as defaultGetTargetRoot, trackProjectWork } from './project-root.js';
24
24
  import { readBuildHistory } from '../lib/build-history.js';
25
25
 
26
26
  // A launcher conflict (another build/GSD run already owns the feature) is a
@@ -32,11 +32,12 @@ function launcherErrorStatus(err) {
32
32
  }
33
33
 
34
34
  export function attachBuildRoutes(app, deps = {}) {
35
- const runBuild = deps.runBuild || defaultRunBuild;
35
+ const tracked = run => (...args) => trackProjectWork(() => run(...args));
36
+ const runBuild = tracked(deps.runBuild || defaultRunBuild);
36
37
  const abortBuild = deps.abortBuild || defaultAbortBuild;
37
- const runNew = deps.runNew || defaultRunNew;
38
- const runBuildAll = deps.runBuildAll || defaultRunBuildAll;
39
- const runGsd = deps.runGsd || defaultRunGsd;
38
+ const runNew = tracked(deps.runNew || defaultRunNew);
39
+ const runBuildAll = tracked(deps.runBuildAll || defaultRunBuildAll);
40
+ const runGsd = tracked(deps.runGsd || defaultRunGsd);
40
41
  const getDataDir = deps.getDataDir || defaultGetDataDir;
41
42
  // Bind build dispatch to the active project root (the same global target the
42
43
  // vision store + getDataDir() use). Without this the runners default to
@@ -9,6 +9,7 @@
9
9
  import { readFileSync, statSync, watch } from 'node:fs';
10
10
  import { join, dirname, basename } from 'node:path';
11
11
  import { existsSync } from 'node:fs';
12
+ import { emitDecisionEvent, buildPolicyViolationEvent } from './decision-event-emit.js';
12
13
 
13
14
  const JSONL_FILENAME = 'build-stream.jsonl';
14
15
  const DEFAULT_CRASH_TIMEOUT_MS = 300_000; // 5 min
@@ -16,6 +17,12 @@ const STALE_GATE_TIMEOUT_MS = 86_400_000; // 24h
16
17
  const DEBOUNCE_MS = 50;
17
18
  const POLL_INTERVAL_MS = 2000;
18
19
 
20
+ // COMP-AGENT-LANES: forward a producer-stamped lane envelope untouched; absent,
21
+ // contribute nothing so lane-less events keep their exact historical shape.
22
+ function laneOf(event) {
23
+ return event.lane && typeof event.lane === 'object' ? { lane: event.lane } : {};
24
+ }
25
+
19
26
  export class BuildStreamBridge {
20
27
  #filePath;
21
28
  #composeDir;
@@ -219,6 +226,23 @@ export class BuildStreamBridge {
219
226
  if (mapped) {
220
227
  this.#broadcast(mapped);
221
228
  }
229
+
230
+ // COMP-POLICY-CHECK-5: a policy match also lands on the feature-level
231
+ // decision timeline, alongside the step event, for live cockpit visibility.
232
+ if (event.type === 'policy_violation') {
233
+ try {
234
+ emitDecisionEvent(this.#broadcast, buildPolicyViolationEvent({
235
+ featureCode: event.featureCode ?? null,
236
+ buildId: event.buildId ?? null,
237
+ stepId: event.stepId,
238
+ rule: event.rule,
239
+ matched: event.matched,
240
+ suppressed: event.suppressed,
241
+ userMode: event.userMode,
242
+ timestamp: event._ts ? new Date(event._ts).toISOString() : undefined,
243
+ }));
244
+ } catch { /* timeline emit is best-effort — never break the tail loop */ }
245
+ }
222
246
  }
223
247
  }
224
248
 
@@ -304,6 +328,7 @@ export class BuildStreamBridge {
304
328
  flowId: event.flowId,
305
329
  ...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
306
330
  ...(event.parallel ? { parallel: true } : {}),
331
+ ...laneOf(event),
307
332
  _source: 'build',
308
333
  };
309
334
 
@@ -311,6 +336,7 @@ export class BuildStreamBridge {
311
336
  return {
312
337
  type: 'assistant',
313
338
  message: { content: [{ type: 'tool_use', name: event.tool, input: event.input }] },
339
+ ...laneOf(event),
314
340
  _source: 'build',
315
341
  };
316
342
 
@@ -318,6 +344,7 @@ export class BuildStreamBridge {
318
344
  return {
319
345
  type: 'assistant', subtype: 'tool_use_summary',
320
346
  summary: event.summary, output: event.output,
347
+ ...laneOf(event),
321
348
  _source: 'build',
322
349
  };
323
350
 
@@ -332,6 +359,7 @@ export class BuildStreamBridge {
332
359
  return {
333
360
  type: 'assistant',
334
361
  message: { content: [{ type: 'text', text: event.content }] },
362
+ ...laneOf(event),
335
363
  _source: 'build',
336
364
  };
337
365
 
@@ -348,6 +376,11 @@ export class BuildStreamBridge {
348
376
  cumulative_cost_usd: event.cumulative_cost_usd ?? 0,
349
377
  ...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
350
378
  ...(event.parallel ? { parallel: true } : {}),
379
+ // COMP-AGENT-LANES: explicit terminal status — the UI must not infer
380
+ // "complete" from the done event's existence.
381
+ ...(event.status ? { status: event.status } : {}),
382
+ ...(event.outcome ? { outcome: event.outcome } : {}),
383
+ ...laneOf(event),
351
384
  _source: 'build',
352
385
  };
353
386
 
@@ -374,6 +407,13 @@ export class BuildStreamBridge {
374
407
  return {
375
408
  type: 'error',
376
409
  message: event.message, source: 'build',
410
+ // COMP-AGENT-LANES: stepId + lane let the cockpit scope a failure to
411
+ // its worker lane (C5 — dropped here before, so lane-scoped failure
412
+ // could never fire). laneTerminal distinguishes a lane-closing error
413
+ // from an advisory one — the reducer closes only on laneTerminal.
414
+ ...(event.stepId ? { stepId: event.stepId } : {}),
415
+ ...(event.laneTerminal === true ? { laneTerminal: true } : {}),
416
+ ...laneOf(event),
377
417
  _source: 'build',
378
418
  };
379
419
 
@@ -505,6 +545,19 @@ export class BuildStreamBridge {
505
545
  _source: 'build',
506
546
  };
507
547
 
548
+ // COMP-POLICY-CHECK-5: pre-response policy check pattern matches
549
+ case 'policy_violation':
550
+ return {
551
+ type: 'system', subtype: 'policy_violation',
552
+ stepId: event.stepId,
553
+ rule: event.rule,
554
+ matched: event.matched,
555
+ suppressed: Boolean(event.suppressed),
556
+ detail: event.detail,
557
+ userMode: event.userMode,
558
+ _source: 'build',
559
+ };
560
+
508
561
  default:
509
562
  return null; // unknown event type — skip
510
563
  }
@@ -273,7 +273,9 @@ export class CCSessionWatcher {
273
273
  }
274
274
 
275
275
  start() {
276
- if (this._watcher) return;
276
+ // C5: the fs.watch fallback leaves `_watcher` null, so guarding on it alone
277
+ // let every resume() start ANOTHER poll interval — one leaked per switch.
278
+ if (this._watcher || this._pollTimer) return;
277
279
  if (!fs.existsSync(this.projectsRoot)) {
278
280
  fs.mkdirSync(this.projectsRoot, { recursive: true });
279
281
  }
@@ -296,6 +298,7 @@ export class CCSessionWatcher {
296
298
  }
297
299
 
298
300
  _startPolling(intervalMs = 2000) {
301
+ if (this._pollTimer) return;
299
302
  this._pollTimer = setInterval(async () => {
300
303
  const files = listJsonlFiles(this.projectsRoot);
301
304
  for (const f of files) {
@@ -5,10 +5,16 @@ export class CoalescingBuffer {
5
5
  #pending = new Map();
6
6
  #timer = null;
7
7
  #flushFn;
8
+ #intervalMs;
8
9
 
9
10
  constructor(flushFn, { intervalMs = 16 } = {}) {
10
11
  this.#flushFn = flushFn;
11
- this.#timer = setInterval(() => this.#flush(), intervalMs);
12
+ this.#intervalMs = intervalMs;
13
+ this.start();
14
+ }
15
+
16
+ start() {
17
+ this.#timer ??= setInterval(() => this.#flush(), this.#intervalMs);
12
18
  }
13
19
 
14
20
  register(key, mode) {