spexcode 0.6.2 → 0.6.6

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 (495) hide show
  1. package/README.md +3 -1
  2. package/bin/spex.mjs +4 -0
  3. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +23 -0
  4. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +195 -0
  5. package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
  6. package/node_modules/@spexcode/session-core/dist/index.js +5 -0
  7. package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
  8. package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
  9. package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
  10. package/node_modules/@spexcode/session-core/dist/message.js +52 -0
  11. package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
  12. package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
  13. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +50 -0
  14. package/node_modules/@spexcode/session-core/dist/runtime-session.js +286 -0
  15. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +14 -0
  16. package/node_modules/@spexcode/session-core/dist/session-cursors.js +82 -0
  17. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +46 -0
  18. package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
  19. package/node_modules/@spexcode/session-core/package.json +33 -0
  20. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +77 -0
  21. package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
  22. package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
  23. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +36 -0
  24. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +316 -0
  25. package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
  26. package/node_modules/@spexcode/spec-cli/dist/cli.js +1908 -0
  27. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +112 -0
  28. package/node_modules/@spexcode/spec-cli/dist/client.js +481 -0
  29. package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
  30. package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
  31. package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
  32. package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
  33. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +58 -0
  34. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +798 -0
  35. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
  36. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
  37. package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
  38. package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
  39. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
  40. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
  41. package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
  42. package/node_modules/@spexcode/spec-cli/dist/doctor.js +553 -0
  43. package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
  44. package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
  45. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
  46. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
  47. package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
  48. package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
  49. package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
  50. package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
  51. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
  52. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
  53. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
  54. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +363 -0
  55. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +38 -0
  56. package/node_modules/@spexcode/spec-cli/dist/gateway.js +527 -0
  57. package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
  58. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +717 -0
  59. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
  60. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +36 -0
  61. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
  62. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
  63. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
  64. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
  65. package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
  66. package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +36 -32
  67. package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
  68. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
  69. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +338 -0
  70. package/node_modules/@spexcode/spec-cli/dist/harness.js +3146 -0
  71. package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
  72. package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
  73. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +15 -0
  74. package/node_modules/@spexcode/spec-cli/dist/help.js +594 -0
  75. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
  76. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
  77. package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
  78. package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
  79. package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
  80. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
  81. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
  82. package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
  83. package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
  84. package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
  85. package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
  86. package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
  87. package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
  88. package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
  89. package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
  90. package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
  91. package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
  92. package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
  93. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +8 -0
  94. package/node_modules/@spexcode/spec-cli/dist/listen.js +24 -0
  95. package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
  96. package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
  97. package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
  98. package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
  99. package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
  100. package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
  101. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
  102. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
  103. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
  104. package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
  105. package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
  106. package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
  107. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +5 -0
  108. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +215 -0
  109. package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
  110. package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
  111. package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
  112. package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
  113. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
  114. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
  115. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
  116. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
  117. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
  118. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
  119. package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
  120. package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
  121. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
  122. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
  123. package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
  124. package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
  125. package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
  126. package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
  127. package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
  128. package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
  129. package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
  130. package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
  131. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
  132. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
  133. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
  134. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
  135. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.d.ts +1 -0
  136. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -0
  137. package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
  138. package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
  139. package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
  140. package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
  141. package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
  142. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
  143. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
  144. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
  145. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
  146. package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
  147. package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
  148. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
  149. package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
  150. package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
  151. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +11 -0
  152. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +20 -0
  153. package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
  154. package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
  155. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +433 -0
  156. package/node_modules/@spexcode/spec-cli/dist/sessions.js +4667 -0
  157. package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
  158. package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
  159. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
  160. package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
  161. package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
  162. package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
  163. package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
  164. package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
  165. package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
  166. package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
  167. package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
  168. package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
  169. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
  170. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
  171. package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
  172. package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
  173. package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
  174. package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
  175. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
  176. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
  177. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
  178. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +1 -1
  179. package/node_modules/@spexcode/spec-cli/package.json +49 -0
  180. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  181. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  182. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  183. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  184. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  185. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  186. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  187. package/node_modules/@spexcode/spec-core/dist/git.d.ts +170 -0
  188. package/node_modules/@spexcode/spec-core/dist/git.js +2768 -0
  189. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  190. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  191. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  192. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  193. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  194. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  195. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  196. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  197. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  198. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  199. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  200. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  201. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +187 -0
  202. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  203. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  204. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  205. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  206. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  207. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  208. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  209. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  210. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  211. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  212. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  213. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  214. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  215. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  216. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  217. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  218. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  219. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  220. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  221. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  222. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  223. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  224. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  225. package/node_modules/@spexcode/spec-core/package.json +28 -0
  226. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  227. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  228. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  229. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  230. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  231. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  232. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  233. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  234. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  235. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  236. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  237. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  238. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  239. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  240. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  241. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  242. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  243. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  244. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  245. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  246. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  247. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  248. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  249. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  250. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  251. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  252. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  253. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  254. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  255. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  256. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  257. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  258. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  259. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  260. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  261. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  262. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  263. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  264. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  265. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  266. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  267. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  268. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  269. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  270. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  271. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  272. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  273. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  274. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  275. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  276. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  277. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  278. package/package.json +33 -19
  279. package/spec-cli/bin/spex.mjs +0 -69
  280. package/spec-cli/src/anchors.ts +0 -728
  281. package/spec-cli/src/attach.ts +0 -43
  282. package/spec-cli/src/claude-headless.ts +0 -321
  283. package/spec-cli/src/cli.ts +0 -1258
  284. package/spec-cli/src/client.ts +0 -414
  285. package/spec-cli/src/cockpit.ts +0 -43
  286. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  287. package/spec-cli/src/commit-surgery.ts +0 -68
  288. package/spec-cli/src/contract-filter.ts +0 -202
  289. package/spec-cli/src/delivery-queue.ts +0 -165
  290. package/spec-cli/src/doctor.ts +0 -516
  291. package/spec-cli/src/execution-trace.ts +0 -444
  292. package/spec-cli/src/file-write.ts +0 -22
  293. package/spec-cli/src/gateway-auth.ts +0 -181
  294. package/spec-cli/src/gateway-hub.ts +0 -358
  295. package/spec-cli/src/gateway.ts +0 -461
  296. package/spec-cli/src/git.ts +0 -2556
  297. package/spec-cli/src/graph.ts +0 -271
  298. package/spec-cli/src/graphCache.ts +0 -710
  299. package/spec-cli/src/graphDelta.ts +0 -77
  300. package/spec-cli/src/graphStream.ts +0 -1019
  301. package/spec-cli/src/harness-select.ts +0 -77
  302. package/spec-cli/src/harness.ts +0 -3025
  303. package/spec-cli/src/headless-controller.ts +0 -63
  304. package/spec-cli/src/help.ts +0 -492
  305. package/spec-cli/src/host-resources.ts +0 -762
  306. package/spec-cli/src/host.ts +0 -611
  307. package/spec-cli/src/identity-presets.d.ts +0 -13
  308. package/spec-cli/src/identity-presets.js +0 -138
  309. package/spec-cli/src/index.ts +0 -818
  310. package/spec-cli/src/init.ts +0 -258
  311. package/spec-cli/src/issues-cli.ts +0 -276
  312. package/spec-cli/src/issues.ts +0 -237
  313. package/spec-cli/src/layout.ts +0 -640
  314. package/spec-cli/src/lint.ts +0 -488
  315. package/spec-cli/src/listen.ts +0 -28
  316. package/spec-cli/src/localIssues.ts +0 -538
  317. package/spec-cli/src/materialize.ts +0 -417
  318. package/spec-cli/src/mentions.ts +0 -81
  319. package/spec-cli/src/opencode-headless.ts +0 -205
  320. package/spec-cli/src/pi-headless.ts +0 -153
  321. package/spec-cli/src/plugin-harness.ts +0 -157
  322. package/spec-cli/src/process-identity.ts +0 -207
  323. package/spec-cli/src/project-identity.ts +0 -73
  324. package/spec-cli/src/project-store.ts +0 -17
  325. package/spec-cli/src/pty-bridge.ts +0 -396
  326. package/spec-cli/src/pty-helper.mjs +0 -96
  327. package/spec-cli/src/pty-native-helper.mjs +0 -22
  328. package/spec-cli/src/ranker.ts +0 -191
  329. package/spec-cli/src/reaper.ts +0 -125
  330. package/spec-cli/src/resilience.ts +0 -41
  331. package/spec-cli/src/reviewSnapshot.ts +0 -28
  332. package/spec-cli/src/reviews.ts +0 -434
  333. package/spec-cli/src/runtime-guard.ts +0 -35
  334. package/spec-cli/src/runtime-ownership.ts +0 -88
  335. package/spec-cli/src/search.bench.mjs +0 -80
  336. package/spec-cli/src/search.ts +0 -63
  337. package/spec-cli/src/session-cursors.ts +0 -91
  338. package/spec-cli/src/session-execution.ts +0 -68
  339. package/spec-cli/src/session-files.ts +0 -121
  340. package/spec-cli/src/session-follow.ts +0 -182
  341. package/spec-cli/src/session-reparent.ts +0 -33
  342. package/spec-cli/src/session-timeline.ts +0 -247
  343. package/spec-cli/src/session-web.ts +0 -135
  344. package/spec-cli/src/sessions.ts +0 -3883
  345. package/spec-cli/src/sh.ts +0 -3
  346. package/spec-cli/src/slash-commands.ts +0 -318
  347. package/spec-cli/src/source-files.ts +0 -104
  348. package/spec-cli/src/specs.ts +0 -498
  349. package/spec-cli/src/supervise.ts +0 -245
  350. package/spec-cli/src/tree.ts +0 -118
  351. package/spec-cli/src/tsx-bin.ts +0 -18
  352. package/spec-cli/src/uninstall.ts +0 -171
  353. package/spec-cli/src/uploads.ts +0 -253
  354. package/spec-cli/src/worktree-sources.ts +0 -42
  355. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  356. package/spec-dashboard/dist/assets/App-B6IbFbGu.js +0 -2
  357. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  358. package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +0 -27
  359. package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +0 -2
  360. package/spec-dashboard/dist/assets/IssuesPage-BG9rNOgV.js +0 -1
  361. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  404. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  405. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  406. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  407. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  408. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  409. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  410. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  411. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  412. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  413. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  414. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  415. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  416. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  417. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  418. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  419. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  420. package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +0 -2
  421. package/spec-dashboard/dist/assets/Modal-B3EcgeA3.js +0 -1
  422. package/spec-dashboard/dist/assets/PageScroll-CNZOugWV.js +0 -1
  423. package/spec-dashboard/dist/assets/ProjectsPage-C2CRHmvs.js +0 -1
  424. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  425. package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +0 -40
  426. package/spec-dashboard/dist/assets/SessionWindow-1nYgZ4CL.js +0 -1
  427. package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +0 -1
  428. package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +0 -13
  429. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  430. package/spec-dashboard/dist/assets/data-SlvxNA2r.js +0 -1
  431. package/spec-dashboard/dist/assets/index-DLVeOHL-.js +0 -41
  432. package/spec-dashboard/dist/assets/index-DYLGfPjx.css +0 -1
  433. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  434. package/spec-dashboard/dist/assets/launch-yxCYr64x.js +0 -276
  435. package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +0 -1
  436. package/spec-dashboard/dist/index.html +0 -29
  437. package/spec-dashboard/src/reviewFilters.js +0 -322
  438. package/spec-dashboard/src/reviewQuery.js +0 -174
  439. package/spec-dashboard/src/session.js +0 -199
  440. package/spec-eval/src/cache.ts +0 -57
  441. package/spec-eval/src/cli.ts +0 -894
  442. package/spec-eval/src/evaltab.ts +0 -264
  443. package/spec-eval/src/filing.ts +0 -39
  444. package/spec-eval/src/freshness.ts +0 -836
  445. package/spec-eval/src/humanok.ts +0 -28
  446. package/spec-eval/src/scenariofresh.ts +0 -244
  447. package/spec-eval/src/scenarios.ts +0 -672
  448. package/spec-eval/src/sessioneval.ts +0 -2220
  449. package/spec-eval/src/sidecar.ts +0 -93
  450. package/spec-eval/src/timeline.ts +0 -54
  451. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  452. package/spec-forge/src/cache.ts +0 -66
  453. package/spec-forge/src/cli.ts +0 -99
  454. package/spec-forge/src/drivers/github.ts +0 -132
  455. package/spec-forge/src/drivers/gitlab.ts +0 -153
  456. package/spec-forge/src/drivers.ts +0 -73
  457. package/spec-forge/src/links.ts +0 -72
  458. package/spec-forge/src/needs-eval.ts +0 -29
  459. package/spec-forge/src/port.ts +0 -36
  460. package/spec-forge/src/resident.ts +0 -46
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  480. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  481. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  482. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  483. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  484. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec.md +0 -0
  485. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -0
  486. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -0
  487. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -0
  488. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -0
  489. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  490. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  491. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  492. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  493. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  494. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  495. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -0,0 +1,582 @@
