@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,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { resolve, join } from 'node:path';
6
+
7
+ const [runId, featureCode, projectArg] = process.argv.slice(2);
8
+ if (!runId || !featureCode) {
9
+ console.error('Usage: node scripts/cost-census.mjs <runId> <featureCode> [projectCwd]');
10
+ process.exitCode = 2;
11
+ } else {
12
+ const projectCwd = resolve(projectArg ?? process.cwd());
13
+ const flowPath = join(homedir(), '.stratum', 'ts', 'flows', `${runId}.json`);
14
+ const accumulatorPath = join(
15
+ projectCwd,
16
+ '.compose',
17
+ 'data',
18
+ 'build-accumulator',
19
+ `${featureCode}.json`,
20
+ );
21
+ const flow = JSON.parse(readFileSync(flowPath, 'utf8'));
22
+ // The live accumulator is cleared when a build completes or aborts
23
+ // (lib/build.js clearBuildAccumulator); after that the terminal totals live in
24
+ // build-history.jsonl. Prefer the live file, fall back to the last history row.
25
+ let accumulator;
26
+ let accumulatorSource;
27
+ if (existsSync(accumulatorPath)) {
28
+ accumulator = JSON.parse(readFileSync(accumulatorPath, 'utf8'));
29
+ accumulatorSource = 'build-accumulator (live)';
30
+ } else {
31
+ const historyPath = join(projectCwd, '.compose', 'data', 'build-history.jsonl');
32
+ const rows = existsSync(historyPath)
33
+ ? readFileSync(historyPath, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line))
34
+ : [];
35
+ const last = rows.filter((row) => row?.feature_code === featureCode).at(-1);
36
+ if (!last) {
37
+ console.error(`no build accumulator or build-history row for ${featureCode} under ${projectCwd}`);
38
+ process.exit(2);
39
+ }
40
+ accumulator = last;
41
+ accumulatorSource = 'build-history.jsonl (terminal)';
42
+ }
43
+ console.log(`accumulator.source=${accumulatorSource}`);
44
+ const receipts = Array.isArray(flow.receipts) ? flow.receipts : [];
45
+ const costReceipts = receipts.filter((receipt) => receipt?.source !== 'engine');
46
+ const clientReceipts = costReceipts.filter(
47
+ (receipt) => !receipt?.dispatchId?.startsWith('legacy:') && receipt?.source !== 'server_dispatch',
48
+ );
49
+ const sumTokens = (rows) => rows.reduce(
50
+ (sum, receipt) => sum + (Number(receipt?.amount?.tokens) || 0),
51
+ 0,
52
+ );
53
+ const sourceCounts = costReceipts.reduce((counts, receipt) => {
54
+ const source = typeof receipt?.source === 'string' && receipt.source.length > 0
55
+ ? receipt.source
56
+ : 'unknown';
57
+ counts[source] = (counts[source] ?? 0) + 1;
58
+ return counts;
59
+ }, {});
60
+ const allHaveModel = receipts.every(
61
+ (receipt) => typeof receipt?.telemetry?.model === 'string' && receipt.telemetry.model.length > 0,
62
+ );
63
+
64
+ console.log(`receipts.tokens.client_sources=${sumTokens(clientReceipts)}`);
65
+ console.log(`receipts.tokens.all_sources=${sumTokens(costReceipts)}`);
66
+ console.log(`flowSpent.tokens=${Number(flow?.flowSpent?.tokens) || 0}`);
67
+ console.log(`accumulator.tokens_total=${Number(accumulator?.tokens_total) || 0}`);
68
+ console.log(`receipts.counts_by_source=${JSON.stringify(sourceCounts)}`);
69
+ console.log(`every_receipt_has_model=${allHaveModel}`);
70
+ }
@@ -0,0 +1,62 @@
1
+ #!/bin/sh
2
+ # compose-guard-sign — COMP-GUARD-ONE-TAP root-owned signer.
3
+ #
4
+ # Installed by `compose guard enrol` to /Library/Compose/guard/sign, owner
5
+ # root:wheel, mode 0755, and named by absolute path in
6
+ # /etc/sudoers.d/compose-guard with `timestamp_timeout=0`. Compose invokes it as
7
+ # `sudo -k /Library/Compose/guard/sign <namespace>` so every invocation
8
+ # re-authenticates (Touch ID via pam_tid, or the account password) and leaves
9
+ # no cached credential behind: one approval, exactly one signature.
10
+ #
11
+ # Usage: sign <namespace> < message > armored-signature
12
+ #
13
+ # The private key lives at /Library/Compose/guard/private/signing-key (root,
14
+ # 0600, in a root 0700 directory; the public half sits readable one level up at
15
+ # /Library/Compose/guard/signing-key.pub) and is never readable by the
16
+ # operator's user. This
17
+ # script pins PATH, calls every binary by absolute path, stages stdin under a
18
+ # root-private directory (never the caller's TMPDIR), signs with
19
+ # `ssh-keygen -Y sign`, prints the armored signature, and removes the staging
20
+ # directory on every exit path. The namespace is the only argument and is
21
+ # validated so it cannot smuggle options into ssh-keygen.
22
+ #
23
+ # Test seam: when NOT running as root, COMPOSE_GUARD_SIGN_KEY may point at an
24
+ # alternative key so the sshsig output can be verified in-suite without sudo,
25
+ # and staging falls back to TMPDIR. As root both are ignored — root's
26
+ # environment is not a caller input (and sudo's env_reset strips them anyway).
27
+
28
+ set -eu
29
+ umask 077
30
+ PATH=/usr/bin:/bin
31
+ export PATH
32
+
33
+ GUARD_DIR=/Library/Compose/guard
34
+ KEY="$GUARD_DIR/private/signing-key"
35
+ STAGE_ROOT="$GUARD_DIR/private/tmp"
36
+
37
+ NAMESPACE="${1:-}"
38
+ case "$NAMESPACE" in
39
+ '' ) echo "compose-guard-sign: usage: sign <namespace>" >&2; exit 64 ;;
40
+ *[!a-z0-9-]* ) echo "compose-guard-sign: invalid namespace" >&2; exit 64 ;;
41
+ esac
42
+ case "$NAMESPACE" in
43
+ [a-z]* ) ;;
44
+ * ) echo "compose-guard-sign: invalid namespace" >&2; exit 64 ;;
45
+ esac
46
+ if [ "${#NAMESPACE}" -gt 64 ]; then echo "compose-guard-sign: invalid namespace" >&2; exit 64; fi
47
+
48
+ if [ "$(/usr/bin/id -u)" -ne 0 ]; then
49
+ # Test seam only. Root ignores both.
50
+ if [ -n "${COMPOSE_GUARD_SIGN_KEY:-}" ]; then KEY="$COMPOSE_GUARD_SIGN_KEY"; fi
51
+ STAGE_ROOT="${TMPDIR:-/tmp}"
52
+ fi
53
+ [ -r "$KEY" ] || { echo "compose-guard-sign: signing key not readable: $KEY" >&2; exit 66; }
54
+ [ -d "$STAGE_ROOT" ] || { echo "compose-guard-sign: staging directory missing: $STAGE_ROOT" >&2; exit 66; }
55
+
56
+ WORK="$(/usr/bin/mktemp -d "$STAGE_ROOT/sign.XXXXXX")"
57
+ trap '/bin/rm -rf "$WORK"' EXIT INT TERM HUP
58
+
59
+ /bin/cat > "$WORK/message"
60
+ # -q: no chatter on stderr; the signature is written next to the message.
61
+ /usr/bin/ssh-keygen -Y sign -q -f "$KEY" -n "$NAMESPACE" "$WORK/message"
62
+ /bin/cat "$WORK/message.sig"
@@ -36,6 +36,7 @@ export class HealthMonitor {
36
36
  #silenceKillMs;
37
37
  #defaultTimeoutMs;
38
38
  #memoryLimitMB;
39
+ #suspended = false;
39
40
 
40
41
  /** @type {Map<string, TrackedAgent>} */
41
42
  #agents = new Map();
@@ -91,6 +92,7 @@ export class HealthMonitor {
91
92
  // Start silence timers
92
93
  this._resetSilenceTimers(agentId);
93
94
 
95
+ if (this.#suspended) return;
94
96
  // Wall-clock timeout
95
97
  entry.wallClockTimer = setTimeout(() => this._kill(agentId, 'wall_clock_timeout'), this.#defaultTimeoutMs);
96
98
 
@@ -117,6 +119,8 @@ export class HealthMonitor {
117
119
  return this.#agents.has(agentId);
118
120
  }
119
121
 
122
+ get hasActiveProcesses() { return this.#agents.size > 0; }
123
+
120
124
  getTerminalReason(agentId) {
121
125
  return this.#agents.get(agentId)?.terminalReason ?? null;
122
126
  }
@@ -126,6 +130,23 @@ export class HealthMonitor {
126
130
  if (entry) entry.terminalReason = reason;
127
131
  }
128
132
 
133
+ suspend() {
134
+ this.#suspended = true;
135
+ for (const entry of this.#agents.values()) this._clearTimers(entry);
136
+ }
137
+
138
+ resume() {
139
+ if (!this.#suspended) return;
140
+ this.#suspended = false;
141
+ for (const [id, entry] of this.#agents) {
142
+ if (entry.terminalReason) continue;
143
+ this._resetSilenceTimers(id);
144
+ entry.wallClockTimer = setTimeout(() => this._kill(id, 'wall_clock_timeout'),
145
+ Math.max(0, this.#defaultTimeoutMs - (Date.now() - entry.startedAt)));
146
+ if (this.#memoryLimitMB > 0) entry.memoryPollTimer = setInterval(() => this._checkMemory(id), MEMORY_POLL_INTERVAL_MS);
147
+ }
148
+ }
149
+
129
150
  /** Clean up all tracked agents and timers. */
130
151
  destroy() {
131
152
  for (const agentId of [...this.#agents.keys()]) {
@@ -136,6 +157,7 @@ export class HealthMonitor {
136
157
  // ── Internal ────────────────────────────────────────────────────────────
137
158
 
138
159
  _resetSilenceTimers(agentId) {
160
+ if (this.#suspended) return;
139
161
  const entry = this.#agents.get(agentId);
140
162
  if (!entry) return;
141
163
 
@@ -9,6 +9,8 @@
9
9
  * Terminal.jsx and vision-server.js).
10
10
  */
11
11
 
12
+ import { getTargetRoot, withProjectContext } from './project-root.js';
13
+ import { deriveId } from '../lib/discover-workspaces.js';
12
14
  import { resolvePort } from '../lib/resolve-port.js';
13
15
 
14
16
  const API_SERVER = `http://127.0.0.1:${resolvePort()}`;
@@ -26,7 +28,11 @@ async function post(path, body) {
26
28
  try {
27
29
  const res = await fetch(`${API_SERVER}${path}`, {
28
30
  method: 'POST',
29
- headers: { 'Content-Type': 'application/json' },
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ 'x-compose-workspace-id': deriveId({ root: getTargetRoot() }).id,
34
+ ...(process.env.COMPOSE_API_TOKEN ? { 'x-compose-token': process.env.COMPOSE_API_TOKEN } : {}),
35
+ },
30
36
  body: JSON.stringify(body),
31
37
  signal: AbortSignal.timeout(5000),
32
38
  });
@@ -102,3 +108,10 @@ export const HOOK_OPTIONS = {
102
108
  SessionStart: [{ hooks: [sessionStartHook] }],
103
109
  SessionEnd: [{ hooks: [sessionEndHook] }],
104
110
  };
111
+
112
+ /** SDK callbacks can be invoked outside the HTTP async context. Pin explicitly. */
113
+ export function hooksForProject(binding) {
114
+ return Object.fromEntries(Object.entries(HOOK_OPTIONS).map(([event, groups]) => [event,
115
+ groups.map(group => ({ ...group, hooks: group.hooks.map(hook => (...args) => withProjectContext(binding, () => hook(...args))) })),
116
+ ]));
117
+ }
@@ -1,249 +1,15 @@
1
- /**
2
- * Agent Server (Tier 1 — immortal, port 3003 during migration, then 3002)
3
- *
4
- * Replaces terminal-server.js. Uses @anthropic-ai/claude-agent-sdk instead of
5
- * node-pty + tmux. Streams typed SDK messages via SSE to the browser.
6
- * User input arrives via POST — SSE is the right transport for a unidirectional
7
- * async iterator.
8
- *
9
- * Process isolation: this server only talks to api-server (4001) via SDK hooks.
10
- * SessionManager and VisionServer require zero changes.
11
- */
12
-
1
+ /** Immortal SDK server. Workspace sessions remain pinned across UI switches. */
13
2
  import http from 'node:http';
14
- import { readFileSync } from 'node:fs';
15
- import path from 'node:path';
16
3
  import express from 'express';
17
4
  import cors from 'cors';
18
5
  import { query } from '@anthropic-ai/claude-agent-sdk';
19
- import { requireSensitiveToken } from './security.js';
20
- import { HOOK_OPTIONS } from './agent-hooks.js';
21
- import { getTargetRoot, getDataDir } from './project-root.js';
22
- import { BuildStreamBridge } from './build-stream-bridge.js';
23
- import { CoalescingBuffer } from './coalescing-buffer.js';
24
-
25
- const _agentBuffer = new CoalescingBuffer((flushed) => {
26
- if (flushed.agentMessage) {
27
- for (const msg of flushed.agentMessage) {
28
- _trackMessage(msg);
29
- broadcast(msg);
30
- }
31
- }
32
- }, { intervalMs: 16 });
33
- _agentBuffer.register('agentMessage', 'append');
34
-
35
- const _recentMessages = [];
36
- const HYDRATE_LIMIT = 50;
37
-
38
- function _trackMessage(msg) {
39
- _recentMessages.push(msg);
40
- if (_recentMessages.length > HYDRATE_LIMIT) _recentMessages.shift();
41
- }
42
-
43
- export function getAgentSnapshot() {
44
- return _recentMessages.length > 0 ? [..._recentMessages] : null;
45
- }
6
+ import { createAgentApp } from './agent-workspace.js';
46
7
 
47
8
  const PORT = process.env.AGENT_PORT || 4002;
48
- const SETTINGS_FILE = path.join(getDataDir(), 'settings.json');
49
-
50
- function _readModelSetting() {
51
- try {
52
- const raw = readFileSync(SETTINGS_FILE, 'utf-8');
53
- return JSON.parse(raw)?.models?.interactive || null;
54
- } catch {
55
- return null;
56
- }
57
- }
58
-
9
+ const workspaceServer = createAgentApp({ query });
59
10
  const app = express();
60
11
  app.use(cors({ origin: /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/ }));
61
- app.use(express.json());
62
-
63
- app.get('/api/health', (_req, res) => res.json({ ok: true }));
64
-
65
- // ---------------------------------------------------------------------------
66
- // Session state — one active SDK session at a time
67
- // ---------------------------------------------------------------------------
68
-
69
- /**
70
- * Active session state:
71
- * id: string|null — SDK session_id captured from init message
72
- * queryIter: Query|null — SDK async iterator, has .interrupt() method
73
- */
74
- let _session = { id: null, queryIter: null };
75
-
76
- /** SSE clients waiting for messages */
77
- const _sseClients = new Set();
78
-
79
- function broadcast(msg) {
80
- const line = `data: ${JSON.stringify(msg)}\n\n`;
81
- for (const client of _sseClients) {
82
- try {
83
- client.write(line);
84
- } catch {
85
- _sseClients.delete(client);
86
- }
87
- }
88
- }
89
-
90
- // ---------------------------------------------------------------------------
91
- // GET /api/agent/stream — SSE subscription
92
- // ---------------------------------------------------------------------------
93
-
94
- app.get('/api/agent/stream', (req, res) => {
95
- res.setHeader('Content-Type', 'text/event-stream');
96
- res.setHeader('Cache-Control', 'no-cache');
97
- res.setHeader('Connection', 'keep-alive');
98
- res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering if proxied
99
- res.flushHeaders();
100
-
101
- _sseClients.add(res);
102
-
103
- const snapshot = getAgentSnapshot();
104
- if (snapshot) {
105
- res.write(`event: hydrate\ndata: ${JSON.stringify(snapshot)}\n\n`);
106
- }
107
-
108
- // Immediately tell the new client whether a session exists
109
- if (_session.id) {
110
- res.write(`data: ${JSON.stringify({
111
- type: 'system', subtype: 'connected', sessionId: _session.id,
112
- })}\n\n`);
113
- }
114
-
115
- req.on('close', () => _sseClients.delete(res));
116
- });
117
-
118
- // ---------------------------------------------------------------------------
119
- // POST /api/agent/session — create a fresh session
120
- // ---------------------------------------------------------------------------
121
-
122
- app.post('/api/agent/session', requireSensitiveToken, (req, res) => {
123
- const { prompt = '' } = req.body || {};
124
- if (!prompt.trim()) return res.status(400).json({ error: 'prompt is required' });
125
-
126
- _killCurrentSession();
127
-
128
- try {
129
- const q = _startQuery(prompt, null);
130
- _session = { id: null, queryIter: q };
131
- _consumeStream(q);
132
- res.json({ ok: true });
133
- } catch (err) {
134
- res.status(500).json({ error: err.message });
135
- }
136
- });
137
-
138
- // ---------------------------------------------------------------------------
139
- // POST /api/agent/message — send a follow-up message (resumes session)
140
- // ---------------------------------------------------------------------------
141
-
142
- app.post('/api/agent/message', requireSensitiveToken, (req, res) => {
143
- const { prompt } = req.body || {};
144
- if (!prompt?.trim()) return res.status(400).json({ error: 'prompt is required' });
145
-
146
- const resumeId = _session.id;
147
- _killCurrentSession();
148
-
149
- try {
150
- const q = _startQuery(prompt, resumeId);
151
- _session = { id: resumeId, queryIter: q };
152
- _consumeStream(q);
153
- res.json({ ok: true, resumeSessionId: resumeId });
154
- } catch (err) {
155
- res.status(500).json({ error: err.message });
156
- }
157
- });
158
-
159
- // ---------------------------------------------------------------------------
160
- // POST /api/agent/interrupt — interrupt current query
161
- // ---------------------------------------------------------------------------
162
-
163
- app.post('/api/agent/interrupt', requireSensitiveToken, (req, res) => {
164
- if (!_session.queryIter) {
165
- return res.status(404).json({ error: 'No active query' });
166
- }
167
- try {
168
- _session.queryIter.interrupt();
169
-
170
- // COMP-AGT-1: Escalation — if not resolved within 5s, force-kill the session
171
- const capturedIter = _session.queryIter;
172
- setTimeout(() => {
173
- if (_session.queryIter === capturedIter) {
174
- console.log('[agent-server] Interrupt escalation: killing session after 5s timeout');
175
- _killCurrentSession();
176
- }
177
- }, 5000);
178
-
179
- res.json({ ok: true });
180
- } catch (err) {
181
- res.status(500).json({ error: err.message });
182
- }
183
- });
184
-
185
- // ---------------------------------------------------------------------------
186
- // GET /api/agent/session/status
187
- // ---------------------------------------------------------------------------
188
-
189
- app.get('/api/agent/session/status', (_req, res) => {
190
- res.json({
191
- active: !!_session.queryIter,
192
- sessionId: _session.id || null,
193
- });
194
- });
195
-
196
- // ---------------------------------------------------------------------------
197
- // Internals
198
- // ---------------------------------------------------------------------------
199
-
200
- function _buildOptions(prompt, resumeId) {
201
- return {
202
- cwd: getTargetRoot(),
203
- model: _readModelSetting() || 'claude-sonnet-4-6',
204
- permissionMode: 'acceptEdits',
205
- settingSources: ['project'],
206
- tools: { type: 'preset', preset: 'claude_code' },
207
- hooks: HOOK_OPTIONS,
208
- ...(resumeId ? { resume: resumeId } : {}),
209
- };
210
- }
211
-
212
- function _startQuery(prompt, resumeId) {
213
- return query({ prompt, options: _buildOptions(prompt, resumeId) });
214
- }
215
-
216
- function _killCurrentSession() {
217
- if (_session.queryIter) {
218
- try { _session.queryIter.return(); } catch { /* ignore */ }
219
- }
220
- _session = { id: null, queryIter: null };
221
- _recentMessages.length = 0;
222
- }
223
-
224
- async function _consumeStream(q) {
225
- try {
226
- for await (const msg of q) {
227
- // Capture session_id from the init message so we can resume
228
- if (msg.type === 'system' && msg.subtype === 'init') {
229
- _session.id = msg.session_id;
230
- }
231
- _agentBuffer.put('agentMessage', msg);
232
- }
233
- } catch (err) {
234
- if (err?.name !== 'AbortError') {
235
- broadcast({ type: 'error', message: err.message || String(err) });
236
- }
237
- } finally {
238
- // Signal completion to clients regardless of success/error
239
- if (_session.queryIter === q) {
240
- _session.queryIter = null;
241
- }
242
- // Keep _recentMessages populated after natural session end so clients
243
- // reloading the tab post-turn can still hydrate recent history.
244
- // Ring is cleared only on force-kill / new-session start in _killCurrentSession.
245
- }
246
- }
12
+ app.use(workspaceServer.app);
247
13
 
248
14
  // ---------------------------------------------------------------------------
249
15
  // Server startup
@@ -252,23 +18,14 @@ async function _consumeStream(q) {
252
18
  let serverListening = false;
253
19
  const server = http.createServer(app);
254
20
 
255
- // Build stream bridge — tails JSONL from CLI build, rebroadcasts via SSE
256
- const _bridge = new BuildStreamBridge(
257
- path.join(getTargetRoot(), '.compose'),
258
- broadcast
259
- );
260
-
261
21
  server.listen(PORT, '127.0.0.1', () => {
262
22
  serverListening = true;
263
- _bridge.start();
264
23
  console.log(`Agent server running on http://127.0.0.1:${PORT}`);
265
24
  });
266
25
 
267
26
  function shutdown(sig) {
268
27
  console.log(`[agent-server] ${sig}, shutting down`);
269
- _agentBuffer.stop();
270
- _bridge.stop();
271
- _killCurrentSession();
28
+ workspaceServer.close();
272
29
  server.close();
273
30
  setTimeout(() => process.exit(0), 1000);
274
31
  }
@@ -11,7 +11,6 @@ import { getTargetRoot } from './project-root.js';
11
11
  import { gracefulKill } from './agent-health.js';
12
12
  import { resolveSpawnProfile } from './mcp-tool-policy.js';
13
13
 
14
- const PROJECT_ROOT = getTargetRoot();
15
14
 
16
15
  // ---------------------------------------------------------------------------
17
16
  // Route registration
@@ -35,7 +34,7 @@ function deriveAgentType(prompt) {
35
34
  return 'claude';
36
35
  }
37
36
 
38
- export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadcastMessage, requireSensitiveToken, registry, sessionManager, healthMonitor, worktreeGC }) {
37
+ export function attachAgentSpawnRoutes(app, { projectRoot = getTargetRoot(), broadcastMessage, requireSensitiveToken, registry, sessionManager, healthMonitor, worktreeGC }) {
39
38
  const _agents = new Map();
40
39
  // POST /api/agent/spawn — spawn a hidden Claude subagent
41
40
  app.post('/api/agent/spawn', requireSensitiveToken, (req, res) => {
@@ -48,7 +47,7 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
48
47
  return res.status(409).json({ error: `Agent ${agentId} already running` });
49
48
  }
50
49
 
51
- const cleanEnv = { ...process.env, NO_COLOR: '1' };
50
+ const cleanEnv = { ...process.env, COMPOSE_TARGET: projectRoot, NO_COLOR: '1' };
52
51
  delete cleanEnv.CLAUDECODE;
53
52
  // COMP-MCP-ENFORCE-1: inject a TRUSTED, spawn-time MCP profile the subagent
54
53
  // cannot rewrite (its compose-mcp child inherits this env). Only restrictive
@@ -217,7 +216,7 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
217
216
  const currentSessionId = sessionManager?.currentSession?.id ?? null;
218
217
  if (agentId === currentSessionId) {
219
218
  const agentPort = process.env.AGENT_PORT || 4002;
220
- const headers = { 'Content-Type': 'application/json' };
219
+ const headers = { 'Content-Type': 'application/json', 'x-compose-project-root': projectRoot };
221
220
  if (process.env.COMPOSE_API_TOKEN) headers['x-compose-token'] = process.env.COMPOSE_API_TOKEN;
222
221
  fetch(`http://127.0.0.1:${agentPort}/api/agent/interrupt`, {
223
222
  method: 'POST',