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