1
+ // @@@ the local issue store - the LOCAL store of the one Issue object ([[issues]] / [[local-issues]] /
2
+ // [[mentions]]). A thread IS an Issue whose `store` is 'local' — membership implied by WHERE the file lives,
3
+ // never written into it. One thread = one PLAIN markdown file at <main>/.spec/.issues/<id>.md; there is
4
+ // deliberately no content-kind taxonomy (a change suggestion, an annotation, a Q&A are the same mechanism —
5
+ // the prose says what it is). Others reply/discuss like an async chatroom; a supervisor drains it via
6
+ // `spex issue ls` (reading is the port's job, issues.ts — this module owns only the store + its write verbs).
7
+ // Because a thread file is NOT named spec.md, the spec walk never nodes it and isSpecMd ignores it —
8
+ // invisible to lint / drift / deriveStatus / board with ZERO exemption. The store lives on the TRUNK, not
9
+ // per-branch: reads and writes target the main checkout and commit STRAIGHT to it (--no-verify, provably
10
+ // store-only), so a post-merge thread lands durably even though the author's own branch already merged.
11
+ // A committing write is allowed ONLY from the trunk checkout itself (isPrimaryCheckout) — a linked-worktree
12
+ // backend sharing that main is refused loud, never left to fabricate a stray commit on a main it doesn't own;
13
+ // SPEXCODE_ISSUES_DIR routes an e2e/test rig to a disposable plain-file store (see localStoreDir below).
14
+ // The on-disk dir was historically `.spec/.forum`; a one-shot self-migration ([[issues-store-rename]])
15
+ // renames any legacy `.spec/.forum` to `.spec/.issues` on the first store touch after a toolchain update.
16
+ import { readdirSync, existsSync, mkdirSync, writeFileSync, readFileSync, rmdirSync, statSync } from 'node:fs';
17
+ import { join, dirname, resolve as resolvePath } from 'node:path';
18
+ import { git, headSha, repoRoot } from '@spexcode/spec-core';
19
+ import { mainCheckout, envSessionId, readConfig } from '@spexcode/spec-core';
20
+ import { dispatchNewMentions, parseMentions } from './mentions.js';
21
+ const LOCAL_STORE_REL = '.spec/.issues';
22
+ // the pre-rename dir ([[issues-store-rename]]); ensureStoreMigrated() renames it to LOCAL_STORE_REL once.
23
+ const LEGACY_STORE_REL = '.spec/.forum';
24
+ // @@@ the on/off switch - the issues workflow is an OPT-OUTABLE feature (default ON). The single source of
25
+ // truth is `spexcode.json`'s `issues.enabled` (the same settings file that carries every other toggle),
26
+ // read via readConfig so a machine-local `spexcode.local.json` can override it. OFF silences the post-merge
27
+ // nudge (and, in the dashboard, hides the issues view); the raw write verbs stay usable, since running one
28
+ // is explicit consent. There is deliberately NO CLI toggle verb (v0.3.0 — "no `spex config set`" is this
29
+ // project's standing rule): flip the key by editing the JSON, effective immediately because readConfig
30
+ // reads the working tree. A legacy pre-rename `proposals.enabled` key is NOT read (no fallback — fail
31
+ // toward the default); `spex doctor` reports it so an old settings file gets repaired, not silently obeyed.
32
+ export const issuesEnabled = () => readConfig(mainCheckout()).issues?.enabled ?? true;
33
+ const list = (v) => v ? v.split(',').map((s) => s.trim()).filter(Boolean) : [];
34
+ // @@@ where a store write lands — ONE deterministic rule ([[local-issues]] "only the trunk checkout may
35
+ // commit"). Every backend resolves the store to `mainCheckout()` (dirname of the shared git-common-dir), so a
36
+ // backend serving a LINKED WORKTREE would git-commit a stray issue onto the REAL, live main — dirtying it and
37
+ // racing its index.lock. The rule:
38
+ // 1. SPEXCODE_ISSUES_DIR override → a DISPOSABLE store of plain files (no git, no shared main); the e2e/test
39
+ // seam. Both reads and writes target it, so a test rig throws its whole store away.
40
+ // 2. else the trunk `.spec/.issues/` — but a *committing* write is allowed ONLY from the trunk checkout
41
+ // itself (repoRoot === mainCheckout: the primary backend, or a throwaway clone owning its own main). A
42
+ // linked-worktree backend is refused loud (requirePrimaryStore) rather than silently writing someone
43
+ // else's main. Reads always resolve to the trunk (visibility never hostages on being primary).
44
+ const overrideStoreDir = () => {
45
+ const p = process.env.SPEXCODE_ISSUES_DIR?.trim();
46
+ return p ? p : null;
47
+ };
48
+ // is THIS process rooted in the trunk checkout itself? True for the primary backend and for a clone (its own
49
+ // .git → its own disposable main); false for a linked-worktree backend, the store-write footgun.
50
+ const isPrimaryCheckout = () => {
51
+ try {
52
+ return resolvePath(repoRoot()) === resolvePath(mainCheckout());
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ };
58
+ // fail LOUD before a committing write from a non-trunk checkout: never fabricate a commit on a shared main
59
+ // this process does not own. (The disposable override bypasses this — it commits to nothing.)
60
+ function requirePrimaryStore(action) {
61
+ if (isPrimaryCheckout())
62
+ return;
63
+ throw new Error(`refusing to ${action}: this backend serves a linked worktree (${resolvePath(repoRoot())}), not the trunk ` +
64
+ `checkout (${resolvePath(mainCheckout())}) — committing here would land a stray issue on the REAL main and ` +
65
+ `race its index. Run the write from the trunk checkout (or its backend), or, for a throwaway/e2e run, set ` +
66
+ `SPEXCODE_ISSUES_DIR=<disposable-dir> to write plain files nowhere near the real store.`);
67
+ }
68
+ // the local issue store dir — a fixed path directly under the trunk's .spec (name-independent, unlike the
69
+ // .plugins system which nests under the named root node), OR the disposable override. Every read and write goes here.
70
+ const localStoreDir = () => overrideStoreDir() ?? join(mainCheckout(), LOCAL_STORE_REL);
71
+ // the author's signature: the effective governed session id (envSessionId handles the claude/codex split).
72
+ export const currentSession = () => envSessionId() || 'unknown';
73
+ // a synchronous sleep for the commit-retry backoff (Date/timers-free, safe in any runtime).
74
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
75
+ // ── file format ──────────────────────────────────────────────────────────────────────────────────────
76
+ // frontmatter (concern/by/status/nodes/evidence/created) + a prose body, then any replies — each
77
+ // preceded by a `<!-- reply: <by> @ <iso> -->` sentinel: invisible in rendered markdown, unambiguous to
78
+ // parse. Only the store writes these files, so a fixed shape is safe.
79
+ //
80
+ // A REMARK ([[remark-substrate]]) is a reply carrying extra state, appended to the SAME sentinel as a
81
+ // ` :: <space-joined k=v attrs>` tail (a plain reply has no tail → parses unchanged, backward compatible):
82
+ // rid=<id> stable per-remark id — its presence marks the reply a remark
83
+ // sha=<targetCodeSha> the reading the remark was authored against
84
+ // resolved=<by>@<at> present only once resolved (absent ⟹ resolved:false)
85
+ const REPLY_RE = /^<!-- reply: (.+?) @ (.+?)(?: :: (.+))? -->$/;
86
+ function parse(id, text) {
87
+ const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
88
+ const fm = {};
89
+ for (const line of (m ? m[1] : '').split('\n')) {
90
+ const mm = line.match(/^([a-zA-Z]+):\s*(.*)$/);
91
+ if (mm)
92
+ fm[mm[1]] = mm[2].trim();
93
+ }
94
+ const body = [];
95
+ const replies = [];
96
+ let cur = null;
97
+ for (const line of (m ? m[2] : text).replace(/^\n+/, '').split('\n')) {
98
+ const rm = line.match(REPLY_RE);
99
+ if (rm) {
100
+ cur = { by: rm[1], at: rm[2], body: '', ...parseRemarkAttrs(rm[3]) };
101
+ replies.push(cur);
102
+ continue;
103
+ }
104
+ if (cur)
105
+ cur.body += (cur.body ? '\n' : '') + line;
106
+ else
107
+ body.push(line);
108
+ }
109
+ return {
110
+ id,
111
+ store: 'local',
112
+ concern: fm.concern || id,
113
+ by: fm.by || 'unknown',
114
+ status: fm.status === 'landed' ? 'landed' : 'open',
115
+ nodes: list(fm.nodes),
116
+ created: fm.created || '',
117
+ body: body.join('\n').trim(),
118
+ replies: replies.map((r) => ({ ...r, body: r.body.trim() })),
119
+ evidence: list(fm.evidence),
120
+ labels: [],
121
+ };
122
+ }
123
+ // user body text must never FORGE a reply sentinel: a line matching `<!-- reply: … -->` in a body would be
124
+ // re-read as a thread boundary, splitting the thread and truncating the body. Neutralize the marker in user
125
+ // content (a zero-width space breaks the `<!-- reply:` prefix — invisible on render, idempotent), so only
126
+ // serialize's OWN sentinels parse as boundaries. Frontmatter scalars are single-line-stripped for the same
127
+ // reason (a newline/`---` in a concern can't break the block).
128
+ const safeBody = (t) => t.trim().replace(/<!-- reply:/g, '<!--​reply:');
129
+ const safeScalar = (t) => t.replace(/[\r\n]+/g, ' ').trim();
130
+ // the ` :: k=v k=v` remark tail on a reply sentinel ↔ the reply's remark fields. `undefined` attrs (a plain
131
+ // reply) → no remark fields; `rid` present → the reply IS a remark ([[remark-substrate]]). Values are
132
+ // space-free (ids, a codeSha, a session id, an ISO instant), so a space-split is unambiguous.
133
+ function parseRemarkAttrs(attrs) {
134
+ if (!attrs)
135
+ return {};
136
+ const kv = new Map();
137
+ for (const tok of attrs.trim().split(/\s+/)) {
138
+ const i = tok.indexOf('=');
139
+ if (i > 0)
140
+ kv.set(tok.slice(0, i), tok.slice(i + 1));
141
+ }
142
+ if (!kv.has('rid'))
143
+ return {};
144
+ const out = { rid: kv.get('rid'), targetCodeSha: kv.get('sha') ?? '', resolved: false };
145
+ const r = kv.get('resolved');
146
+ if (r) {
147
+ const at = r.indexOf('@');
148
+ out.resolved = true;
149
+ out.resolvedBy = r.slice(0, at);
150
+ out.resolvedAt = r.slice(at + 1);
151
+ }
152
+ return out;
153
+ }
154
+ function serializeRemarkAttrs(r) {
155
+ if (r.rid === undefined)
156
+ return '';
157
+ const parts = [`rid=${r.rid}`, `sha=${r.targetCodeSha ?? ''}`];
158
+ if (r.resolved)
159
+ parts.push(`resolved=${r.resolvedBy ?? ''}@${r.resolvedAt ?? ''}`);
160
+ return ` :: ${parts.join(' ')}`;
161
+ }
162
+ function serialize(p) {
163
+ const fm = [
164
+ `concern: ${safeScalar(p.concern)}`,
165
+ `by: ${safeScalar(p.by)}`,
166
+ `status: ${safeScalar(p.status)}`,
167
+ p.nodes.length ? `nodes: ${p.nodes.join(', ')}` : '',
168
+ p.evidence.length ? `evidence: ${p.evidence.join(', ')}` : '',
169
+ `created: ${p.created}`,
170
+ ].filter(Boolean);
171
+ let out = `---\n${fm.join('\n')}\n---\n\n${safeBody(p.body)}\n`;
172
+ for (const r of p.replies)
173
+ out += `\n<!-- reply: ${safeScalar(r.by)} @ ${safeScalar(r.at)}${serializeRemarkAttrs(r)} -->\n${safeBody(r.body)}\n`;
174
+ return out;
175
+ }
176
+ export function loadLocalIssues() {
177
+ ensureStoreMigrated(); // read path: a pre-rename deployment migrates on first read, so no thread is lost
178
+ const dir = localStoreDir();
179
+ if (!existsSync(dir))
180
+ return [];
181
+ return readdirSync(dir, { withFileTypes: true })
182
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
183
+ .map((e) => parse(e.name.replace(/\.md$/, ''), readFileSync(join(dir, e.name), 'utf8')))
184
+ .sort((a, b) => a.created.localeCompare(b.created));
185
+ }
186
+ export function loadOne(id) {
187
+ ensureStoreMigrated();
188
+ const f = join(localStoreDir(), `${id}.md`);
189
+ if (!existsSync(f))
190
+ throw new Error(`no local issue '${id}' (see \`spex issue ls --all --store local\`)`);
191
+ return parse(id, readFileSync(f, 'utf8'));
192
+ }
193
+ // a filesystem-safe, readable, collision-free id from the concern (slug + numeric suffix if taken).
194
+ // RESERVED_IDS are the words an issue address gives its own meaning — `new` is the compose PAGE
195
+ // ([[issues-view]]), so an issue owning that id would be unreachable at its own detail address. The slug
196
+ // steps around them with the same numeric suffix a taken id gets: a collision, not a silent shadow.
197
+ const RESERVED_IDS = new Set(['new']);
198
+ function uniqueId(concern) {
199
+ const base = concern.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'issue';
200
+ const dir = localStoreDir();
201
+ let id = base;
202
+ for (let n = 2; RESERVED_IDS.has(id) || existsSync(join(dir, `${id}.md`)); n++)
203
+ id = `${base}-${n}`;
204
+ return id;
205
+ }
206
+ // @@@ store lock - local-issue writes contend two ways: git commits share the repo index.lock, and a
207
+ // reply's read-modify-write of one thread file loses an update if two race (both read the base, last write
208
+ // wins). A single cross-process lock serializes the WHOLE prepare→write→commit, killing both. It is an
209
+ // atomic `mkdir` (in .git, never committed); a lock whose holder crashed is stolen after it goes stale.
210
+ // Because the commit is `--no-verify` fast (below), serialized writes stay quick even under a burst. The
211
+ // lock FILE keeps its historical `spexcode-forum.lock` name ON PURPOSE: during the one-shot store-dir
212
+ // migration ([[issues-store-rename]]) an old and a new toolchain can run against the SAME checkout at once,
213
+ // and they mutually exclude only while contending on the SAME lock name — renaming it would open exactly
214
+ // the migration race the lock exists to close.
215
+ let lockHeld = false; // re-entrancy guard so ensureStoreMigrated() no-ops when reached from inside a hold
216
+ function withStoreLock(fn) {
217
+ // the disposable store locks inside its own dir; the trunk store locks in the shared `.git` (one lock name
218
+ // across every worktree of the clone, so all writers of the SAME real store mutually exclude).
219
+ const override = overrideStoreDir();
220
+ const lock = override ? join(override, '.spexcode-issues.lock') : join(mainCheckout(), '.git', 'spexcode-forum.lock');
221
+ mkdirSync(dirname(lock), { recursive: true }); // parent must exist for the atomic mkdir-acquire (a no-op for .git)
222
+ for (let i = 0;; i++) {
223
+ try {
224
+ mkdirSync(lock);
225
+ break;
226
+ } // atomic acquire
227
+ catch {
228
+ try {
229
+ if (Date.now() - statSync(lock).mtimeMs > 20000)
230
+ rmdirSync(lock);
231
+ }
232
+ catch { /* released meanwhile */ }
233
+ sleep(40 + Math.floor(Math.random() * 80)); // spin with jitter until free / stolen
234
+ }
235
+ }
236
+ lockHeld = true;
237
+ try {
238
+ return fn();
239
+ }
240
+ finally {
241
+ lockHeld = false;
242
+ try {
243
+ rmdirSync(lock);
244
+ }
245
+ catch { /* already gone */ }
246
+ }
247
+ }
248
+ // @@@ one-shot store-dir migration ([[issues-store-rename]]) - the local issue store's on-disk dir was
249
+ // historically `.spec/.forum`; it is now `.spec/.issues`. Any pre-rename deployment migrates ITSELF on the
250
+ // first store touch after its toolchain updates: a single committed `git mv` on the trunk, so a thread's
251
+ // whole reply history reads identically afterwards (`git log --follow` traces through the rename — the data
252
+ // is git, and a rename preserves it). Called at every store entrypoint BEFORE the lock is taken; it takes
253
+ // the lock itself to do the mv, so the whole find→check→mv is atomic against a concurrent first-touch burst:
254
+ // exactly ONE mv commit, because a racer that waited on the lock re-checks under it and finds nothing to do.
255
+ // Fast path: no legacy dir → instant, no lock (the common case after migration / on a fresh repo). Both
256
+ // dirs present (pathological) → fail LOUD with the repair, never a silent merge.
257
+ function ensureStoreMigrated() {
258
+ if (lockHeld)
259
+ return; // inside a hold: an outer entrypoint already ran this
260
+ if (overrideStoreDir())
261
+ return; // disposable store: no legacy trunk dir to migrate
262
+ if (!isPrimaryCheckout())
263
+ return; // a git-committing migration is trunk-only; the primary migrates on its next touch
264
+ const root = mainCheckout();
265
+ if (!existsSync(join(root, LEGACY_STORE_REL)))
266
+ return; // fresh or already-migrated: nothing to do (no lock)
267
+ withStoreLock(() => {
268
+ const r = mainCheckout();
269
+ const legacy = join(r, LEGACY_STORE_REL), current = join(r, LOCAL_STORE_REL);
270
+ if (!existsSync(legacy))
271
+ return; // a racer migrated it while we waited on the lock
272
+ if (existsSync(current))
273
+ throw new Error(`both ${LEGACY_STORE_REL} and ${LOCAL_STORE_REL} exist in ${r} — refusing to auto-merge the local ` +
274
+ `issue store. Reconcile by hand: move any threads from ${LEGACY_STORE_REL} into ${LOCAL_STORE_REL}, ` +
275
+ `then \`git rm -r ${LEGACY_STORE_REL} && git commit\`, and re-run.`);
276
+ git(['-C', r, 'mv', LEGACY_STORE_REL, LOCAL_STORE_REL]);
277
+ git(['-C', r, 'commit', '--no-verify', '-m', `issues: store dir ${LEGACY_STORE_REL} → ${LOCAL_STORE_REL}`,
278
+ '--', LEGACY_STORE_REL, LOCAL_STORE_REL]);
279
+ });
280
+ }
281
+ // write + commit ONE store file STRAIGHT to the trunk. The commit is `--no-verify`: the file is DATA,
282
+ // structurally invisible to spec-lint, and the commit is provably store-only (one .spec/.issues/ path), so the
283
+ // pre-commit gate would only pass anyway — running it just burns seconds (tsx cold-start) holding the lock.
284
+ // A NO-CHANGE write is idempotent SUCCESS, never a failure: when the serialized bytes already equal the
285
+ // stored state (a duplicate close — the store IS the requested state), `git commit` would exit 1
286
+ // 'nothing to commit', so we detect the no-op after `add` (the staged path equals HEAD → status is silent)
287
+ // and skip the commit. Returns whether the store actually changed, so a caller can say 'already <state>'.
288
+ // MUST run while holding withStoreLock — it is the write half of a locked read-modify-write; its callers
289
+ // (commitStore, findOrCreateEvalThread) own the lock, so it never acquires one itself (mkdir is not re-entrant).
290
+ function writeStoreFile(p, message) {
291
+ const override = overrideStoreDir();
292
+ if (override) { // disposable store: a plain file, never a commit, never a shared main
293
+ mkdirSync(override, { recursive: true });
294
+ const f = join(override, `${p.id}.md`);
295
+ const data = serialize(p);
296
+ if (existsSync(f) && readFileSync(f, 'utf8') === data)
297
+ return false;
298
+ writeFileSync(f, data);
299
+ return true;
300
+ }
301
+ requirePrimaryStore('write a local issue'); // a linked-worktree backend must NOT commit onto the real main
302
+ const root = mainCheckout();
303
+ const rel = `${LOCAL_STORE_REL}/${p.id}.md`;
304
+ mkdirSync(join(root, LOCAL_STORE_REL), { recursive: true });
305
+ writeFileSync(join(root, rel), serialize(p));
306
+ git(['-C', root, 'add', '--', rel]);
307
+ if (!git(['-C', root, 'status', '--porcelain', '--', rel]).trim())
308
+ return false; // staged == HEAD: the no-op
309
+ git(['-C', root, 'commit', '--no-verify', '-m', message, '--', rel]);
310
+ return true;
311
+ }
312
+ // @@@ the shared trunk-data commit — the committing half of the store discipline, exported for OTHER
313
+ // git-tracked data writes that need a durable landing with no worktree ritual behind them (the eval
314
+ // sidecar's human-ok, [[human-ok]]). Same rules as the issue store's own write: only the trunk checkout
315
+ // itself commits (a linked-worktree caller gets 'not-primary' back and leaves its append for its own
316
+ // session's ritual commit — the sidecar, unlike the issue store, legitimately lives per-branch and
317
+ // merges); the commit is `--no-verify` and provably scoped to the one data path; the shared store lock
318
+ // serializes it against every other trunk data write so racing writers never fight over the git index.
319
+ export function commitTrunkData(relPath, message) {
320
+ if (overrideStoreDir())
321
+ return 'not-primary'; // a disposable-store test rig commits nothing, ever
322
+ if (!isPrimaryCheckout())
323
+ return 'not-primary';
324
+ return withStoreLock(() => {
325
+ const root = mainCheckout();
326
+ git(['-C', root, 'add', '--', relPath]);
327
+ if (!git(['-C', root, 'status', '--porcelain', '--', relPath]).trim())
328
+ return 'no-op';
329
+ git(['-C', root, 'commit', '--no-verify', '-m', message, '--', relPath]);
330
+ return 'committed';
331
+ });
332
+ }
333
+ // prepare (a FRESH read-modify or a new thread) + write + commit a single store file, all under the store
334
+ // lock so the read-modify-write is atomic. prepare() runs INSIDE the lock, so a reply/close reads the
335
+ // current thread, never a stale copy. A pre-rename store migrates first (before the lock — ensure takes it).
336
+ // `changed: false` = the store already held exactly this state (see writeStoreFile) — success, nothing committed.
337
+ function commitStore(message, prepare) {
338
+ ensureStoreMigrated();
339
+ return withStoreLock(() => {
340
+ const p = prepare();
341
+ const changed = writeStoreFile(p, message);
342
+ return { issue: p, changed };
343
+ });
344
+ }
345
+ // `author` defaults to the effective session id, but a caller (the dashboard's human write path) may pass
346
+ // `'human'` — the write mechanism is identical either way, only the signature differs.
347
+ // The thread's `nodes:` are INFERRED from the text's `[[node]]` topic links ([[mentions]] — the one in-text
348
+ // reference primitive every input already carries), unioned with any explicitly-passed ids (`--node`): a
349
+ // writer links nodes by writing them, never by re-typing ids into a separate field.
350
+ export function openIssue(concern, opts = {}) {
351
+ const nodes = [...new Set([...(opts.nodes || []), ...parseMentions(`${concern}\n${opts.body || ''}`).nodes])];
352
+ return commitStore(`issue: ${concern}`, () => ({
353
+ id: uniqueId(concern), // minted INSIDE the lock, so two racing posts can't pick the same id
354
+ store: 'local',
355
+ concern,
356
+ by: opts.author || currentSession(),
357
+ status: 'open',
358
+ nodes,
359
+ created: new Date().toISOString(),
360
+ body: (opts.body || `(no detail given — ${concern})`).trim(),
361
+ replies: [],
362
+ evidence: opts.evidence || [],
363
+ labels: [],
364
+ })).issue;
365
+ }
366
+ // mint a per-thread-unique remark id (retry on the rare collision within one thread). Short + readable so a
367
+ // `<thread-id>#<rid>` ref is typeable; stable, so a resolve/retract never lands on the wrong remark.
368
+ function mintRid(existing) {
369
+ let rid;
370
+ do {
371
+ rid = 'r' + Math.random().toString(36).slice(2, 6);
372
+ } while (existing.has(rid));
373
+ return rid;
374
+ }
375
+ // a REMARK ([[remark-substrate]]) rides the SAME committed reply write, just stamping the remark fields: a
376
+ // fresh unresolved bit + a minted stable rid + the codeSha it was authored against. Absent `remark` this is
377
+ // an ordinary reply, unchanged. Returns the thread; the new reply is its last, so a caller reads back its rid.
378
+ export function reply(id, body, author, evidence, remark) {
379
+ const by = author || currentSession();
380
+ return commitStore(remark ? `remark(${id}): by ${by}` : `issue(${id}): reply by ${by}`, () => {
381
+ const p = loadOne(id); // fresh read under the lock → no lost-update when replies race
382
+ const post = { by, at: new Date().toISOString(), body: body.trim() };
383
+ if (remark) {
384
+ post.rid = mintRid(new Set(p.replies.map((r) => r.rid).filter((x) => !!x)));
385
+ post.targetCodeSha = remark.targetCodeSha;
386
+ post.resolved = false;
387
+ }
388
+ p.replies.push(post);
389
+ // an anchored annotation carries its frame blob: the reply's evidence hashes accrue onto the THREAD's
390
+ // typed evidence[] (deduped), so the thread stays the one place a video finding's blobs are indexed.
391
+ if (evidence?.length)
392
+ p.evidence = [...new Set([...p.evidence, ...evidence])];
393
+ return p;
394
+ }).issue;
395
+ }
396
+ // @@@ the PROGRAMMATIC store write surface — the dashboard's human write path calls these (author `'human'`).
397
+ // The store is git-native data, so a human's write goes through the SAME open/reply the CLI uses (committed
398
+ // straight to the trunk). @session remains text in that committed discussion; only @new dispatches after it.
399
+ // Originator courtesy is composed above this store because it needs eval-aware candidate resolution.
400
+ export async function replyLocalIssue(id, body, author, evidence, remark) {
401
+ const thread = reply(id, body, author, evidence, remark);
402
+ return {
403
+ thread,
404
+ outcomes: await dispatchNewMentions(body, { threadId: id, node: thread.nodes[0] || null, author, status: thread.status }),
405
+ };
406
+ }
407
+ export async function postLocalIssue(concern, opts) {
408
+ const thread = openIssue(concern, { nodes: opts.nodes, body: opts.body, evidence: opts.evidence, author: opts.author });
409
+ return {
410
+ thread,
411
+ outcomes: await dispatchNewMentions(opts.body || concern, {
412
+ threadId: thread.id,
413
+ node: thread.nodes[0] || null,
414
+ author: opts.author,
415
+ status: thread.status,
416
+ }),
417
+ };
418
+ }
419
+ export function closeLocalIssue(id) {
420
+ const { changed } = commitStore(`issue(${id}): close`, () => {
421
+ const p = loadOne(id);
422
+ p.status = 'landed';
423
+ return p;
424
+ });
425
+ return { status: 'landed', already: !changed };
426
+ }
427
+ // ── remarks ([[remark-substrate]]) ──────────────────────────────────────────────────────────────────
428
+ // A remark is a reply carrying a resolvable bit, attached to a HOST: a local issue, or a scenario keyed by
429
+ // (node, scenario). The scenario track is NOT a new store — it is the annotator's lazy eval thread, keyed by
430
+ // its `eval: <node> · <scenario>` concern; a remark reuses it, creating it on first remark as a stub
431
+ // container (every remark is a reply, never the thread body, so the resolved bit always lives in one place).
432
+ // @@@ the eval-concern format, once - composer and parser side by side. The parser existed in two copies while
433
+ // a CLI surface down here had to recognise the format without being able to import the module that composes it;
434
+ // [[issues-cli]] removed that constraint, so the format is one pair now. `node` is matched non-greedily because
435
+ // it can never contain ' · ' while a scenario name may.
436
+ const evalConcernKey = (node, scenario) => `eval: ${node} · ${scenario}`;
437
+ const EVAL_CONCERN_RE = /^eval: (.+?) · (.+)$/;
438
+ export const parseEvalConcern = (concern) => {
439
+ const m = EVAL_CONCERN_RE.exec(concern);
440
+ return m ? { node: m[1].trim(), scenario: m[2].trim() } : null;
441
+ };
442
+ // find-or-create the ONE scenario thread for (node, scenario), keyed by its eval concern, ATOMICALLY under
443
+ // the store lock. R4 says a scenario's remark track lives ONCE — but a concurrent first-remark burst is a
444
+ // normal dogfood situation (SpexCode runs parallel workers), and if the not-found read sat OUTSIDE the lock
445
+ // two racers could both read "absent" and both create, minting a second thread whose remarks are invisible
446
+ // to the concern key (a silent teeth blind spot). Holding one lock across both the find AND the create closes
447
+ // that window: a racer either sees the thread the first created, or is the first. The stub is a pure
448
+ // container (its body carries a [[wiki-link]], never an @-reference), so it needs no async notification — a
449
+ // synchronous create suffices, and staying sync is exactly what lets it share the lock hold.
450
+ function findOrCreateEvalThread(node, scenario, author) {
451
+ ensureStoreMigrated(); // migrate before the lock (ensure takes it itself; never nest a store-lock hold)
452
+ const concern = evalConcernKey(node, scenario);
453
+ return withStoreLock(() => {
454
+ const existing = loadLocalIssues().find((t) => t.store === 'local' && t.concern === concern);
455
+ if (existing)
456
+ return existing;
457
+ const p = {
458
+ id: uniqueId(concern), store: 'local', concern, by: author, status: 'open',
459
+ nodes: [node], created: new Date().toISOString(),
460
+ body: `Remarks on the \`${scenario}\` eval of [[${node}]].`, replies: [], evidence: [], labels: [],
461
+ };
462
+ writeStoreFile(p, `issue: ${concern}`);
463
+ return p;
464
+ });
465
+ }
466
+ function resolveRemarkHost(host, author) {
467
+ if (host.scenario) {
468
+ const node = host.node;
469
+ if (!node)
470
+ throw new Error('a scenario remark needs a node plus --scenario <name>');
471
+ return findOrCreateEvalThread(node, host.scenario, author).id;
472
+ }
473
+ if (!host.issue)
474
+ throw new Error('a remark needs a host: an issue id, or a node with --scenario <name>');
475
+ return loadOne(host.issue).id; // throws loudly if the issue doesn't exist
476
+ }
477
+ // author a remark on a host — the ONE write both the CLI (`spex remark add`) and the server call. Stamps the
478
+ // codeSha it was authored against (the worktree HEAD by default — R2). Returns the `<thread-id>#<rid>` ref.
479
+ export async function remarkOnHost(host, body, opts = {}) {
480
+ const author = opts.author || currentSession();
481
+ const codeSha = opts.codeSha || headSha(repoRoot());
482
+ const id = resolveRemarkHost(host, author);
483
+ const { thread, outcomes } = await replyLocalIssue(id, body, author, opts.evidence, { targetCodeSha: codeSha });
484
+ const rid = thread.replies[thread.replies.length - 1].rid;
485
+ return { ref: `${id}#${rid}`, rid, codeSha, thread, author, outcomes };
486
+ }
487
+ // a remark ref is `<thread-id>#<rid>`; the thread id (a store slug) never contains '#', so split on the last.
488
+ function parseRemarkRef(ref) {
489
+ const i = ref.lastIndexOf('#');
490
+ if (i <= 0 || i === ref.length - 1)
491
+ throw new Error(`bad remark ref '${ref}' — expected <thread-id>#<rid> (the id \`spex remark add\` printed)`);
492
+ return { id: ref.slice(0, i), rid: ref.slice(i + 1) };
493
+ }
494
+ // resolve a remark (R3): a DELIBERATE call by a SECOND PARTY — never the author (no self-resolve: an
495
+ // identity comparison, so the dashboard's `human` cannot resolve a human-authored remark either), and
496
+ // MONOTONIC (no un-resolve — a regression is a NEW remark). The resolver's identity is derived by the
497
+ // SURFACE ([[remark-substrate]] LAW L): a governed session id from the CLI, the `human` sentinel from the
498
+ // dashboard — both are real second parties, so the same rule runs on both. `by` is the resolving party.
499
+ export function resolveRemark(ref, by) {
500
+ const { id, rid } = parseRemarkRef(ref);
501
+ const { issue: thread } = commitStore(`remark(${id}#${rid}): resolved by ${by}`, () => {
502
+ const p = loadOne(id);
503
+ const r = p.replies.find((x) => x.rid === rid);
504
+ if (!r)
505
+ throw new Error(`no remark '${ref}' in that thread`);
506
+ if (!by || by === 'unknown')
507
+ throw new Error(`resolve needs a real identity (got '${by || 'none'}'): run it under a governed session, or from the dashboard (its actor is 'human')`);
508
+ if (r.resolved)
509
+ throw new Error(`remark '${ref}' is already resolved — monotonic: a regression is a NEW remark, never an un-resolve`);
510
+ if (r.by === by)
511
+ throw new Error(`refusing to self-resolve '${ref}': the author (${by}) may not resolve their own remark — resolve is a second party's deliberate judgment`);
512
+ r.resolved = true;
513
+ r.resolvedBy = by;
514
+ r.resolvedAt = new Date().toISOString();
515
+ return p;
516
+ });
517
+ return { thread, rid };
518
+ }
519
+ // retract a remark (R3): the AUTHOR withdraws their OWN remark, removing it — but ONLY while it is
520
+ // unresolved. Only the author may retract; and once a SECOND party has deliberately resolved it, the remark
521
+ // (and that recorded judgment) is part of the record — retract may not erase it. This makes R3's
522
+ // monotonicity two-sided: resolve can't be undone, and retract can't back-door an un-resolve by deleting the
523
+ // resolved remark. A regression after a resolve is a NEW remark, never a retract-and-reraise.
524
+ export function retractRemark(ref, by) {
525
+ const { id, rid } = parseRemarkRef(ref);
526
+ const { issue: thread } = commitStore(`remark(${id}#${rid}): retracted by ${by}`, () => {
527
+ const p = loadOne(id);
528
+ const idx = p.replies.findIndex((x) => x.rid === rid);
529
+ if (idx < 0)
530
+ throw new Error(`no remark '${ref}' in that thread`);
531
+ if (p.replies[idx].by !== by)
532
+ throw new Error(`only the author (${p.replies[idx].by}) may retract '${ref}' — you are '${by}'`);
533
+ if (p.replies[idx].resolved)
534
+ throw new Error(`refusing to retract '${ref}': it was resolved by ${p.replies[idx].resolvedBy} — a resolved remark is part of the record (monotonic), retract only withdraws an UNRESOLVED remark; a regression is a NEW remark`);
535
+ p.replies.splice(idx, 1);
536
+ return p;
537
+ });
538
+ return { thread, rid };
539
+ }
540
+ // the post-merge nudge TEXT ([[local-issues]]) — produced HERE so the toggle and the wording live in one
541
+ // place; the post-merge git hook is a thin caller that just echoes this. Returns '' when the feature is OFF,
542
+ // so the hook prints nothing.
543
+ export function nudge(node) {
544
+ if (!issuesEnabled())
545
+ return '';
546
+ return [
547
+ '── issues ─────────────────────────────────────────────────────────',
548
+ `Your work (${node || 'this node'}) just landed. Two issue checks before you close:`,
549
+ '',
550
+ '1. CLOSE what you finished. An issue whose work just landed is closed, not',
551
+ ' left open — the open set is the OUTSTANDING work, so a stale open reads as a',
552
+ ' lie: spex issue ls --store local then spex issue close <id>',
553
+ '',
554
+ '2. RECORD only what OUTLIVES this task — a concern you are NOT acting on now:',
555
+ ' an off-mainline smell / awkward boundary / wish, or a trivial-but-must-not-',
556
+ " forget to-do that doesn't earn a spec node. NOT a bug tracker (that is the",
557
+ ' spec graph + the forge), NOT your assigned task or a fix you are about to',
558
+ ' make — those need no issue. Only the taste that would otherwise evaporate:',
559
+ ' spex issue ls # read first — reply if already raised',
560
+ ' spex issue open "<concern>" [--node <id>] # else open one',
561
+ 'A supervisor drains the store later. (Advisory — skip if nothing is owed.)',
562
+ '───────────────────────────────────────────────────────────────────',
563
+ ].join('\n');
564
+ }
565
+ // @@@ close-time issue closeout ([[local-issues]] / [[state]]) — the DATA half of the propose-close nudge:
566
+ // appended to the `done --propose close` declaration BESIDE the resource-cleanup reminder (cli.ts, same
567
+ // insertion point, same semantics — a nudge, never a gate). Data-driven so it earns its line: it lists the
568
+ // still-open local threads THIS session touched (authored or replied; eval `eval: <node> · <scenario>`
569
+ // containers excluded — they host remarks and outlive every session by design) and prints NOTHING when the
570
+ // session owes nothing, when the feature is OFF, or when there is no session identity. Some issues rightly
571
+ // outlive their session (a taste concern waiting for the drain), so the ask is close OR say why it stays
572
+ // open — never a forced close.
573
+ export function closeoutNudge(sessionId) {
574
+ if (!sessionId || sessionId === 'unknown' || !issuesEnabled())
575
+ return '';
576
+ const mine = loadLocalIssues().filter((t) => t.status === 'open' && !parseEvalConcern(t.concern) &&
577
+ (t.by === sessionId || t.replies.some((r) => r.by === sessionId)));
578
+ if (!mine.length)
579
+ return '';
580
+ return `\n\nIssue closeout — ${mine.length} still-open local issue(s) you touched (opened or replied): ${mine.map((t) => t.id).join(', ')}. For each, close it now if its work is finished (\`spex issue close <id>\`), or reply why it should stay open past this session (\`spex issue reply <id> --body "<why>"\`). Some issues rightly outlive their session — this is a reminder to sweep, not a gate.`;
581
+ }
582
+ // ───────────────────────── CLI ─────────────────────────
@@ -0,0 +1,5 @@
1
+ export declare function loginPage(error?: boolean, opts?: {
2
+ action?: string;
3
+ heading?: string;
4
+ sub?: string;
5
+ }): string;
@@ -5,12 +5,12 @@
5
5
  // One page serves every gate: the single-project gateway's /login (the defaults) and the multi-project
6
6
  // hub's admin + per-project logins ([[gateway-hub]] passes action/heading/sub). Every option is
7
7
  // HTML-escaped at this sink — projectIds are path-derived, attacker-influencable strings.
8
- export function loginPage(error = false, opts: { action?: string; heading?: string; sub?: string } = {}): string {
9
- const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!))
10
- const action = esc(opts.action ?? '/login')
11
- const heading = esc(opts.heading ?? 'Restricted access')
12
- const sub = esc(opts.sub ?? 'This is a private agent workspace. Enter the access password to continue.')
13
- return `<!doctype html>
8
+ export function loginPage(error = false, opts = {}) {
9
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
10
+ const action = esc(opts.action ?? '/login');
11
+ const heading = esc(opts.heading ?? 'Restricted access');
12
+ const sub = esc(opts.sub ?? 'This is a private agent workspace. Enter the access password to continue.');
13
+ return `<!doctype html>
14
14
  <html lang="en">
