@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
package/server/index.js CHANGED
@@ -3,16 +3,10 @@ import cors from 'cors';
3
3
  import http from 'node:http';
4
4
  import path from 'node:path';
5
5
  import { existsSync, statSync, readFileSync } from 'node:fs';
6
- import { FileWatcherServer } from './file-watcher.js';
7
- import { VisionStore } from './vision-store.js';
8
- import { VisionServer } from './vision-server.js';
9
- import { SessionManager } from './session-manager.js';
10
- import { scanFeatures, seedFeatures, scanSubPackages, seedSubPackages } from './feature-scan.js';
11
- import { attachGraphExportRoutes } from './graph-export.js';
6
+ import { WorkspaceRuntime } from './workspace-runtime.js';
12
7
  import { attachWorkspaceRoutes } from './workspace-routes.js';
13
- import { attachGraphLayoutRoutes } from './graph-layout-routes.js';
14
8
  import { createWorkspaceMiddleware } from './workspace-middleware.js';
15
- import { getTargetRoot, getDataDir, ensureDataDir, loadProjectConfig, resolveProjectPath, switchProject, COMPOSE_HOME } from './project-root.js';
9
+ import { getTargetRoot, getDataDir, ensureDataDir, loadProjectConfig, withProjectContext, COMPOSE_HOME } from './project-root.js';
16
10
  import { resolveStratumEngine } from './stratum-client.js';
17
11
  import { probeStratumBin, resolveStratumBin } from '../lib/stratum-engine.js';
18
12
  import { createAuthStore } from './auth-store.js';
@@ -64,21 +58,44 @@ if (projectConfig.capabilities.stratum) {
64
58
  }
65
59
 
66
60
  // Handle unexpected errors — fatal startup errors exit (supervisor retries),
67
- // runtime errors keep the process alive to preserve PTY sessions
61
+ // runtime errors keep the process alive to preserve PTY sessions.
62
+ //
63
+ // stdio death is fatal by design: when the parent process dies, stdout/stderr
64
+ // become dead sockets and every console.* write throws EPIPE. Logging from the
65
+ // uncaughtException handler then throws again, which re-enters the handler via
66
+ // the stream's error emission — an infinite setImmediate loop pinning a core
67
+ // (observed 2026-09-01: 2 days at 100% CPU as an orphan). So: stream errors on
68
+ // stdio exit immediately, and every log in an error/signal path is guarded.
69
+ const isDeadStdio = (err) =>
70
+ err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED' || err.code === 'ERR_STREAM_WRITE_AFTER_END');
71
+ const safeLog = (...args) => {
72
+ try {
73
+ console.error(...args);
74
+ } catch {
75
+ process.exit(0); // stdio is gone — parent died, nothing left to serve logs to
76
+ }
77
+ };
78
+ process.stdout.on('error', (err) => {
79
+ if (isDeadStdio(err)) process.exit(0);
80
+ });
81
+ process.stderr.on('error', (err) => {
82
+ if (isDeadStdio(err)) process.exit(0);
83
+ });
68
84
  let serverListening = false;
69
85
  process.on('uncaughtException', (err) => {
86
+ if (isDeadStdio(err)) process.exit(0);
70
87
  if (!serverListening && err.code === 'EADDRINUSE') {
71
- console.error(`[compose] Port in use, exiting for supervisor retry: ${err.message}`);
88
+ safeLog(`[compose] Port in use, exiting for supervisor retry: ${err.message}`);
72
89
  process.exit(1);
73
90
  }
74
- console.error('[compose] Uncaught exception (process kept alive):', err.message);
75
- console.error(err.stack);
91
+ safeLog('[compose] Uncaught exception (process kept alive):', err.message);
92
+ safeLog(err.stack);
76
93
  });
77
94
  process.on('unhandledRejection', (reason) => {
78
- console.error('[compose] Unhandled rejection (process kept alive):', reason);
95
+ safeLog('[compose] Unhandled rejection (process kept alive):', reason);
79
96
  });
80
97
  process.on('SIGTERM', () => {
81
- console.log('[compose] SIGTERM received, shutting down gracefully');
98
+ safeLog('[compose] SIGTERM received, shutting down gracefully');
82
99
  process.exit(0);
83
100
  });
84
101
 
