spexcode 0.6.1 → 0.6.5

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 (480) hide show
  1. package/README.md +99 -149
  2. package/bin/spex.mjs +4 -0
  3. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
  4. package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
  5. package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
  6. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
  7. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
  8. package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
  9. package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
  10. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
  11. package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
  12. package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
  13. package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
  14. package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
  15. package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
  16. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
  17. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
  18. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
  19. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
  20. package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
  21. package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
  22. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
  23. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
  24. package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
  25. package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
  26. package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
  27. package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
  28. package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
  29. package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
  30. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
  31. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
  32. package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
  33. package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
  34. package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
  35. package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
  36. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
  37. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
  38. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
  39. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
  40. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
  41. package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
  42. package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
  43. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
  44. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
  45. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
  46. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
  47. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
  48. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
  49. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
  50. package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
  51. package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +115 -22
  52. package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
  53. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
  54. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +325 -0
  55. package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
  56. package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
  57. package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
  58. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
  59. package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
  60. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
  61. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
  62. package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
  63. package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
  64. package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
  65. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
  66. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
  67. package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
  68. package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
  69. package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
  70. package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
  71. package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
  72. package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
  73. package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
  74. package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
  75. package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
  76. package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
  77. package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
  78. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
  79. package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
  80. package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
  81. package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
  82. package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
  83. package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
  84. package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
  85. package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
  86. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
  87. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
  88. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
  89. package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
  90. package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
  91. package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
  92. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
  93. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
  94. package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
  95. package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
  96. package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
  97. package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
  98. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
  99. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
  100. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
  101. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
  102. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
  103. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
  104. package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
  105. package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
  106. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
  107. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
  108. package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
  109. package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
  110. package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
  111. package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
  112. package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
  113. package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
  114. package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
  115. package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
  116. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
  117. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
  118. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
  119. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
  120. package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
  121. package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
  122. package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
  123. package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
  124. package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
  125. package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
  126. package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
  127. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
  128. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
  129. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
  130. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
  131. package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
  132. package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
  133. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
  134. package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
  135. package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
  136. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
  137. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
  138. package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
  139. package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
  140. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
  141. package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
  142. package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
  143. package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
  144. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
  145. package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
  146. package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
  147. package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
  148. package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
  149. package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
  150. package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
  151. package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
  152. package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
  153. package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
  154. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
  155. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
  156. package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
  157. package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
  158. package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
  159. package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
  160. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
  161. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
  162. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
  163. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +27 -7
  164. package/node_modules/@spexcode/spec-cli/package.json +47 -0
  165. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  166. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
  167. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  168. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  169. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +22 -0
  170. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  171. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  172. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
  173. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
  174. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
  175. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
  176. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  177. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  178. package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
  179. package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
  180. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  181. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  182. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  183. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  184. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  185. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  186. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  187. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  188. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  189. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  190. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  191. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  192. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
  193. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  194. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  195. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  196. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  197. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  198. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  199. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  200. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  201. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  202. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  203. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  204. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  205. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  206. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  207. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  208. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  209. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  210. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  211. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  212. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  213. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  214. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  215. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  216. package/node_modules/@spexcode/spec-core/package.json +28 -0
  217. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  218. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  219. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  220. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  221. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  222. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  223. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  224. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  225. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  226. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  227. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  228. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  229. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  230. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  231. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  232. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  233. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  234. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  235. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  236. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  237. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  238. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  239. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  240. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  241. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  242. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  243. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  244. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  245. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  246. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  247. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  248. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  249. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  250. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  251. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  252. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  253. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  254. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  255. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  256. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  257. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  258. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  259. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  260. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  261. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  262. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  263. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  264. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  265. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  266. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  267. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  268. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  269. package/package.json +33 -19
  270. package/spec-cli/bin/spex.mjs +0 -69
  271. package/spec-cli/src/anchors.ts +0 -706
  272. package/spec-cli/src/attach.ts +0 -43
  273. package/spec-cli/src/claude-headless.ts +0 -321
  274. package/spec-cli/src/cli.ts +0 -1115
  275. package/spec-cli/src/client.ts +0 -315
  276. package/spec-cli/src/cockpit.ts +0 -43
  277. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  278. package/spec-cli/src/commit-surgery.ts +0 -68
  279. package/spec-cli/src/contract-filter.ts +0 -202
  280. package/spec-cli/src/delivery-queue.ts +0 -107
  281. package/spec-cli/src/doctor.ts +0 -516
  282. package/spec-cli/src/file-write.ts +0 -22
  283. package/spec-cli/src/gateway-auth.ts +0 -181
  284. package/spec-cli/src/gateway-hub.ts +0 -356
  285. package/spec-cli/src/gateway.ts +0 -368
  286. package/spec-cli/src/git.ts +0 -2180
  287. package/spec-cli/src/graph.ts +0 -221
  288. package/spec-cli/src/graphCache.ts +0 -641
  289. package/spec-cli/src/graphDelta.ts +0 -77
  290. package/spec-cli/src/graphStream.ts +0 -1019
  291. package/spec-cli/src/harness-select.ts +0 -77
  292. package/spec-cli/src/harness.ts +0 -2747
  293. package/spec-cli/src/headless-controller.ts +0 -63
  294. package/spec-cli/src/help.ts +0 -480
  295. package/spec-cli/src/host-resources.ts +0 -707
  296. package/spec-cli/src/host.ts +0 -611
  297. package/spec-cli/src/identity-presets.d.ts +0 -13
  298. package/spec-cli/src/identity-presets.js +0 -128
  299. package/spec-cli/src/index.ts +0 -730
  300. package/spec-cli/src/init.ts +0 -258
  301. package/spec-cli/src/issues-cli.ts +0 -276
  302. package/spec-cli/src/issues.ts +0 -237
  303. package/spec-cli/src/layout.ts +0 -557
  304. package/spec-cli/src/lint.ts +0 -458
  305. package/spec-cli/src/localIssues.ts +0 -538
  306. package/spec-cli/src/materialize.ts +0 -412
  307. package/spec-cli/src/mentions.ts +0 -81
  308. package/spec-cli/src/opencode-headless.ts +0 -205
  309. package/spec-cli/src/pi-headless.ts +0 -153
  310. package/spec-cli/src/plugin-harness.ts +0 -157
  311. package/spec-cli/src/process-identity.ts +0 -207
  312. package/spec-cli/src/project-identity.ts +0 -73
  313. package/spec-cli/src/project-store.ts +0 -17
  314. package/spec-cli/src/pty-bridge.ts +0 -396
  315. package/spec-cli/src/pty-helper.mjs +0 -96
  316. package/spec-cli/src/pty-native-helper.mjs +0 -22
  317. package/spec-cli/src/ranker.ts +0 -191
  318. package/spec-cli/src/reaper.ts +0 -125
  319. package/spec-cli/src/resilience.ts +0 -41
  320. package/spec-cli/src/reviewSnapshot.ts +0 -28
  321. package/spec-cli/src/reviews.ts +0 -405
  322. package/spec-cli/src/runtime-guard.ts +0 -35
  323. package/spec-cli/src/runtime-ownership.ts +0 -88
  324. package/spec-cli/src/search.bench.mjs +0 -80
  325. package/spec-cli/src/search.ts +0 -63
  326. package/spec-cli/src/session-cursors.ts +0 -91
  327. package/spec-cli/src/session-follow.ts +0 -182
  328. package/spec-cli/src/session-timeline.ts +0 -98
  329. package/spec-cli/src/sessions.ts +0 -3403
  330. package/spec-cli/src/sh.ts +0 -3
  331. package/spec-cli/src/slash-commands.ts +0 -318
  332. package/spec-cli/src/source-files.ts +0 -104
  333. package/spec-cli/src/specs.ts +0 -498
  334. package/spec-cli/src/supervise.ts +0 -245
  335. package/spec-cli/src/tree.ts +0 -118
  336. package/spec-cli/src/tsx-bin.ts +0 -18
  337. package/spec-cli/src/uninstall.ts +0 -171
  338. package/spec-cli/src/uploads.ts +0 -253
  339. package/spec-cli/src/worktree-sources.ts +0 -42
  340. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  341. package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -28
  342. package/spec-cli/templates/spec/project/.plugins/core/spec.md +0 -23
  343. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -32
  344. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -9
  345. package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -15
  346. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -18
  347. package/spec-dashboard/dist/assets/App-F9uaAVcH.js +0 -2
  348. package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
  349. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  350. package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
  351. package/spec-dashboard/dist/assets/IssuesPage-CuKLFhH3.js +0 -1
  352. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  353. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  354. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  355. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  356. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  357. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  358. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  359. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  360. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  361. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  404. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  405. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  406. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  407. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  408. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  409. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  410. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  411. package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
  412. package/spec-dashboard/dist/assets/Modal-CQgYymmr.js +0 -1
  413. package/spec-dashboard/dist/assets/PageScroll-hT7UTLvD.js +0 -1
  414. package/spec-dashboard/dist/assets/ProjectsPage-CtXxakF9.js +0 -1
  415. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  416. package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
  417. package/spec-dashboard/dist/assets/SessionWindow-CixDi4PI.js +0 -1
  418. package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
  419. package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
  420. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  421. package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
  422. package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
  423. package/spec-dashboard/dist/assets/index-DrVao0Ep.js +0 -41
  424. package/spec-dashboard/dist/assets/launch-BBH02b1v.js +0 -276
  425. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  426. package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
  427. package/spec-dashboard/dist/index.html +0 -29
  428. package/spec-dashboard/src/reviewFilters.js +0 -322
  429. package/spec-dashboard/src/reviewQuery.js +0 -174
  430. package/spec-dashboard/src/session.js +0 -199
  431. package/spec-eval/src/cache.ts +0 -57
  432. package/spec-eval/src/cli.ts +0 -889
  433. package/spec-eval/src/evaltab.ts +0 -262
  434. package/spec-eval/src/filing.ts +0 -39
  435. package/spec-eval/src/freshness.ts +0 -557
  436. package/spec-eval/src/humanok.ts +0 -28
  437. package/spec-eval/src/scenariofresh.ts +0 -244
  438. package/spec-eval/src/scenarios.ts +0 -672
  439. package/spec-eval/src/sessioneval.ts +0 -2226
  440. package/spec-eval/src/sidecar.ts +0 -93
  441. package/spec-eval/src/timeline.ts +0 -54
  442. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  443. package/spec-forge/src/cache.ts +0 -66
  444. package/spec-forge/src/cli.ts +0 -99
  445. package/spec-forge/src/drivers/github.ts +0 -132
  446. package/spec-forge/src/drivers/gitlab.ts +0 -153
  447. package/spec-forge/src/drivers.ts +0 -73
  448. package/spec-forge/src/links.ts +0 -72
  449. package/spec-forge/src/needs-eval.ts +0 -29
  450. package/spec-forge/src/port.ts +0 -36
  451. package/spec-forge/src/resident.ts +0 -46
  452. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  453. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  454. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  455. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  456. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  457. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  458. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  459. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  460. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  480. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -1,1019 +0,0 @@