15
15
  <head>
16
16
  <meta charset="utf-8">
@@ -82,5 +82,5 @@ export function loginPage(error = false, opts: { action?: string; heading?: stri
82
82
  <div class="foot">Trusted collaborators only.</div>
83
83
  </form>
84
84
  </body>
85
- </html>`
85
+ </html>`;
86
86
  }
@@ -0,0 +1,23 @@
1
+ import { type LoopIn } from './mentions.js';
2
+ import { replyIssue } from './issues.js';
3
+ import { remarkOnHost } from './localIssues.js';
4
+ /** `issue reply` for every store, with the originator loop-in composed on top. */
5
+ export declare function replyIssueWithLoopIn(id: string, body: string, opts?: {
6
+ author?: string;
7
+ node?: string | null;
8
+ evidence?: string[];
9
+ }): Promise<Awaited<ReturnType<typeof replyIssue>> & {
10
+ loopIn: LoopIn | null;
11
+ }>;
12
+ /** `remark add` on a host, with the same loop-in composed by the same code. */
13
+ export declare function remarkWithLoopIn(host: {
14
+ issue?: string;
15
+ node?: string;
16
+ scenario?: string;
17
+ }, body: string, opts?: {
18
+ codeSha?: string;
19
+ author?: string;
20
+ evidence?: string[];
21
+ }): Promise<Awaited<ReturnType<typeof remarkOnHost>> & {
22
+ loopIn: LoopIn | null;
23
+ }>;