@@ -141,23 +158,21 @@ attachAuthRoutes(app, {
141
158
  } catch { return null; }
142
159
  },
143
160
  broadcast: (msg) => {
144
- if (typeof visionServer?.broadcastMessage === 'function') {
145
- visionServer.broadcastMessage(msg);
161
+ if (typeof workspaces?.active?.visionServer?.broadcastMessage === 'function') {
162
+ activeVision().broadcastMessage(msg);
146
163
  }
147
164
  },
148
165
  requireSensitive: requireSensitiveToken,
149
166
  });
150
167
 
151
168
  // ---------------------------------------------------------------------------
152
- // Agent proxy (BOTH modes additive) — mounted after the gate so gate clears
153
- // requests first. Proxy injects the real sensitive token server-side.
169
+ // Each workspace installs its agent proxy after workspace resolution. The
170
+ // proxy stamps the selected root and sensitive token for the SDK process.
154
171
  // ---------------------------------------------------------------------------
155
172
  const _agentPort = parseInt(process.env.AGENT_PORT || '4002', 10);
156
- attachAgentProxy(app, { agentPort: _agentPort });
157
173
 
158
174
  attachWorkspaceRoutes(app);
159
- attachGraphLayoutRoutes(app);
160
- app.use(createWorkspaceMiddleware());
175
+ app.use(createWorkspaceMiddleware({ resolveKnownWorkspace: (id) => workspaces.resolveKnownWorkspace(id) }));
161
176
 
162
177
  // `remote` lets clients (desktop cockpit served through a tunnel) detect
163
178
  // remote mode at boot and switch their WS/SSE URLs to token-carrying form.
@@ -174,81 +189,27 @@ app.get('/api/project', (_req, res) => {
174
189
  });
175
190
  });
176
191
 
192
+ const server = http.createServer(app);
193
+ const workspaces = new WorkspaceRuntime(server, { agentPort: _agentPort });
194
+ try { workspaces.switch(getTargetRoot(), projectConfig); }
195
+ catch (err) {
196
+ console.error(`[compose] Workspace startup failed: ${err.message}`);
197
+ process.exit(1);
198
+ }
199
+
200
+ // Accessors always select the current context; old requests retain their own router.
201
+ const activeVision = () => workspaces.active.visionServer;
177
202
  app.post('/api/project/switch', (req, res) => {
178
203
  const { path: projectPath } = req.body || {};
179
- if (!projectPath) return res.status(400).json({ error: 'path is required' });
204
+ if (typeof projectPath !== 'string' || !projectPath) return res.status(400).json({ error: 'path is required' });
180
205
  try {
181
- const result = switchProject(projectPath);
182
- ensureDataDir();
183
- // Reload store from new data directory
184
- visionStore.reloadFrom(result.dataDir);
185
- // Re-scan features and sub-packages from new project
186
- try {
187
- const features = scanFeatures();
188
- if (features.length > 0) seedFeatures(features, visionStore);
189
- const packages = scanSubPackages();
190
- if (packages.length > 0) seedSubPackages(packages, visionStore);
191
- } catch (err) {
192
- console.error('[compose] Feature scan after switch:', err.message);
193
- }
194
- // Broadcast new state to all connected clients
195
- visionServer.scheduleBroadcast();
196
- res.json({ ok: true, targetRoot: result.targetRoot, name: path.basename(result.targetRoot) });
206
+ const { binding } = workspaces.switch(projectPath);
207
+ res.json({ ok: true, targetRoot: binding.targetRoot, name: path.basename(binding.targetRoot) });
197
208
  } catch (err) {
198
209
  res.status(400).json({ error: err.message });
199
210
  }
200
211
  });