1
- import { streamSSE } from 'hono/streaming'
2
- import type { Context } from 'hono'
3
- import { watch, mkdirSync, readdirSync, readFileSync, type FSWatcher } from 'node:fs'
4
- import { join, dirname, relative, resolve, basename } from 'node:path'
5
- import { sessionsRoot, gitCommonDir, sessionBranchIndex, mainBranch } from './layout.js'
6
- import { hotSignature, warmSignature, listSessions, pendingSessionCreateWorktreePaths } from './sessions.js'
7
- import { getBoard, getBoardForSessionRefresh, invalidateBoard, patrolBoard } from './graphCache.js'
8
- import { unitize, tagOf, diffUnits, type Units } from './graphDelta.js'
9
- import {
10
- holdSessionEvalProjectionObserver,
11
- invalidateSessionEvalProjections,
12
- releaseSessionEvalProjectionObserver,
13
- setSessionEvalProjectionNotify,
14
- setSessionEvalProjectionWarmup,
15
- } from '../../spec-eval/src/sessioneval.js'
16
-
17
- type Scope = 'sessions' | 'full'
18
- type EvalTarget = 'all' | { id?: string; path?: string }
19
- type Notify = () => void
20
- type Frame = { event: string; data: string }
21
- type DeltaSend = (frame: Frame) => void
22
- type TreeWatchCallback = (event: 'rename' | 'change', filename: string | Buffer | null) => void
23
- // the one filesystem primitive the registry is allowed to call — `fs.watch`'s overloads narrowed to the
24
- // exact shape both transports use, so an injected fake is the same three arguments.
25
- type WatchFactory = (path: string, options: { recursive?: boolean }, callback: TreeWatchCallback) => FSWatcher
26
- type WatchTransport = 'consolidated-recursive' | 'exact-directory'
27
-
28
- type TreeWatcherOptions = {
29
- root: string
30
- source: string
31
- scope: Scope
32
- recursive?: boolean
33
- ignore?: (relativePath: string) => boolean
34
- watchFactory?: WatchFactory
35
- transport?: WatchTransport
36
- onInput: (event: 'rename' | 'change', relativePath: string) => void
37
- onFailure: (error: Error) => void
38
- }
39
-
40
- // @@@ watch transport - the ONE place the platform appears. Observing a tree is a single product-level
41
- // registration; how many registrations the OS actually holds for it is the transport's business.
42
- // consolidated-recursive — the OS itself observes a whole subtree from one registration (Darwin
43
- // FSEvents, Windows ReadDirectoryChangesW). It reports by PATH, so an atomic replacement inside the
44
- // tree stays visible, and exclusions filter on delivery because nothing is consumed per directory.
45
- // exact-directory — no such observer exists (Linux), and Node's `recursive` option is a USERSPACE
46
- // fan-out registering one inotify watch per file AND directory (measured: 201 directories -> 801
47
- // watches) which, watching files by inode, goes blind the moment a file is atomically replaced. So
48
- // we enumerate directories once and install one NON-recursive watch each, all multiplexed onto the
49
- // loop's single shared inotify descriptor, excluding .git/node_modules at traversal time.
50
- export const consolidatedRecursiveWatch = (platform: NodeJS.Platform = process.platform): boolean =>
51
- platform === 'darwin' || platform === 'win32'
52
-
53
- // The live census across every registry: `sources` is what graph-stream asked the platform for (one per
54
- // canonical root), `registrations` is what the platform is actually holding for them. Both are read by the
55
- // watcher-budget tests and logged under SPEXCODE_BOARD_DEBUG, so a plateau is observable on every platform
56
- // rather than only where /proc exposes inotify descriptors.
57
- const liveRegistries = new Set<TreeWatcherRegistry>()
58
- let liveRegistrations = 0
59
- export function graphWatcherCensus(): { sources: number; registrations: number } {
60
- return { sources: liveRegistries.size, registrations: liveRegistrations }
61
- }
62
-
63
- export class TreeWatcherRegistry {
64
- readonly root: string
65
- readonly source: string
66
- readonly scope: Scope
67
- readonly transport: WatchTransport
68
- private readonly recursive: boolean
69
- private readonly ignore: (relativePath: string) => boolean
70
- private readonly watchFactory: WatchFactory
71
- private readonly onInput: TreeWatcherOptions['onInput']
72
- private readonly onFailure: TreeWatcherOptions['onFailure']
73
- private readonly handles = new Map<string, FSWatcher>()
74
- private refreshImmediate: ReturnType<typeof setImmediate> | null = null
75
- private failed = false
76
-
77
- constructor(options: TreeWatcherOptions) {
78
- this.root = resolve(options.root)
79
- this.source = options.source
80
- this.scope = options.scope
81
- this.recursive = options.recursive !== false
82
- this.transport = options.transport
83
- ?? (this.recursive && consolidatedRecursiveWatch() ? 'consolidated-recursive' : 'exact-directory')
84
- this.ignore = options.ignore ?? (() => false)
85
- this.watchFactory = options.watchFactory ?? (watch as unknown as WatchFactory)
86
- this.onInput = options.onInput
87
- this.onFailure = options.onFailure
88
- }
89
-
90
- get size(): number { return this.handles.size }
91
- paths(): string[] { return [...this.handles.keys()].sort() }
92
- // one kernel-side observer covers the subtree: the root is the only path we register, and no rename can
93
- // make the desired set drift, so there is nothing to re-walk.
94
- private get consolidated(): boolean { return this.recursive && this.transport === 'consolidated-recursive' }
95
-
96
- private desiredDirectories(): Set<string> {
97
- const desired = new Set<string>()
98
- const visit = (dir: string): void => {
99
- desired.add(dir)
100
- if (!this.recursive || this.consolidated) return
101
- let entries: import('node:fs').Dirent[]
102
- try { entries = readdirSync(dir, { withFileTypes: true }) }
103
- catch (error) {
104
- if (dir !== this.root && (error as NodeJS.ErrnoException).code === 'ENOENT') {
105
- desired.delete(dir)
106
- return
107
- }
108
- throw error
109
- }
110
- for (const entry of entries) {
111
- if (!entry.isDirectory() || entry.isSymbolicLink()) continue
112
- const path = join(dir, entry.name)
113
- const rel = relative(this.root, path)
114
- if (!this.ignore(rel)) visit(path)
115
- }
116
- }
117
- visit(this.root)
118
- return desired
119
- }
120
-
121
- private closeHandles(): void {
122
- if (this.refreshImmediate) { clearImmediate(this.refreshImmediate); this.refreshImmediate = null }
123
- const handles = [...this.handles.values()]
124
- this.handles.clear()
125
- liveRegistrations -= handles.length
126
- liveRegistries.delete(this)
127
- for (const handle of handles) { try { handle.close() } catch { /* already gone */ } }
128
- }
129
-
130
- private sourceError(path: string, error: unknown): Error {
131
- const err = error as NodeJS.ErrnoException
132
- const reason = error instanceof Error ? error.message : String(error)
133
- // source + path + errno, always — an exhausted platform must name which syscall budget it refused.
134
- const errno = err?.code && !reason.includes(err.code) ? ` (errno ${err.code})` : ''
135
- return new Error(`spec-cli: graph watcher '${this.source}' failed at ${path}: ${reason}${errno}`)
136
- }
137
-
138
- private fail(path: string, error: unknown): false {
139
- if (this.failed) return false
140
- this.failed = true
141
- this.closeHandles()
142
- this.onFailure(this.sourceError(path, error))
143
- return false
144
- }
145
-
146
- private scheduleRefresh(): void {
147
- if (this.refreshImmediate || this.failed) return
148
- this.refreshImmediate = setImmediate(() => {
149
- this.refreshImmediate = null
150
- this.refresh()
151
- })
152
- this.refreshImmediate.unref?.()
153
- }
154
-
155
- refresh(): boolean {
156
- if (this.refreshImmediate) { clearImmediate(this.refreshImmediate); this.refreshImmediate = null }
157
- this.failed = false
158
- let desired: Set<string>
159
- try { desired = this.desiredDirectories() }
160
- catch (error) { return this.fail(this.root, error) }
161
-
162
- const before = this.handles.size
163
- for (const [path, handle] of [...this.handles]) {
164
- if (desired.has(path)) continue
165
- this.handles.delete(path)
166
- liveRegistrations--
167
- try { handle.close() } catch { /* already gone */ }
168
- }
169
- const options = this.consolidated ? { recursive: true } : {}
170
- for (const path of desired) {
171
- if (this.handles.has(path)) continue
172
- try {
173
- let handle: FSWatcher
174
- const callback: TreeWatchCallback = (event, filename) => {
175
- if (this.handles.get(path) !== handle) return
176
- if (filename == null) { this.fail(path, new Error('pathless filesystem event')); return }
177
- const inputPath = resolve(path, String(filename))
178
- const rel = relative(this.root, inputPath)
179
- if (rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || rel === '..') {
180
- this.fail(path, new Error(`event escaped watcher root: ${String(filename)}`))
181
- return
182
- }
183
- if (this.ignore(rel)) return
184
- this.onInput(event, rel)
185
- // only the userspace transport's desired set can drift: a new directory under a consolidated
186
- // observer is already covered by the kernel.
187
- if (this.recursive && !this.consolidated && event === 'rename') this.scheduleRefresh()
188
- }
189
- handle = this.watchFactory(path, options, callback)
190
- this.handles.set(path, handle)
191
- liveRegistrations++
192
- handle.on('error', (error) => {
193
- if (this.handles.get(path) === handle) this.fail(path, error)
194
- })
195
- } catch (error) { return this.fail(path, error) }
196
- }
197
- if (this.handles.size) liveRegistries.add(this)
198
- if (DEBUG && this.handles.size !== before)
199
- console.warn(`spec-cli: graph watchers — sources=${liveRegistries.size} registrations=${liveRegistrations} (${this.source}: ${this.transport} ${this.handles.size})`)
200
- return true
201
- }
202
-
203
- close(): void {
204
- this.failed = false
205
- this.closeHandles()
206
- }
207
- }
208
-
209
- const plainSubs = new Set<Notify>()
210
- const deltaSubs = new Set<DeltaSend>()
211
- let debounce: ReturnType<typeof setTimeout> | null = null
212
- let pendingFull = false
213
- let pendingSessions = false
214
- export type PendingGraphChanges = { full: boolean; sessions: boolean }
215
- export const addPendingGraphChange = (pending: PendingGraphChanges, scope: Scope): PendingGraphChanges => ({
216
- full: pending.full || scope === 'full',
217
- sessions: pending.sessions || scope === 'sessions',
218
- })
219
-
220
- // Git creates the registry entry before the session transaction publishes its durable record. A registry
221
- // event for that private candidate must not start a full board build while the `git worktree add` child is
222
- // still running; the create route flushes the deferred event after publication (or cleanup).
223
- export function isSessionCreateCandidateRegistryEvent(relativePath: string, candidatePaths: Iterable<string>): boolean {
224
- const entry = relativePath.split(/[\\/]+/).filter(Boolean)[0] ?? ''
225
- if (!entry) return false
226
- for (const path of candidatePaths) if (basename(resolve(path)) === entry) return true
227
- return false
228
- }
229
-
230
- // under SPEXCODE_BOARD_DEBUG=1, every broadcast logs its changed unit keys + trigger tags + build ms.
231
- const DEBUG = process.env.SPEXCODE_BOARD_DEBUG === '1'
232
- function traceLatency(stage: 'sessions-signal' | 'session-projection-complete' | 'broadcast', detail: Record<string, unknown> = {}): void {
233
- if (DEBUG) console.warn(`spec-cli: graph latency ${JSON.stringify({ at: Date.now(), stage, ...detail })}`)
234
- }
235
- // the set of trigger tags accrued SINCE THE LAST BROADCAST — each fireChanged adds its scope, the cold tick
236
- // adds 'patrol'. Cleared on every broadcast. Its job: prove WHO caused a broadcast, so a change that only
237
- // the patrol saw (tag set === {'patrol'}) is flagged as a repair — some leaf watcher was blind.
238
- const triggerTags = new Set<string>()
239
-
240
- // watchers a test can amputate to prove the patrol still heals the graph ([[graph-stream]]): a CSV of
241
- // store,refs,worktrees makes the matching ensure* a no-op (with a one-time warning), so a change on that
242
- // path reaches subscribers ONLY via the cold-tick patrol — the missing-watcher scenario, on demand.
243
- const DISABLED = new Set((process.env.SPEXCODE_DISABLE_WATCHERS || '').split(',').map((s) => s.trim()).filter(Boolean))
244
- const warnedDisabled = new Set<string>()
245
- function isDisabled(name: string): boolean {
246
- if (!DISABLED.has(name)) return false
247
- if (!warnedDisabled.has(name)) { warnedDisabled.add(name); console.warn(`spec-cli: graph watcher '${name}' disabled via SPEXCODE_DISABLE_WATCHERS — the cold-tick patrol must cover it`) }
248
- return true
249
- }
250
-
251
- // ---- the rebuild→diff→broadcast pipeline (runs only while delta subscribers exist) ----
252
- // last successfully-broadcast snapshot: the delta chain's anchor. `lastFullFrame` is what a fresh
253
- // subscriber gets instantly; `lastUnits`+`lastTag` are what the next diff chains from. A snapshot that
254
- // failed the unitize precondition anchors nothing (lastUnits=null) so every following send is a full.
255
- let lastUnits: Units | null = null
256
- let lastTag = ''
257
- let lastFullFrame: Frame | null = null
258
- let building = false
259
- let dirty = false
260
- let patrolPending = false
261
- let sessionRefreshRequested = false
262
- let wakeSessionRefresh: (() => void) | null = null
263
-
264
- async function rebuildAndBroadcast(patrol = false, sessions = false, full = false): Promise<void> {
265
- if (patrol) patrolPending = true
266
- if (sessions) sessionRefreshRequested = true
267
- if (building) {
268
- dirty = true
269
- if (sessions) {
270
- wakeSessionRefresh?.()
271
- wakeSessionRefresh = null
272
- }
273
- return
274
- }
275
- building = true
276
- try {
277
- do {
278
- dirty = false
279
- const validate = patrolPending
280
- patrolPending = false
281
- const sessionsFirst = sessionRefreshRequested
282
- sessionRefreshRequested = false
283
- let servedSessionProjection = sessionsFirst
284
- let board: unknown
285
- // share the route's single-flight build ([[graph-cache]]); fireChanged() already invalidated the
286
- // cache (at the accumulated scope), so this gets a fresh build/splice (or joins one a concurrent poll
287
- // already started). The patrol instead asks that same cache flight to validate its input revision;
288
- // equal inputs return the anchor without invoking a producer.
289
- const t0 = Date.now()
290
- try {
291
- let wake!: () => void
292
- const sessionWake = new Promise<void>((resolve) => { wake = resolve })
293
- wakeSessionRefresh = wake
294
- // A sessions-first turn can still fall back to an active route-owned full. Keep its wait wakeable
295
- // too: a later persisted session change must re-enter the cheap projection rather than queue behind it.
296
- const boardWait = sessionsFirst
297
- ? getBoardForSessionRefresh()
298
- : validate ? patrolBoard() : getBoard()
299
- const outcome = await Promise.race([
300
- boardWait.then((value) => ({ value })),
301
- sessionWake.then(() => ({ value: null as unknown })),
302
- ])
303
- if (wakeSessionRefresh === wake) wakeSessionRefresh = null
304
- if (outcome.value === null) {
305
- boardWait.catch(() => {})
306
- board = await getBoardForSessionRefresh()
307
- servedSessionProjection = true
308
- if (validate) patrolPending = true
309
- dirty = true // the full wait was preempted only for delivery; it remains owed.
310
- } else {
311
- board = outcome.value
312
- if (sessionsFirst && validate) { patrolPending = true; dirty = true }
313
- }
314
- }
315
- catch {
316
- // A failed refresh consumes no cause: graph-cache restores the producer scope, so its stream-side
317
- // attribution must remain owed too. This also retains watcher causes that arrived while the failed
318
- // flight was occupied. A later patrol may recover the work, but it is then one cause alongside those
319
- // healthy leaf signals rather than a false patrol-only blind-watcher repair.
320
- for (const n of [...plainSubs]) { try { n() } catch { /* swept on abort */ } }
321
- continue
322
- }
323
- const buildMs = Date.now() - t0
324
- if (servedSessionProjection) traceLatency('session-projection-complete', { patrol: validate })
325
- const boardJson = JSON.stringify(board)
326
- const { units, ok } = unitize(board as Record<string, unknown>)
327
- const tag = tagOf(units)
328
- // A session-first frame consumes its own cause, but a full/patrol cause remains owed until structural
329
- // convergence. Otherwise the first cheap projection would erase patrol accountability before the full
330
- // result could name it. A normal frame consumes its whole trigger set, including a no-op frame.
331
- const tags = [...triggerTags]
332
- triggerTags.clear()
333
- if (servedSessionProjection)
334
- for (const tag of tags) if (tag === 'full' || tag === 'patrol') triggerTags.add(tag)
335
- if (sessionsFirst && full) dirty = true
336
- if (tag === lastTag) continue
337
- // the changed unit keys — computed against the prior anchor when we have one (a first paint has no
338
- // anchor, so no repair claim can be made against it).
339
- const changedKeys = lastUnits ? (() => { const { set, del } = diffUnits(lastUnits, units); return [...Object.keys(set), ...del] })() : []
340
- const fullFrame: Frame = { event: 'graph-full', data: `{"to":"${tag}","graph":${boardJson}}` }
341
- let frame = fullFrame
342
- if (lastUnits && ok) {
343
- const { set, del } = diffUnits(lastUnits, units)
344
- const deltaData = JSON.stringify({ from: lastTag, to: tag, set, del })
345
- // guaranteed win: ship the patch only when it actually beats the snapshot
346
- if (deltaData.length < fullFrame.data.length) frame = { event: 'graph-delta', data: deltaData }
347
- }
348
- // the anchor is only meaningful while a delta subscriber holds the chain live: with none left,
349
- // nothing rebuilds on change, so a cached anchor would silently age into a stale first frame for the
350
- // NEXT era's subscriber (issue #70). A build that completes after the last unsub caches nothing
351
- // (stopSourcesIfIdle cleared the anchor; leaving lastTag/lastUnits stale-cleared is consistent —
352
- // rebuilds only run while delta subscribers exist, so nothing chains from them meanwhile).
353
- if (deltaSubs.size) { lastUnits = ok ? units : null; lastTag = tag; lastFullFrame = fullFrame }
354
- traceLatency('broadcast', { event: frame.event, sessionProjection: servedSessionProjection, tags, changedKeys })
355
- for (const send of [...deltaSubs]) { try { send(frame) } catch { /* swept on abort */ } }
356
- for (const n of [...plainSubs]) { try { n() } catch { /* swept on abort */ } }
357
- // ---- repair accounting: a real (tag-moved) broadcast whose ONLY trigger was the cold-tick patrol
358
- // means a leaf watcher was BLIND — the patrol self-healed it. That is a bug report, not routine, so
359
- // it is ALWAYS loud (repairs are supposed to be zero — [[graph-stream]]). Under DEBUG, every
360
- // broadcast logs its changed keys + triggers + build ms.
361
- if (changedKeys.length && tags.length === 1 && tags[0] === 'patrol')
362
- console.warn(`spec-cli: PATROL-REPAIR — the cold tick caught a change no leaf watcher pushed; changed units: [${changedKeys.join(', ')}] — a blind watcher, investigate`)
363
- if (DEBUG)
364
- console.warn(`spec-cli: graph broadcast — changed [${changedKeys.join(', ')}] triggers {${tags.join(', ')}} build ${buildMs}ms`)
365
- } while (dirty)
366
- } finally { building = false }
367
- }
368
-
369
- // a merge/launch/close touches several record files at once; collapse the burst into ONE signal. Each call
370
- // carries its own change SCOPE: full and sessions are independent obligations, not a max-scope replacement.
371
- // With delta subscribers the debounced fire rebuilds and broadcasts (plain subs then ride the same
372
- // tag-moved gate — no spurious refetches); without them it stays the zero-build legacy notify.
373
- function fireChanged(scope: Scope = 'full', evalTarget?: EvalTarget): void {
374
- // Advance eval input generations BEFORE invalidating/building the board, so the first frame caused by an
375
- // input event is `updating(lastKnown)`. Summary completion calls this function without a target.
376
- if (scope === 'sessions') traceLatency('sessions-signal')
377
- if (evalTarget) invalidateSessionEvalProjections(evalTarget)
378
- const pending = addPendingGraphChange({ full: pendingFull, sessions: pendingSessions }, scope)
379
- pendingFull = pending.full
380
- pendingSessions = pending.sessions
381
- // invalidate the route's board cache ([[graph-cache]]) on EVERY change signal at its OWN scope,
382
- // before the debounce guard — a plain-mode client that polls /api/graph (no delta rebuild here) must
383
- // still see fresh data on its next poll, and a delta rebuild below re-reads the same now-stale cache.
384
- invalidateBoard(scope)
385
- triggerTags.add(scope)
386
- // DEBOUNCE = 25ms. Real fs-event bursts (a merge touching many records) were MEASURED to span 0–5ms, so a
387
- // 25ms window collapses them with room to spare while shaving ~125ms off the old 150ms lag; anything
388
- // wider than the window is coalesced anyway by the in-flight build's dirty-rerun loop, which is the real
389
- // burst absorber. So the debounce is a micro-collapse, not the coalescer.
390
- if (debounce) return
391
- debounce = setTimeout(() => {
392
- debounce = null
393
- const full = pendingFull, sessions = pendingSessions
394
- pendingFull = false
395
- pendingSessions = false
396
- if (deltaSubs.size) void rebuildAndBroadcast(false, sessions, full)
397
- else for (const notify of [...plainSubs]) { try { notify() } catch { /* swept on abort */ } }
398
- }, 25)
399
- }
400
-
401
- // ---- event source 0: an EXPLICIT server-side nudge ----
402
- // for a server-side mutation that must show instantly regardless of watcher health: /rename writes the
403
- // session's global record (`session.json` — [[session-rename]]), which lives INSIDE the watched store, so
404
- // source 1 normally sees the write too. The explicit route call stays because that fs watch is best-effort
405
- // (it can fail to attach), and the nudge makes the sub-second rename guarantee deterministic. Same
406
- // debounced funnel as every other source; defaults to 'full' but the rename route passes 'sessions'.
407
- export const notifyBoardChanged = (scope: Scope = 'full'): void =>
408
- fireChanged(scope, scope === 'full' ? 'all' : undefined)
409
-
410
- // Stable summary batches re-enter the SAME session-unit graph path; this is not a second transport.
411
- setSessionEvalProjectionNotify(() => fireChanged('sessions'))
412
-
413
- // ---- ONE repair scheduler for every filesystem source ----
414
- // A source the platform refuses keeps its observer hold and is retried by THIS timer alone — never by a
415
- // graph build, an HTTP read, a poller tick or a registry event. That is the whole anti-storm rule: the
416
- // resource being exhausted (a registration budget, a descriptor table) is PROCESS-wide, so its backoff
417
- // belongs to the process, not to each source racing its own retry and re-walking the corpus every time.
418
- // `heldSources` is what an ordinary pass may not re-attempt; only the scheduled pass lifts that.
419
- const heldSources = new Set<string>()
420
- let repairTimer: ReturnType<typeof setTimeout> | null = null
421
- let repairStep = 0
422
- let repairing = false
423
-
424
- const mayAttach = (source: string): boolean => repairing || !heldSources.has(source)
425
-
426
- // The failure is loud ONCE per episode and names source + path + errno (the registry built that message);
427
- // every later source felled by the same exhausted budget is counted, not re-printed, and the repair line
428
- // reports the total. A half-attached registry is already closed by the registry itself before we get here.
429
- function noteSourceFailure(source: string, error: unknown): void {
430
- const known = heldSources.has(source)
431
- heldSources.add(source)
432
- if (!known && heldSources.size === 1) console.error(error instanceof Error ? error.message : String(error))
433
- scheduleWatcherRepair()
434
- }
435
-
436
- function noteSourceHealthy(source: string): void {
437
- if (!heldSources.delete(source)) return
438
- if (heldSources.size === 0) repairStep = 0
439
- }
440
-
441
- function scheduleWatcherRepair(): void {
442
- if (repairTimer || !heldSources.size) return
443
- const delay = Math.min(30_000, 250 * 2 ** Math.min(repairStep, 7))
444
- repairStep++
445
- const era = watcherEra
446
- console.error(`spec-cli: graph watcher repair — ${heldSources.size} source(s) held, retrying in ${delay}ms; the cold-tick patrol covers the gap until they reattach`)
447
- repairTimer = setTimeout(() => {
448
- repairTimer = null
449
- if (era !== watcherEra) return
450
- repairing = true
451
- void ensureBoardFileWatchers()
452
- .catch((error) => console.error(`spec-cli: graph watcher repair failed — ${error instanceof Error ? error.message : String(error)}`))
453
- .finally(() => {
454
- repairing = false
455
- if (heldSources.size) scheduleWatcherRepair()
456
- })
457
- }, delay)
458
- repairTimer.unref?.()
459
- }
460
-
461
- // ---- event source 1: the session store (lifecycle status writes) → 'sessions' ----
462
- let activeStoreRoot: string | null = null
463
- let activeCommonRoot: string | null = null
464
- let watcherEra = 0
465
- let storeWatcher: TreeWatcherRegistry | null = null
466
-
467
- function ensureWatcher(root: string): void {
468
- if (storeWatcher?.root === root) return
469
- if (storeWatcher) { storeWatcher.close(); storeWatcher = null }
470
- if (isDisabled('store') || !mayAttach('store')) return
471
- try { mkdirSync(root, { recursive: true }) }
472
- catch (error) {
473
- console.error(`spec-cli: graph watcher 'store' could not create ${root}: ${error instanceof Error ? error.message : String(error)}`)
474
- }
475
- const registry = new TreeWatcherRegistry({
476
- root,
477
- source: 'store',
478
- scope: 'sessions',
479
- onInput: () => { dropBranchIndex(); fireChanged('sessions') }, // a created/renamed session may own a new branch
480
- onFailure: (error) => {
481
- if (storeWatcher === registry) storeWatcher = null
482
- noteSourceFailure('store', error)
483
- fireChanged('sessions')
484
- },
485
- })
486
- storeWatcher = registry
487
- if (!registry.refresh()) {
488
- if (storeWatcher === registry) storeWatcher = null
489
- return
490
- }
491
- noteSourceHealthy('store')
492
- }
493
-
494
- // ---- event source 2: git refs (a commit/merge reshapes the tree the moment the ref moves) → 'full' ----
495
- // refs/ recursively for loose refs (heads, worktree branches), plus the common dir itself non-recursively
496
- // for packed-refs rewrites and HEAD flips. Ordinary graph units still have the patrol; eval projections are
497
- // observer-held across a failure and only become current after a replacement watch authorizes a rescan.
498
- type RegistryGroup = {
499
- root: string
500
- close(): void
501
- }
502
- let refsWatchers: RegistryGroup | null = null
503
- const REFS_OBSERVER = 'graph:refs'
504
-
505
- // @@@ the moved ref NAMES its scope - the watcher has always known which ref moved and threw it away, so
506
- // every ref movement anywhere invalidated every session's evaluation. On a host carrying dozens of branches
507
- // and a bot that commits continuously that means nothing is ever warm: observed on z-code as an input
508
- // generation of 1208 against a last-known 254. A session's fingerprint reads exactly three refs — its own
509
- // tip, the base tip, and their merge-base — so a ref that is neither cannot move it. `packed-refs` and
510
- // `HEAD` stay broad on purpose: a packed update rewrites many refs behind ONE event, so it names nothing.
511
- // the index is read from the session store, so it is only ever stale when that store moved — and the store
512
- // has its own watcher, which drops it. A ref burst therefore costs one map lookup, not 76 record reads.
513
- let branchIndexMemo: Map<string, string> | null = null
514
- function branchIndex(): Map<string, string> {
515
- return (branchIndexMemo ??= sessionBranchIndex())
516
- }
517
- function dropBranchIndex(): void { branchIndexMemo = null }
518
-
519
- export function evalTargetForRef(ref: string | undefined, base: string, branches: Map<string, string>): EvalTarget | undefined {
520
- if (ref === undefined) return 'all' // an unnamed movement must assume the worst
521
- const rel = ref.replace(/\\/g, '/')
522
- if (!rel.startsWith('heads/')) return undefined // tags and remotes feed no session fingerprint
523
- const branch = rel.slice('heads/'.length)
524
- if (!branch) return 'all'
525
- if (branch === base) return 'all' // every merge-base may have moved
526
- const id = branches.get(branch)
527
- return id ? { id } : undefined // a branch no session owns moves no session's evaluation
528
- }
529
-
530
- export function watchSessionEvalRefs(
531
- common: string,
532
- onInput: (ref?: string) => void,
533
- onFailure: (error: Error) => void,
534
- ): RegistryGroup {
535
- let attached: TreeWatcherRegistry[] = []
536
- let failed = false
537
- let ready = false
538
- let attachError: Error | null = null
539
- const close = () => {
540
- const registries = attached
541
- attached = []
542
- for (const registry of registries) registry.close()
543
- }
544
- const fail = (error: Error) => {
545
- if (failed) return
546
- failed = true
547
- close()
548
- if (ready) onFailure(error)
549
- else attachError = error
550
- }
551
- const refs = new TreeWatcherRegistry({
552
- root: join(common, 'refs'),
553
- source: 'refs',
554
- scope: 'full',
555
- onInput: (_event, rel) => onInput(rel),
556
- onFailure: fail,
557
- })
558
- attached.push(refs)
559
- if (!refs.refresh()) throw attachError ?? new Error(`spec-cli: graph watcher 'refs' failed at ${refs.root}`)
560
-
561
- const commonFiles = new TreeWatcherRegistry({
562
- root: common,
563
- source: 'refs-common',
564
- scope: 'full',
565
- recursive: false,
566
- onInput: (_event, file) => { if (file === 'packed-refs' || file === 'HEAD') onInput() }, // names nothing -> 'all'
567
- onFailure: fail,
568
- })
569
- attached.push(commonFiles)
570
- if (!commonFiles.refresh()) throw attachError ?? new Error(`spec-cli: graph watcher 'refs-common' failed at ${common}`)
571
- ready = true
572
- return { root: resolve(common), close }
573
- }
574
-
575
- function refsWatcherFailed(error: Error): void {
576
- refsWatchers = null
577
- noteSourceFailure('refs', error)
578
- if (holdSessionEvalProjectionObserver(REFS_OBSERVER, 'all')) fireChanged('full')
579
- }
580
-
581
- function ensureRefsWatcher(common = activeCommonRoot): void {
582
- if (!common) return
583
- if (refsWatchers?.root === common) return
584
- if (refsWatchers) { refsWatchers.close(); refsWatchers = null }
585
- if (isDisabled('refs')) {
586
- if (holdSessionEvalProjectionObserver(REFS_OBSERVER, 'all')) fireChanged('full')
587
- return
588
- }
589
- if (!mayAttach('refs')) return
590
- try {
591
- refsWatchers = watchSessionEvalRefs(common, (ref) => fireChanged('full', evalTargetForRef(ref, mainBranch(), branchIndex())), refsWatcherFailed)
592
- noteSourceHealthy('refs')
593
- if (releaseSessionEvalProjectionObserver(REFS_OBSERVER)) fireChanged('full')
594
- } catch (error) {
595
- refsWatchers = null
596
- noteSourceFailure('refs', error)
597
- if (holdSessionEvalProjectionObserver(REFS_OBSERVER, 'all')) fireChanged('full')
598
- }
599
- }
600
-
601
- // ---- event source 3: worktree registry + working roots + per-worktree indexes → 'full' ----
602
- // Summary inputs include ordinary dirty source and staged-only changes, not just `.spec`. Each live
603
- // worktree is therefore TWO canonical roots — its working tree and git's metadata dir for it (`index`) —
604
- // and that pair is the whole per-worktree registration cost, whatever the corpus inside it holds.
605
- let registryWatcher: TreeWatcherRegistry | null = null
606
- let registryReady = false
607
- type WorktreeWatch = {
608
- path: string
609
- root: TreeWatcherRegistry
610
- index: TreeWatcherRegistry
611
- close(): void
612
- }
613
- const worktreeWatchers = new Map<string, WorktreeWatch>()
614
- const worktreeObserver = (name: string): string => `graph:worktree:${name}`
615
- const worktreeSource = (name: string): string => `worktree:${name}`
616
-
617
- const ignoredWorktreePath = (file: string): boolean =>
618
- file.split(/[\\/]/).some((segment) => segment === '.git' || segment === 'node_modules')
619
-
620
- export function watchSessionEvalWorktree(
621
- wtPath: string,
622
- gitDir: string,
623
- onInput: () => void,
624
- onFailure: (error: Error) => void,
625
- ): WorktreeWatch {
626
- let failed = false
627
- let ready = false
628
- let attachError: Error | null = null
629
- let root: TreeWatcherRegistry | null = null
630
- let index: TreeWatcherRegistry | null = null
631
- const close = () => {
632
- root?.close()
633
- index?.close()
634
- }
635
- const fail = (error: Error) => {
636
- if (failed) return
637
- failed = true
638
- close()
639
- if (ready) onFailure(error)
640
- else attachError = error
641
- }
642
- root = new TreeWatcherRegistry({
643
- root: wtPath,
644
- source: `worktree:${resolve(wtPath)}`,
645
- scope: 'full',
646
- ignore: ignoredWorktreePath,
647
- onInput: () => onInput(),
648
- onFailure: fail,
649
- })
650
- if (!root.refresh()) throw attachError ?? new Error(`spec-cli: graph watcher 'worktree' failed at ${wtPath}`)
651
- index = new TreeWatcherRegistry({
652
- root: gitDir,
653
- source: `worktree-index:${resolve(wtPath)}`,
654
- scope: 'full',
655
- recursive: false,
656
- onInput: (_event, file) => { if (file === 'index') onInput() },
657
- onFailure: fail,
658
- })
659
- if (!index.refresh()) throw attachError ?? new Error(`spec-cli: graph watcher 'worktree-index' failed at ${gitDir}`)
660
- ready = true
661
- return { path: resolve(wtPath), root, index, close }
662
- }
663
-
664
- function dropWorktreeWatcher(name: string): WorktreeWatch | null {
665
- const row = worktreeWatchers.get(name)
666
- if (!row) return null
667
- worktreeWatchers.delete(name)
668
- row.close()
669
- return row
670
- }
671
-
672
- function watcherFailed(name: string, path: string, error: Error): void {
673
- // Failure/overflow has no trustworthy path. Keep last-known, mark the target updating, and hand the
674
- // reattach to the one repair scheduler; the authorized summary build is the one authoritative rescan,
675
- // never a periodic sweep and never a re-walk driven by whatever read noticed the failure.
676
- dropWorktreeWatcher(name)
677
- noteSourceFailure(worktreeSource(name), error)
678
- if (holdSessionEvalProjectionObserver(worktreeObserver(name), { path })) fireChanged('full')
679
- }
680
-
681
- const forcedWorktreeSessions = new Set<string>()
682
- let worktreeReconcileFlight: Promise<void> | null = null
683
-
684
- export function sessionWorktreeWatchPaths(
685
- sessions: { id: string; path: string; liveness?: string }[],
686
- forcedSessions: Set<string> = new Set(),
687
- ): Set<string> {
688
- return new Set(sessions
689
- .filter((session) => session.liveness !== 'offline' || forcedSessions.has(session.id))
690
- .map((session) => resolve(session.path)))
691
- }
692
-
693
- async function reconcileWorktreePass(forcedSessions: Set<string>, era: number, common: string): Promise<void> {
694
- const dir = join(common, 'worktrees')
695
- let sessions: Awaited<ReturnType<typeof listSessions>>
696
- try { sessions = await listSessions() } catch { return }
697
- if (era !== watcherEra) return
698
- const wantedPaths = sessionWorktreeWatchPaths(sessions, forcedSessions)
699
- let ents: import('node:fs').Dirent[] = []
700
- try { ents = readdirSync(dir, { withFileTypes: true }) } catch { /* no worktrees registry yet */ }
701
- const wantedNames = new Set<string>()
702
- let released = false
703
- for (const e of ents) {
704
- if (!e.isDirectory()) continue
705
- let wtPath: string
706
- try { wtPath = dirname(readFileSync(join(dir, e.name, 'gitdir'), 'utf8').trim()) } catch { continue }
707
- const normalizedPath = resolve(wtPath)
708
- if (!wantedPaths.has(normalizedPath)) {
709
- if (dropWorktreeWatcher(e.name)) released = releaseSessionEvalProjectionObserver(worktreeObserver(e.name)) || released
710
- noteSourceHealthy(worktreeSource(e.name))
711
- continue
712
- }
713
- wantedNames.add(e.name)
714
- const existing = worktreeWatchers.get(e.name)
715
- if (existing?.path === normalizedPath) continue
716
- const rootChanged = existing != null
717
- if (existing) dropWorktreeWatcher(e.name)
718
- // a held worktree is the repair scheduler's to reattach — an ordinary reconcile pass must not re-walk
719
- // it, which is what turned one refused registration into a per-read attach storm.
720
- if (!mayAttach(worktreeSource(e.name))) continue
721
- try {
722
- // the entry's `gitdir` file points at the worktree's `<tree>/.git` (file or dir); its parent is the tree.
723
- const row = watchSessionEvalWorktree(
724
- wtPath,
725
- join(dir, e.name),
726
- () => fireChanged('full', { path: wtPath }),
727
- (error) => watcherFailed(e.name, wtPath, error),
728
- )
729
- if (era !== watcherEra) { row.close(); return }
730
- worktreeWatchers.set(e.name, row)
731
- noteSourceHealthy(worktreeSource(e.name))
732
- if (rootChanged) fireChanged('full', { path: wtPath })
733
- // The replacement is live before its hold is removed. This delta authorizes one double-read rescan,
734
- // so edits made anywhere in the unwatched interval are inside the new generation's fingerprint.
735
- if (releaseSessionEvalProjectionObserver(worktreeObserver(e.name))) fireChanged('full')
736
- } catch (error) {
737
- // An attach failure is observable: mark unknown/full now. The repair scheduler owns the reattach; no
738
- // patrol is allowed to call the eval projection current meanwhile.
739
- noteSourceFailure(worktreeSource(e.name), error)
740
- if (holdSessionEvalProjectionObserver(worktreeObserver(e.name), { path: wtPath })) fireChanged('full')
741
- }
742
- }
743
- for (const name of worktreeWatchers.keys()) if (!wantedNames.has(name)) {
744
- dropWorktreeWatcher(name)
745
- released = releaseSessionEvalProjectionObserver(worktreeObserver(name)) || released
746
- }
747
- for (const source of [...heldSources]) {
748
- const name = source.startsWith('worktree:') ? source.slice('worktree:'.length) : null
749
- if (name && !wantedNames.has(name)) {
750
- noteSourceHealthy(source)
751
- released = releaseSessionEvalProjectionObserver(worktreeObserver(name)) || released
752
- }
753
- }
754
- if (released) fireChanged('full')
755
- }
756
-
757
- function reconcileWorktrees(forceSessionId?: string): Promise<void> {
758
- // Blinding a leaf must blind it from EVERY entry point. Reconciliation is reached from the liveness
759
- // poller and from registry events as well as from the ensure pass, so gating only the ensure pass left
760
- // the per-worktree observers attaching anyway — the injection looked applied while the leaf still saw
761
- // everything, which makes the patrol's accountability untestable rather than merely untested.
762
- if (isDisabled('worktrees')) return Promise.resolve()
763
- if (forceSessionId) forcedWorktreeSessions.add(forceSessionId)
764
- if (worktreeReconcileFlight) return worktreeReconcileFlight
765
- const era = watcherEra
766
- const common = activeCommonRoot
767
- if (!common) return Promise.resolve()
768
- const flight = (async () => {
769
- do {
770
- if (era !== watcherEra) return
771
- const forced = new Set(forcedWorktreeSessions)
772
- forcedWorktreeSessions.clear()
773
- await reconcileWorktreePass(forced, era, common)
774
- } while (forcedWorktreeSessions.size)
775
- })().finally(() => { if (worktreeReconcileFlight === flight) worktreeReconcileFlight = null })
776
- worktreeReconcileFlight = flight
777
- void flight.catch((error) => {
778
- console.warn(`spec-cli: worktree watcher reconciliation failed — ${error instanceof Error ? error.message : String(error)}`)
779
- })
780
- return flight
781
- }
782
- const WORKTREE_REGISTRY_OBSERVER = 'graph:worktree-registry'
783
- let deferredRegistryChange = false
784
- let deferredRegistryTimer: ReturnType<typeof setTimeout> | null = null
785
-
786
- function flushDeferredRegistryChange(): void {
787
- if (!deferredRegistryChange) return
788
- deferredRegistryChange = false
789
- if (deferredRegistryTimer) { clearTimeout(deferredRegistryTimer); deferredRegistryTimer = null }
790
- void reconcileWorktrees()
791
- fireChanged('full', 'all')
792
- }
793
-
794
- // A crashed or disconnected create request must not leave the registry event deferred forever. The normal
795
- // route flush is immediate; this bounded fallback preserves the watcher contract if the process never reaches
796
- // its response boundary.
797
- function deferRegistryChange(): void {
798
- deferredRegistryChange = true
799
- if (deferredRegistryTimer) return
800
- deferredRegistryTimer = setTimeout(() => {
801
- deferredRegistryTimer = null
802
- flushDeferredRegistryChange()
803
- }, 10_000)
804
- deferredRegistryTimer.unref?.()
805
- }
806
-
807
- export function flushDeferredWorktreeRegistryChange(): void {
808
- flushDeferredRegistryChange()
809
- }
810
-
811
- export function watchSessionEvalRegistry(
812
- dir: string,
813
- onInput: (event: 'rename' | 'change', relativePath: string) => void,
814
- onFailure: (error: Error) => void,
815
- ): TreeWatcherRegistry {
816
- let ready = false
817
- let attachError: Error | null = null
818
- const registry = new TreeWatcherRegistry({
819
- root: dir,
820
- source: 'worktree-registry',
821
- scope: 'full',
822
- recursive: false,
823
- onInput: (_event, relativePath) => onInput(_event, relativePath),
824
- onFailure: (error) => {
825
- if (ready) onFailure(error)
826
- else attachError = error
827
- },
828
- })
829
- if (!registry.refresh()) throw attachError ?? new Error(`spec-cli: graph watcher 'worktree-registry' failed at ${dir}`)
830
- ready = true
831
- return registry
832
- }
833
-
834
- function registryWatcherFailed(error: Error): void {
835
- registryWatcher = null
836
- registryReady = false
837
- noteSourceFailure('worktree-registry', error)
838
- if (holdSessionEvalProjectionObserver(WORKTREE_REGISTRY_OBSERVER, 'all')) fireChanged('full')
839
- }
840
-
841
- async function ensureWorktreeRegistry(forceSessionId?: string): Promise<void> {
842
- const common = activeCommonRoot
843
- if (!common) return
844
- const dir = resolve(join(common, 'worktrees'))
845
- // The registry watcher already reconciles add/remove events. Re-scanning every ordinary graph/evals read
846
- // turns a large worktree registry into an artificial request latency floor; only a scoped read may demand
847
- // one target after startup, while the unscoped hot path reuses the attached live watchers.
848
- // an attached registry short-circuits the scan for ordinary reads, but a REPAIR pass exists precisely to
849
- // reattach held worktrees, so it must reach the reconcile even when the registry itself is healthy.
850
- if (registryWatcher?.root === dir) {
851
- if (forceSessionId || repairing) await reconcileWorktrees(forceSessionId)
852
- return
853
- }
854
- if (registryWatcher) { registryWatcher.close(); registryWatcher = null; registryReady = false }
855
- // A platform may reject registry watches. Once the initial reconciliation has run, ordinary
856
- // reads must not repeat its full worktree scan while the repair pass is pending; scoped reads can
857
- // still demand their target explicitly.
858
- if (registryReady && !forceSessionId && !repairing) return
859
- if (registryReady && (forceSessionId || repairing)) { await reconcileWorktrees(forceSessionId); return }
860
- if (isDisabled('worktrees')) {
861
- registryReady = true
862
- if (holdSessionEvalProjectionObserver(WORKTREE_REGISTRY_OBSERVER, 'all')) fireChanged('full')
863
- return
864
- }
865
- if (!mayAttach('worktree-registry')) return
866
- registryReady = true
867
- try {
868
- try { mkdirSync(dir, { recursive: true }) }
869
- catch (error) { console.error(`spec-cli: graph watcher 'worktree-registry' could not create ${dir}: ${error instanceof Error ? error.message : String(error)}`) }
870
- // a registry add/remove is itself a 'full' change (a new/gone worktree reshapes the overlay); also
871
- // reconcile the per-worktree `.spec` watchers on every registry event.
872
- registryWatcher = watchSessionEvalRegistry(dir, (_event, relativePath) => {
873
- if (isSessionCreateCandidateRegistryEvent(relativePath, pendingSessionCreateWorktreePaths())) {
874
- deferRegistryChange()
875
- return
876
- }
877
- void reconcileWorktrees()
878
- fireChanged('full', 'all')
879
- }, registryWatcherFailed)
880
- noteSourceHealthy('worktree-registry')
881
- } catch (error) {
882
- registryWatcher = null
883
- registryReady = false
884
- noteSourceFailure('worktree-registry', error)
885
- if (holdSessionEvalProjectionObserver(WORKTREE_REGISTRY_OBSERVER, 'all')) fireChanged('full')
886
- }
887
- await reconcileWorktrees(forceSessionId) // attach for the live/demanded worktrees that already exist
888
- if (registryWatcher && releaseSessionEvalProjectionObserver(WORKTREE_REGISTRY_OBSERVER)) fireChanged('full')
889
- }
890
-
891
- // Attach the canonical filesystem sources before an HTTP snapshot starts summary work. This closes the
892
- // request→SSE handoff gap: an edit after the snapshot build has a watcher before it can occur. A source
893
- // the platform refused is NOT retried here — it is held for the repair scheduler, so an HTTP read never
894
- // becomes a reattach loop.
895
- export async function ensureBoardFileWatchers(forceSessionId?: string): Promise<void> {
896
- const storeRoot = resolve(sessionsRoot())
897
- const commonRoot = resolve(gitCommonDir())
898
- if ((activeStoreRoot && activeStoreRoot !== storeRoot) || (activeCommonRoot && activeCommonRoot !== commonRoot))
899
- closeBoardFileWatchers()
900
- activeStoreRoot = storeRoot
901
- activeCommonRoot = commonRoot
902
- ensureWatcher(storeRoot)
903
- ensureRefsWatcher(commonRoot)
904
- await ensureWorktreeRegistry(forceSessionId)
905
- }
906
-
907
- export function closeBoardFileWatchers(): void {
908
- watcherEra++
909
- if (repairTimer) { clearTimeout(repairTimer); repairTimer = null }
910
- if (deferredRegistryTimer) { clearTimeout(deferredRegistryTimer); deferredRegistryTimer = null }
911
- deferredRegistryChange = false
912
- heldSources.clear()
913
- repairStep = 0
914
- repairing = false
915
- forcedWorktreeSessions.clear()
916
- worktreeReconcileFlight = null
917
-
918
- storeWatcher?.close()
919
- storeWatcher = null
920
- refsWatchers?.close()
921
- refsWatchers = null
922
- registryWatcher?.close()
923
- registryWatcher = null
924
- registryReady = false
925
- for (const [name, row] of worktreeWatchers) {
926
- row.close()
927
- releaseSessionEvalProjectionObserver(worktreeObserver(name))
928
- }
929
- worktreeWatchers.clear()
930
- releaseSessionEvalProjectionObserver(REFS_OBSERVER)
931
- releaseSessionEvalProjectionObserver(WORKTREE_REGISTRY_OBSERVER)
932
- activeStoreRoot = null
933
- activeCommonRoot = null
934
- }
935
-
936
- // ---- event source 4: the two-tier tmux-derived pollers (liveness + activity — never a file write) → 'sessions' ----
937
- // The signals a store watch can't see are tmux-derived, and they split by cost ([[sessions]]): a HOT 100ms
938
- // poll of a cheap syscall-only fingerprint (a socket dying, a listener wedging) and a WARM 1s poll of the
939
- // pane-title self-summaries (a headline change is a tmux round-trip, too dear at 100ms). Both fire 'sessions'.
940
- let hotPoller: ReturnType<typeof setInterval> | null = null
941
- let warmPoller: ReturnType<typeof setInterval> | null = null
942
- let lastHot = ''
943
- let lastWarm = ''
944
- function ensurePollers(): void {
945
- if (!hotPoller) hotPoller = setInterval(() => {
946
- void hotSignature().then((sig) => {
947
- if (sig !== lastHot) { lastHot = sig; void reconcileWorktrees(); fireChanged('sessions') }
948
- }).catch(() => {})
949
- }, 100)
950
- if (!warmPoller) warmPoller = setInterval(() => {
951
- void warmSignature().then((sig) => { if (sig !== lastWarm) { lastWarm = sig; fireChanged('sessions') } }).catch(() => {})
952
- }, 1000)
953
- }
954
-
955
- // ---- event source 5: the cold-tick PATROL — the server-side replacement for every client's slow fallback
956
- // poll, AND the self-heal authority. A relaxed tick asks graph-cache's ONE flight to compare its board-input
957
- // revision: unchanged state returns the anchor without assembly, while an unobserved ref/worktree/store move
958
- // escalates there to a full rebuild. The 'patrol' tag keeps that repair accountable. Delta-gated: plain-only
959
- // clients keep their own client-side fallback, so without delta subscribers this does no validation work.
960
- let coldTick: ReturnType<typeof setInterval> | null = null
961
- function ensureColdTick(): void {
962
- if (coldTick) return
963
- coldTick = setInterval(() => {
964
- if (!deltaSubs.size) return
965
- triggerTags.add('patrol')
966
- void rebuildAndBroadcast(true)
967
- }, 15000)
968
- }
969
-
970
- function stopSourcesIfIdle(): void {
971
- // the delta anchor dies with its era's last subscriber: past this point changes invalidate the cache but
972
- // never rebuild, so lastFullFrame would drift arbitrarily far from the real board — and the next era's
973
- // first subscriber would be anchored on it (its warm-terminal set then drops live sessions' panes, and the
974
- // client's recovery lanes can latch each other out — issue #70). A new era opens on a fresh build instead.
975
- if (deltaSubs.size === 0) { lastUnits = null; lastTag = ''; lastFullFrame = null; patrolPending = false }
976
- if (deltaSubs.size === 0) setSessionEvalProjectionWarmup(false)
977
- if (plainSubs.size + deltaSubs.size > 0) return
978
- if (hotPoller) { clearInterval(hotPoller); hotPoller = null; lastHot = '' }
979
- if (warmPoller) { clearInterval(warmPoller); warmPoller = null; lastWarm = '' }
980
- if (coldTick) { clearInterval(coldTick); coldTick = null }
981
- }
982
-
983
- // GET /api/graph/stream — one SSE per dashboard tab, server→client only, with a periodic `ping` so an
984
- // idle proxy never times the connection out. On a backend hot-reload the stream drops and EventSource
985
- // auto-reconnects to the fresh child; a delta subscriber's reconnect lands a fresh `graph-full`, so the
986
- // chain re-anchors with no client-side repair logic.
987
- export async function boardStream(c: Context) {
988
- const delta = c.req.query('mode') === 'delta'
989
- await ensureBoardFileWatchers()
990
- return streamSSE(c, async (stream) => {
991
- let aborted = false
992
- const send: DeltaSend = (frame) => { void stream.writeSSE(frame).catch(() => {}) }
993
- const notify: Notify = () => { void stream.writeSSE({ event: 'graph-changed', data: 'x' }).catch(() => {}) }
994
- if (delta) {
995
- deltaSubs.add(send)
996
- setSessionEvalProjectionWarmup(true)
997
- ensureColdTick()
998
- } else { plainSubs.add(notify) }
999
- ensurePollers()
1000
- const unsub = (): void => { deltaSubs.delete(send); plainSubs.delete(notify); stopSourcesIfIdle() }
1001
- stream.onAbort(() => { aborted = true; unsub() })
1002
- try {
1003
- await stream.writeSSE({ event: 'ready', data: 'x' })
1004
- if (delta) {
1005
- // seed the chain: the cached anchor snapshot immediately (same tag the next delta chains from),
1006
- // then a fire so a connect during a quiet stretch converges to truly-current within one build.
1007
- if (lastFullFrame) { await stream.writeSSE(lastFullFrame).catch(() => {}) ; fireChanged() }
1008
- else void rebuildAndBroadcast()
1009
- }
1010
- while (!aborted) {
1011
- // ping every 10s — the client's heartbeat contract is 2.5× this window ([[graph-stream]]), so a
1012
- // silent-death gap is caught inside one client watchdog interval, and idle proxies never time out.
1013
- await stream.sleep(10000)
1014
- if (aborted) break
1015
- await stream.writeSSE({ event: 'ping', data: 'x' })
1016
- }
1017
- } finally { unsub() }
1018
- })
1019
- }