201
-
202
- const server = http.createServer(app);
203
- const fileWatcher = new FileWatcherServer();
204
- fileWatcher.attach(server, app);
205
-
206
- const visionStore = new VisionStore(getDataDir());
207
- const sessionManager = new SessionManager({
208
- getFeaturePhase: (featureCode) => {
209
- const item = visionStore.getItemByFeatureCode(featureCode);
210
- return item?.lifecycle?.currentPhase || null;
211
- },
212
- featureRoot: resolveProjectPath('features'),
213
- });
214
- const visionServer = new VisionServer(visionStore, sessionManager, { config: projectConfig });
215
- visionServer.attach(server, app);
216
-
217
- // Seed feature folders and sub-packages into vision store on startup
218
- try {
219
- const features = scanFeatures();
220
- if (features.length > 0) seedFeatures(features, visionStore);
221
- const packages = scanSubPackages();
222
- if (packages.length > 0) seedSubPackages(packages, visionStore);
223
- } catch (err) {
224
- console.error('[compose] Feature scan startup error:', err.message);
225
- }
226
-
227
- // Wire feature folder changes → auto-reseed vision store
228
- fileWatcher.onFeatureChanged = (_relativePath) => {
229
- try {
230
- const features = scanFeatures();
231
- seedFeatures(features, visionStore);
232
- visionServer.scheduleBroadcast();
233
- } catch (err) {
234
- console.error('[compose] Feature reseed error:', err.message);
235
- }
236
- };
237
-
238
- // Wire build state changes → broadcast over /ws/vision
239
- fileWatcher.onBuildStateChanged = (state) => {
240
- if (state) {
241
- // Broadcast flat payload per STRAT-COMP-4 contract
242
- visionServer.broadcastMessage({ type: 'buildState', ...state });
243
- }
244
- };
245
-
246
- // Wire pipelines/*.stratum.yaml external changes → broadcast `specChanged` on
247
- // the VISION WS (COMP-PIPE-EDIT-6 — the channel the pipeline editor store uses,
248
- // not /ws/files). The message already carries { type:'specChanged', file, path }.
249
- fileWatcher.onSpecChanged = (message) => {
250
- visionServer.broadcastMessage(message);
251
- };
212
+ app.use((req, res, next) => workspaces.handle(req, res, next));
252
213
 
253
214
  // ---------------------------------------------------------------------------
254
215
  // Static serving + SPA fallback (BOTH modes — additive)
@@ -282,18 +243,27 @@ server.on('upgrade', (req, socket, head) => {
282
243
  return;
283
244
  }
284
245
 
285
- const { pathname } = new URL(req.url, 'http://localhost');
286
- if (pathname === '/ws/files' && fileWatcher.wss) {
287
- fileWatcher.wss.handleUpgrade(req, socket, head, (ws) => {
288
- fileWatcher.wss.emit('connection', ws, req);
289
- });
290
- } else if (pathname === '/ws/vision' && visionServer.wss) {
291
- visionServer.wss.handleUpgrade(req, socket, head, (ws) => {
292
- visionServer.wss.emit('connection', ws, req);
293
- });
294
- } else {
295
- socket.destroy();
246
+ const url = new URL(req.url, 'http://localhost');
247
+ const workspaceId = req.headers['x-compose-workspace-id'] || url.searchParams.get('workspaceId');
248
+ let context = workspaces.active;
249
+ if (workspaceId) {
250
+ try {
251
+ const resolved = workspaces.resolveKnownWorkspace(workspaceId);
252
+ context = resolved && workspaces.get(resolved.root);
253
+ } catch { context = null; }
254
+ if (!context) {
255
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
256
+ socket.destroy();
257
+ return;
258
+ }
296
259
  }
260
+ const { fileWatcher, visionServer } = context;
261
+ const wss = url.pathname === '/ws/files' ? fileWatcher.wss
262
+ : url.pathname === '/ws/vision' ? visionServer.wss : null;
263
+ if (!wss) { socket.destroy(); return; }
264
+ withProjectContext(context.binding, () => {
265
+ wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
266
+ });
297
267
  });
298
268
 
299
269
  server.listen(PORT, _host, () => {
@@ -14,12 +14,20 @@
14
14
 
15
15
  import { createHash } from 'node:crypto';
16
16
  import { readFileSync } from 'node:fs';
17
- import { spawnSync } from 'node:child_process';
17
+ import { execFile, spawnSync } from 'node:child_process';
18
18
  import path from 'node:path';
19
+ import { promisify } from 'node:util';
20
+
21
+ import { descriptorIdFor, deriveBackfillPolicy, ensureSignedDescriptors } from '../lib/guard-descriptors.js';
22
+ import { acquireDirLock } from '../lib/dir-lock.js';
23
+ import { custodySign, DESCRIPTOR_LOCK_TIMEOUT_MS, HINT_APPROVE } from '../lib/guard-custody.js';
19
24
 
20
25
  import {
21
26
  guardRegister as _guardRegister,
22
27
  guardTransition as _guardTransition,
28
+ guardPolicy as _guardPolicy,
29
+ guardApplyUpgrade as _guardApplyUpgrade,
30
+ guardDescriptors as _guardDescriptors,
23
31
  } from './stratum-client.js';
24
32
  import { setFeatureStatus as _setFeatureStatus } from '../lib/feature-writer.js';
25
33
  import {
@@ -31,6 +39,8 @@ import {
31
39
  edgeEvidenceOf,
32
40
  } from '../lib/lifecycle-modes.js';
33
41
 
42
+ const execFileAsync = promisify(execFile);
43
+
34
44
  // ---------------------------------------------------------------------------
35
45
  // Canonical phase graph (compose-owned data — single source of truth)
36
46
  // ---------------------------------------------------------------------------
@@ -53,7 +63,12 @@ export const BASE_TRANSITIONS = {
53
63
  export const SKIPPABLE = new Set(['prd', 'architecture', 'report']);
54
64
 
55
65
  /** Terminal phases — no outgoing edges. */
56
- export const TERMINAL = new Set(['complete', 'killed']);
66
+ export const TERMINAL = new Set(['complete', 'killed', 'complete_backfilled']);
67
+
68
+ /** Normalise raw stratum and local adapter error envelopes at the boundary. */
69
+ export const isGuardError = (result) => !result || Boolean(result.error) || result.status === 'error';
70
+ export const guardErrorType = (result) => (result && (result.error?.code ?? result.error_type)) ?? null;
71
+ export const guardErrorMessage = (result) => (result && (result.error?.message ?? result.message)) ?? 'no guard response';
57
72
 
58
73
  /**
59
74
  * Assemble the FULL guarded graph the design requires: the forward
@@ -77,6 +92,13 @@ export function buildPhaseGraph(mode = 'build') {
77
92
  // separately in vision-routes at /lifecycle/complete, so absent from the mode's
78
93
  // forward transitions). For build this is `ship → complete`.
79
94
  graph[completable] = [...(graph[completable] || []), 'complete'];
95
+ // Every non-terminal phase → complete_backfilled. This must precede the
96
+ // killed loop: stratum fingerprints adjacency arrays in their stored order.
97
+ for (const s of nodes) {
98
+ if (terminal.has(s)) continue;
99
+ graph[s] = graph[s] || [];
100
+ if (!graph[s].includes('complete_backfilled')) graph[s].push('complete_backfilled');
101
+ }
80
102
  // Every non-terminal phase → killed (vision-routes /lifecycle/kill allows
81
103
  // kill from any non-terminal phase, including the completable phase).
82
104
  for (const s of nodes) {
@@ -141,6 +163,7 @@ export function resourceId(featureCode, workspaceRoot, mode = 'build') {
141
163
  * and stay set_feature_status's domain.
142
164
  */
143
165
  export function phaseToStatus(phase) {
166
+ if (phase === 'complete_backfilled') return 'COMPLETE';
144
167
  if (phase === 'complete') return 'COMPLETE';
145
168
  if (phase === 'killed') return 'KILLED';
146
169
  return 'IN_PROGRESS';
@@ -223,20 +246,110 @@ export async function verifyCompletionEvidence({ commitSha, cwd, testCommand, te
223
246
  return { ok: reasons.length === 0, reasons, testsAttested };
224
247
  }
225
248
 
249
+ /**
250
+ * Async equivalent for backfill completion while a directory lock heartbeat is
251
+ * active. The synchronous live-completion verifier above remains unchanged.
252
+ */
253
+ export async function verifyCompletionEvidenceAsync({ commitSha, cwd, testCommand, testsPassClaim }) {
254
+ const reasons = [];
255
+
256
+ if (!commitSha || typeof commitSha !== 'string' || !commitSha.trim()) {
257
+ reasons.push('commit_sha is required for evidence-bound completion');
258
+ } else {
259
+ try {
260
+ await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `${commitSha.trim()}^{commit}`], { cwd, encoding: 'utf8' });
261
+ } catch {
262
+ reasons.push(`commit ${commitSha.trim()} not found in repository (server-read git verification)`);
263
+ }
264
+ }
265
+
266
+ let testsAttested = false;
267
+ if (Array.isArray(testCommand) && testCommand.length > 0) {
268
+ const [bin, ...rest] = testCommand;
269
+ try {
270
+ await execFileAsync(bin, rest, { cwd, encoding: 'utf8' });
271
+ testsAttested = true;
272
+ } catch (error) {
273
+ if (error?.code === 'ENOENT') reasons.push(`test command failed to run: ${error.message}`);
274
+ else reasons.push(`test command exited ${error?.code ?? 'null'} (not 0)`);
275
+ }
276
+ } else if (testsPassClaim !== true) {
277
+ reasons.push('tests_pass must be explicitly true (no configured test command to attest test results)');
278
+ }
279
+
280
+ return { ok: reasons.length === 0, reasons, testsAttested };
281
+ }
282
+
226
283
  // ---------------------------------------------------------------------------
227
284
  // Guard client (injectable for tests)
228
285
  // ---------------------------------------------------------------------------
229
286
 
230
- let _client = { register: _guardRegister, transition: _guardTransition };
287
+ let _client = {
288
+ register: _guardRegister,
289
+ transition: _guardTransition,
290
+ policy: _guardPolicy,
291
+ applyUpgrade: _guardApplyUpgrade,
292
+ descriptors: _guardDescriptors,
293
+ };
231
294
  /** @internal test seam */
232
295
  export function _testOnly_setGuardClient(c) { _client = c; }
233
296
 
297
+ const _custody = { sign: custodySign };
298
+ let _ensureDescriptors = ensureSignedDescriptors;
299
+ /** @internal test seam: real enumeration bypasses the injectable guard client. */
300
+ export function _testOnly_setEnsureDescriptors(ensure) {
301
+ _ensureDescriptors = ensure ?? ensureSignedDescriptors;
302
+ }
303
+
234
304
  // Per-process registration cache — register is idempotent server-side, but the
235
305
  // cache avoids a subprocess per request once a resource is known-registered.
236
- const _registered = new Set();
306
+ const _registered = new Map();
237
307
  /** @internal test seam */
238
308
  export function _testOnly_resetGuardCache() { _registered.clear(); }
239
309
 
310
+ /** The four and only four fields covered by stratum's policy checksum. */
311
+ export function policyChecksumFields(policy) {
312
+ return {
313
+ graph: policy.graph,
314
+ edge_predicates: policy.edge_predicates,
315
+ terminal: policy.terminal,
316
+ stakes: policy.stakes,
317
+ };
318
+ }
319
+
320
+ /** Reverse buildPhaseGraph's complete_backfilled additions for legacy comparison. */
321
+ export function legacyPolicyProjection(policy) {
322
+ const fields = policyChecksumFields(policy);
323
+ const graph = {};
324
+ for (const [phase, targets] of Object.entries(fields.graph || {})) {
325
+ if (phase === 'complete_backfilled') continue;
326
+ graph[phase] = (targets || []).filter((target) => target !== 'complete_backfilled');
327
+ }
328
+ return {
329
+ graph,
330
+ edge_predicates: fields.edge_predicates,
331
+ terminal: (fields.terminal || []).filter((phase) => phase !== 'complete_backfilled'),
332
+ stakes: fields.stakes,
333
+ };
334
+ }
335
+
336
+ function _sortPolicyObject(value) {
337
+ if (Array.isArray(value)) return value.map(_sortPolicyObject);
338
+ if (value && typeof value === 'object') {
339
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, _sortPolicyObject(value[key])]));
340
+ }
341
+ return value;
342
+ }
343
+
344
+ /** Object key order is irrelevant; terminal order is not; adjacency order is. */
345
+ export function policiesEqual(a, b) {
346
+ const normalise = (policy) => {
347
+ const fields = policyChecksumFields(policy);
348
+ return _sortPolicyObject({ ...fields, terminal: [...(fields.terminal || [])].sort() });
349
+ };
350
+ return JSON.stringify(normalise(a)) === JSON.stringify(normalise(b));
351
+ }
352
+
240
353
  /**
241
354
  * Resolve the feature directory RELATIVE to the served `workspaceRoot` — read
242
355
  * from `<workspaceRoot>/.compose/compose.json` (NOT the process-global config,
@@ -298,11 +411,13 @@ export function guardTestCommand(workspaceRoot) {
298
411
  */
299
412
  export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode = 'build') {
300
413
  const rid = resourceId(featureCode, workspaceRoot, mode);
301
- if (_registered.has(rid)) return { guard_id: rid, status: 'cached' };
414
+ if (_registered.has(rid)) {
415
+ return { guard_id: rid, status: _registered.get(rid) === 'legacy' ? 'legacy' : 'cached' };
416
+ }
302
417
 
303
418
  let res;
304
419
  try {
305
- res = await _client.register({
420
+ const newPolicy = {
306
421
  resourceId: rid,
307
422
  graph: buildPhaseGraph(mode),
308
423
  edgePredicates: edgePredicates(_featureRelDir(featureCode, workspaceRoot, mode), mode),
@@ -310,18 +425,123 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode
310
425
  terminal: terminalOf(mode),
311
426
  stakes: {},
312
427
  workspaceRoot,
313
- });
428
+ };
429
+ res = await _client.register(newPolicy);
430
+ if (guardErrorType(res) === 'guard_already_registered') {
431
+ const stored = await _client.policy(rid);
432
+ if (isGuardError(stored)) {
433
+ return { error: { code: guardErrorType(stored), message: guardErrorMessage(stored) } };
434
+ }
435
+ const projected = legacyPolicyProjection({
436
+ graph: newPolicy.graph,
437
+ edge_predicates: newPolicy.edgePredicates,
438
+ terminal: newPolicy.terminal,
439
+ stakes: newPolicy.stakes,
440
+ });
441
+ if (policiesEqual(projected, policyChecksumFields(stored))) {
442
+ _registered.set(rid, 'legacy');
443
+ return { guard_id: rid, status: 'legacy', storedChecksum: stored.checksum };
444
+ }
445
+ return {
446
+ error: {
447
+ code: 'GUARD_POLICY_DIVERGED',
448
+ message: `stored guard policy for ${rid} differs from the expected legacy policy`,
449
+ },
450
+ };
451
+ }
314
452
  } catch (e) {
315
453
  // A thrown Stratum CLI failure must NOT escape as
316
454
  // a generic 500/400 — normalise to a fail-closed error result.
317
455
  return { error: { code: 'GUARD_UNREACHABLE', message: e.message } };
318
456
  }
319
457
  if (res && (res.status === 'registered' || res.status === 'exists')) {
320
- _registered.add(rid);
458
+ _registered.set(rid, 'cached');
321
459
  }
322
460
  return res;
323
461
  }
324
462
 
463
+ /** Lazily apply the signed legacy-policy upgrade required for backfilled completion. */
464
+ export async function applyBackfillUpgrade({ featureCode, workspaceRoot, mode = 'build' }) {
465
+ const rid = resourceId(featureCode, workspaceRoot, mode);
466
+ let stored;
467
+ try {
468
+ stored = await _client.policy(rid);
469
+ } catch (error) {
470
+ const message = error?.message || String(error);
471
+ return { ok: false, reasons: ['could not read stored guard policy'], error: { code: 'GUARD_UNREACHABLE', message, hint: HINT_APPROVE } };
472
+ }
473
+ if (isGuardError(stored)) {
474
+ const message = guardErrorMessage(stored);
475
+ return { ok: false, reasons: ['could not read stored guard policy'], error: { code: guardErrorType(stored) ?? 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE } };
476
+ }
477
+ const expected = deriveBackfillPolicy(policyChecksumFields(stored), mode);
478
+ if (policiesEqual(expected, policyChecksumFields(stored))) {
479
+ _registered.set(rid, 'cached');
480
+ return { ok: true, status: 'unchanged', checksum: stored.checksum };
481
+ }
482
+
483
+ let release;
484
+ try {
485
+ release = await acquireDirLock(
486
+ path.join(workspaceRoot, '.compose', 'data', 'locks', 'guard-descriptors'),
487
+ { timeoutMs: DESCRIPTOR_LOCK_TIMEOUT_MS },
488
+ );
489
+ } catch (error) {
490
+ const message = error?.message || String(error);
491
+ return { ok: false, reasons: [message], error: { code: 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE } };
492
+ }
493
+ try {
494
+ const ensured = await _ensureDescriptors({
495
+ workspaceRoot,
496
+ needChecksums: [stored.checksum],
497
+ custody: _custody,
498
+ verifier: _client.descriptors,
499
+ });
500
+ if (ensured.status === 'refused') {
501
+ return {
502
+ ok: false,
503
+ reasons: [ensured.message],
504
+ error: { code: ensured.code ?? 'upgrade_descriptor_unavailable', message: ensured.message, hint: ensured.hint ?? HINT_APPROVE },
505
+ };
506
+ }
507
+
508
+ let applied;
509
+ try {
510
+ applied = await _client.applyUpgrade({
511
+ resourceId: rid,
512
+ descriptorId: descriptorIdFor(stored.checksum, mode),
513
+ descriptorsPath: ensured.path,
514
+ });
515
+ } catch (error) {
516
+ const message = error?.message || String(error);
517
+ return { ok: false, reasons: ['guard upgrade failed'], error: { code: 'GUARD_UNREACHABLE', message, hint: HINT_APPROVE } };
518
+ }
519
+ if (isGuardError(applied)) {
520
+ const message = guardErrorMessage(applied);
521
+ return {
522
+ ok: false,
523
+ reasons: ['guard upgrade failed'],
524
+ error: { code: guardErrorType(applied) ?? 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE },
525
+ };
526
+ }
527
+ if (applied.status !== 'applied' && applied.status !== 'unchanged') {
528
+ const message = 'guard upgrade returned an unexpected result';
529
+ return { ok: false, reasons: [message], error: { code: 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE } };
530
+ }
531
+ _registered.set(rid, 'cached');
532
+ return {
533
+ ok: true,
534
+ status: applied.status,
535
+ ledgerRef: applied.ledger_ref,
536
+ checksum: applied.checksum,
537
+ descriptorId: descriptorIdFor(stored.checksum, mode),
538
+ generation: ensured.sha,
539
+ };
540
+ } finally {
541
+ await release();
542
+ }
543
+ }
544
+
325
545
  /**
326
546
  * Attempt a guarded lifecycle transition. Registers (idempotently) then
327
547
  * transitions. FAIL-CLOSED: any guard error (unreachable, illegal edge, error
@@ -330,14 +550,22 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode
330
550
  * @returns {Promise<{applied:boolean, refused?:boolean, verdict?:object,
331
551
  * ledgerRef?:string, currentState?:string, error?:object}>}
332
552
  */
333
- export async function guardedTransition({ featureCode, from, to, workspaceRoot, commitSha, resolvedBy = 'agent', mode = 'build' }) {
553
+ export async function guardedTransition({ featureCode, from, to, workspaceRoot, commitSha, resolvedBy = 'agent', mode = 'build', artifacts: extraArtifacts, idempotencyKey, expectedPolicyChecksum }) {
334
554
  const reg = await ensureGuard(featureCode, from, workspaceRoot, mode);
335
555
  if (reg && (reg.error || reg.status === 'error')) {
336
556
  return { applied: false, error: reg.error || reg };
337
557
  }
338
558
 
339
559
  const rid = resourceId(featureCode, workspaceRoot, mode);
340
- const artifacts = commitSha ? { commit_sha: commitSha } : {};
560
+ // COMP-COMPLETION-GATE: callers may add artifacts (e.g. the completion gate's
561
+ // `operation_id`). Artifacts feed the ledger's payload_digest, which is the
562
+ // only way two commit-less transitions on one resource are distinguishable —
563
+ // without it, every null-SHA completion hashes identically and a crash-recovery
564
+ // check cannot tell one operation from another.
565
+ const artifacts = {
566
+ ...(commitSha ? { commit_sha: commitSha } : {}),
567
+ ...(extraArtifacts || {}),
568
+ };
341
569
  // No idempotency_key: a refuse→fix→retry is a NEW logical attempt that must
342
570
  // re-evaluate evidence, but it carries an identical (from,to,artifacts)
343
571
  // payload — an idempotency_key would make the guard replay the prior refusal.
@@ -350,6 +578,8 @@ export async function guardedTransition({ featureCode, from, to, workspaceRoot,
350
578
  fromState: from,
351
579
  toState: to,
352
580
  artifacts,
581
+ idempotencyKey,
582
+ expectedPolicyChecksum,
353
583
  resolvedBy,
354
584
  });
355
585
  } catch (e) {