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
@@ -0,0 +1,1076 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { createInterface } from 'node:readline/promises';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { HARNESSES, MISSING_DEFAULT_LAUNCHER_ERROR, defaultLauncher, harnessById, launcherList, resolveLauncher } from './harness.js';
8
+ import { ensureDashboardArtifact } from './dashboard-assets.js';
9
+ const PKG = fileURLToPath(new URL('..', import.meta.url));
10
+ const SPEX = join(PKG, 'bin', 'spex.mjs');
11
+ // The branch Flatcode commits the spec tree onto inside its OWN clone. Never pushed: the flat is a reading of
12
+ // the repository, not a change to it, and a user who wants it upstream opens that as their own change.
13
+ export const FLAT_BRANCH = 'flatcode';
14
+ const DEFAULT_ROUNDS = 6;
15
+ const DEFAULT_COVERAGE = 90;
16
+ // A Flatcode run begins outside the target repository, so an installed CLI cannot rely on that directory
17
+ // already having SpexCode launcher profiles. These are the regular local agent commands an operator can select
18
+ // at the boundary; an existing named launcher still wins and preserves its configured command.
19
+ export const FLAT_AGENT_CHOICES = [
20
+ { name: 'claude', label: 'Claude Code', harness: 'claude', cmd: 'claude' },
21
+ { name: 'codex', label: 'Codex', harness: 'codex', cmd: 'codex' },
22
+ { name: 'opencode', label: 'OpenCode', harness: 'opencode', cmd: 'opencode' },
23
+ { name: 'pi', label: 'Pi', harness: 'pi', cmd: 'pi' },
24
+ ];
25
+ // @@@ prose language - a spec is written FOR the people who maintain the repository, and for most of the
26
+ // world that is not English. The language governs the prose only: node ids are directory names, which
27
+ // [[id-url-safe]] holds to a URL-safe ASCII form, so a tree written in Chinese still has ascii-kebab ids.
28
+ // Unlisted codes pass through verbatim, so `--lang "Brazilian Portuguese"` works without a table entry.
29
+ const LANGUAGE_NAMES = {
30
+ zh: '简体中文 (Simplified Chinese)', 'zh-cn': '简体中文 (Simplified Chinese)',
31
+ 'zh-tw': '繁體中文 (Traditional Chinese)', en: 'English', ja: '日本語 (Japanese)',
32
+ ko: '한국어 (Korean)', fr: 'French', de: 'German', es: 'Spanish', pt: 'Portuguese', ru: 'Russian',
33
+ };
34
+ export const languageName = (code) => LANGUAGE_NAMES[code.toLowerCase()] ?? code;
35
+ // @@@ source extensions are an allowlist, never a guess - an unknown extension is ignored rather than
36
+ // governed. Governing a file nobody can spec (a lockfile, a minified vendor bundle, a fixture) manufactures
37
+ // permanent coverage debt that no round can ever pay off, so the gate would never close. Under-claiming costs
38
+ // a user one edit to the emitted config; over-claiming costs an unbounded loop.
39
+ const SOURCE_EXTENSIONS = {
40
+ ts: 'TypeScript', tsx: 'TypeScript', js: 'JavaScript', jsx: 'JavaScript', mjs: 'JavaScript',
41
+ py: 'Python', rb: 'Ruby', go: 'Go', rs: 'Rust', java: 'Java', kt: 'Kotlin', swift: 'Swift',
42
+ c: 'C', h: 'C', cc: 'C++', cpp: 'C++', hpp: 'C++', cs: 'C#', php: 'PHP', scala: 'Scala',
43
+ ex: 'Elixir', exs: 'Elixir', erl: 'Erlang', hs: 'Haskell', ml: 'OCaml', clj: 'Clojure',
44
+ lua: 'Lua', dart: 'Dart', vue: 'Vue', svelte: 'Svelte', sh: 'Shell', zig: 'Zig',
45
+ };
46
+ // Tracked directories that are somebody else's code or a build product. They are real source by extension,
47
+ // which is exactly why they must be named: governing them buys nothing and drowns the real tree.
48
+ const NON_SOURCE_DIRS = new Set([
49
+ 'node_modules', 'vendor', 'third_party', 'thirdparty', 'dist', 'build', 'out', 'target',
50
+ 'generated', 'gen', '.venv', 'venv', 'site-packages', 'testdata', 'fixtures', '__pycache__',
51
+ // SpexCode's own tree is never the subject of a flat. It is absent on the first pass (profiling runs before
52
+ // the seed) but present on any re-profile, where counting it would shift the share threshold under roots
53
+ // that qualified the first time.
54
+ '.spec',
55
+ ]);
56
+ function run(command, args, cwd, stdin = '') {
57
+ return new Promise((done) => {
58
+ const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
59
+ let stdout = '';
60
+ let stderr = '';
61
+ child.stdout.setEncoding('utf8');
62
+ child.stderr.setEncoding('utf8');
63
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
64
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
65
+ child.on('error', (error) => done({ code: 127, stdout, stderr: `${stderr}${error.message}` }));
66
+ child.on('close', (code) => done({ code: code ?? 1, stdout, stderr }));
67
+ child.stdin.on('error', () => { });
68
+ child.stdin.end(stdin);
69
+ });
70
+ }
71
+ // One agent turn, streamed to the operator's terminal as it happens. A conversion round takes minutes; a
72
+ // silent pipe would read as a hang, and the turn's own narration is the only progress there is to show.
73
+ function runTurn(turn, cwd) {
74
+ return new Promise((done) => {
75
+ const child = spawn('sh', ['-c', turn.command], { cwd, stdio: ['pipe', 'inherit', 'inherit'] });
76
+ child.on('error', () => done(127));
77
+ child.on('close', (code) => done(code ?? 1));
78
+ // @@@ the exit code is the verdict, not the pipe - a harness that takes the prompt and closes stdin, or
79
+ // that exits before reading it at all (a rejected credential, a bad flag), makes this write EPIPE. That is
80
+ // information about the pipe, not about the round: an unhandled EPIPE here would abort the whole flat and
81
+ // throw away every converged round before it, where the exit code turns the same event into one reported
82
+ // failed round the loop can measure and continue past.
83
+ child.stdin.on('error', () => { });
84
+ child.stdin.end(turn.stdin);
85
+ });
86
+ }
87
+ const spex = (args, cwd, env = {}) => new Promise((done) => {
88
+ const child = spawn(process.execPath, [SPEX, ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env } });
89
+ let stdout = '';
90
+ let stderr = '';
91
+ child.stdout.setEncoding('utf8');
92
+ child.stderr.setEncoding('utf8');
93
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
94
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
95
+ child.on('error', (error) => done({ code: 127, stdout, stderr: error.message }));
96
+ child.on('close', (code) => done({ code: code ?? 1, stdout, stderr }));
97
+ });
98
+ // @@@ profile - derive what counts as this repository's source from what it actually tracks. `git ls-files`
99
+ // is the enumeration because it already answers "tracked, not ignored" exactly; walking the tree would have to
100
+ // re-implement .gitignore and would sweep in build output the repository deliberately ignores.
101
+ export function profileFiles(files) {
102
+ const byExtension = new Map();
103
+ const byRoot = new Map();
104
+ let fileCount = 0;
105
+ for (const file of files) {
106
+ const segments = file.split('/');
107
+ if (segments.some((segment) => NON_SOURCE_DIRS.has(segment)))
108
+ continue;
109
+ const leaf = segments[segments.length - 1];
110
+ const dot = leaf.lastIndexOf('.');
111
+ if (dot <= 0)
112
+ continue; // no extension at all, or a dotfile (.gitignore)
113
+ const extension = leaf.slice(dot + 1).toLowerCase();
114
+ if (!(extension in SOURCE_EXTENSIONS))
115
+ continue;
116
+ fileCount += 1;
117
+ byExtension.set(extension, (byExtension.get(extension) ?? 0) + 1);
118
+ // A file at the repository top level makes the repository root the governed root; there is no dir to name.
119
+ const root = segments.length > 1 ? segments[0] : '.';
120
+ byRoot.set(root, (byRoot.get(root) ?? 0) + 1);
121
+ }
122
+ const sourceExtensions = [...byExtension.keys()].sort();
123
+ const languages = [...new Set(sourceExtensions.map((extension) => SOURCE_EXTENSIONS[extension]))].sort();
124
+ // A root earns governance by holding a real SHARE of the source, and the repository root earns it the same
125
+ // way as any other. Without that, one top-level packaging script (`setup.py`) qualifies the root, the root
126
+ // subsumes every sibling, and a library flat silently takes on its whole test and docs tree. The share is
127
+ // deliberately coarse — the emitted config is the user's to correct, and under-claiming costs one edit
128
+ // while over-claiming costs rounds spent specifying files nobody wanted specified.
129
+ const floor = Math.max(2, Math.ceil(fileCount * 0.05));
130
+ const qualified = [...byRoot.entries()].filter(([, count]) => count >= floor).map(([root]) => root).sort();
131
+ const governedRoots = qualified.includes('.') ? ['.'] : qualified;
132
+ // Report the count of what will actually BE governed, not of everything recognised: a file under a root that
133
+ // did not qualify is never gated, and counting it here would put the report permanently at odds with the
134
+ // gate's own denominator.
135
+ const governedCount = governedRoots.includes('.')
136
+ ? fileCount
137
+ : governedRoots.reduce((total, root) => total + (byRoot.get(root) ?? 0), 0);
138
+ return { sourceExtensions, governedRoots, languages, fileCount: governedCount };
139
+ }
140
+ // The gate reading. `spex spec lint --json` exits non-zero when it found errors — that IS the reading, not a
141
+ // crash, so only an unparseable payload is a failure.
142
+ export function readGate(payload) {
143
+ const report = JSON.parse(payload);
144
+ const findings = report.findings ?? [];
145
+ const sourceFiles = report.sourceFiles ?? [];
146
+ const governed = sourceFiles.length;
147
+ const uncoveredFiles = findings.filter((f) => f.rule === 'coverage').map((f) => f.file ?? f.msg);
148
+ const errorFindings = findings.filter((f) => f.level === 'error').map((f) => `${f.rule}: ${f.msg}`);
149
+ return {
150
+ errors: errorFindings.length,
151
+ governed,
152
+ uncovered: uncoveredFiles.length,
153
+ coverage: governed ? Math.round(((governed - uncoveredFiles.length) / governed) * 100) : 0,
154
+ errorFindings,
155
+ uncoveredFiles,
156
+ sourceFiles,
157
+ };
158
+ }
159
+ export const gatePassed = (gate, coverageFloor) => gate.errors === 0 && gate.governed > 0 && gate.coverage >= coverageFloor;
160
+ // @@@ confirmProfile - narrow the PROPOSED governed set to the one lint actually keeps. The proposal is read
161
+ // off the file tree, but lint applies the product's own source policy on top (its testGlobs drop `tests/`,
162
+ // `test_*`, `*.test.*` wholesale), so a root can be proposed, written into the config, and then contribute
163
+ // nothing. Left uncorrected that root is a lie in two places at once: the emitted config lists a root the gate
164
+ // ignores, and the report prints a file count the gate's own denominator contradicts — measured on psf/requests
165
+ // as "37 source files across docs, src, tests" beside a gate reading of 21. The profile must be CONFIRMED
166
+ // against lint's accounting for the same reason convergence is: this file does not get to assert what is
167
+ // governed. Reimplementing the exclusion policy here instead would put a second copy of it in the tree, and the
168
+ // copies would drift.
169
+ export function confirmProfile(proposed, sourceFiles) {
170
+ const kept = new Set(sourceFiles.map((file) => (file.includes('/') ? file.split('/')[0] : '.')));
171
+ const governedRoots = proposed.governedRoots.filter((root) => root === '.' || kept.has(root));
172
+ return { ...proposed, governedRoots, fileCount: sourceFiles.length };
173
+ }
174
+ // The prose language is stated as its own paragraph rather than folded into the task, because it applies to
175
+ // every node and the one thing it must NOT touch is the ids.
176
+ const languageClause = (lang) => lang && lang.toLowerCase() !== 'en' ? [
177
+ ``,
178
+ `WRITE THE SPEC PROSE IN ${languageName(lang)}. That means every node's \`title\`, \`desc\` and body — the`,
179
+ `whole tree reads in that language, not a translation layer over English.`,
180
+ `Node IDS STAY lowercase ascii-kebab (they are directory names and part of URLs), and so do the file paths`,
181
+ `in \`code:\`/\`related:\` and the \`[[id]]\` mentions that point at them.`,
182
+ ] : [];
183
+ function surveyPrompt(profile, coverageFloor, lang) {
184
+ return [
185
+ `You are flattening this repository into a SpexCode .spec tree. This is the whole task; there is no other work.`,
186
+ ``,
187
+ `FIRST, read the authoring format — it is the authority, not this message:`,
188
+ ` spex guide spec`,
189
+ ``,
190
+ `The repository is ${profile.languages.join(', ')} (${profile.fileCount} governed source files under ${profile.governedRoots.join(', ')}).`,
191
+ ``,
192
+ `Write .spec nodes until every governed source file is claimed by exactly one node's \`code:\` list.`,
193
+ `Shape the tree the way the SYSTEM is shaped — one node per real responsibility, nesting where a`,
194
+ `responsibility genuinely contains sub-responsibilities. Do not mirror the directory layout for its own sake.`,
195
+ ``,
196
+ `A node body states INTENT, INVARIANTS, and CONTRACTS: what this part guarantees, what it refuses, what`,
197
+ `would be a bug. It is not a paraphrase of the code — a reader who has the source already gets nothing`,
198
+ `from prose that restates it. Write what the source CANNOT tell them: why this shape, what it must never do.`,
199
+ ...languageClause(lang),
200
+ ``,
201
+ `You will be measured, and you will be told what failed:`,
202
+ ` - \`spex spec lint\` must report ZERO errors.`,
203
+ ` - Coverage must reach ${coverageFloor}% of governed source files.`,
204
+ ` - \`spex doctor\` must not find nodes that dumped mechanics instead of stating intent.`,
205
+ ``,
206
+ `Run those yourself as you go. Commit nothing; the driver commits.`,
207
+ ].join('\n');
208
+ }
209
+ function repairPrompt(gate, doctor, coverageFloor, lang) {
210
+ const lines = [
211
+ `Continue flattening this repository. The gate was measured and it did NOT pass. Fix exactly what follows.`,
212
+ ...languageClause(lang),
213
+ ``,
214
+ `Coverage: ${gate.coverage}% of ${gate.governed} governed files (floor ${coverageFloor}%). Lint errors: ${gate.errors}.`,
215
+ ];
216
+ if (gate.errorFindings.length) {
217
+ lines.push(``, `LINT ERRORS — these are structural and block everything:`);
218
+ for (const finding of gate.errorFindings.slice(0, 40))
219
+ lines.push(` - ${finding}`);
220
+ }
221
+ if (gate.uncoveredFiles.length) {
222
+ lines.push(``, `UNCLAIMED SOURCE FILES — each needs a node whose \`code:\` claims it:`);
223
+ for (const file of gate.uncoveredFiles.slice(0, 60))
224
+ lines.push(` - ${file}`);
225
+ if (gate.uncoveredFiles.length > 60)
226
+ lines.push(` … and ${gate.uncoveredFiles.length - 60} more; run \`spex spec lint\` for the rest.`);
227
+ }
228
+ if (doctor.trim()) {
229
+ lines.push(``, `SPEC HEALTH — nodes phrased as mechanics dumps rather than intent, and parents fanned too wide:`, ``, doctor.trim(), ``,
230
+ // `spex init` seeds .plugins with SpexCode's own workflow nodes, whose bodies ARE the prompt text that
231
+ // gets materialized for the agent. Doctor measures them like any other node, so its findings name them —
232
+ // and a round spent "fixing" them would trim live product behaviour and specify nothing about this
233
+ // repository. The boundary belongs in the instruction, not in a fragile filter over doctor's prose.
234
+ `IGNORE any finding under \`.spec/*/.plugins/\`. Those are SpexCode's own seeded workflow nodes, not`, `your work, and their bodies are prompt text that must not be trimmed. Fix only nodes you wrote.`);
235
+ }
236
+ lines.push(``, `Commit nothing; the driver commits.`);
237
+ return lines.join('\n');
238
+ }
239
+ function configuredTurnHarness(name, root) {
240
+ const chosen = resolveLauncher(name, root);
241
+ const harness = harnessById(chosen.harness);
242
+ if (!harness.oneShotTurn) {
243
+ const capable = HARNESSES.filter((candidate) => candidate.oneShotTurn).map((candidate) => candidate.id).join(', ');
244
+ throw new Error(`spex flat: launcher '${name}' runs harness '${harness.id}', which has no non-interactive turn, so it ` +
245
+ `cannot run a conversion round. Harnesses that can: ${capable}. Pick a launcher on one of those with --launcher.`);
246
+ }
247
+ return { harness, cmd: chosen.cmd, name, harnessId: harness.id };
248
+ }
249
+ function builtinTurnHarness(name) {
250
+ const choice = FLAT_AGENT_CHOICES.find((candidate) => candidate.name === name);
251
+ if (!choice)
252
+ return null;
253
+ const harness = harnessById(choice.harness);
254
+ if (!harness.oneShotTurn)
255
+ return null;
256
+ return { harness, cmd: choice.cmd, name: choice.name, harnessId: choice.harness };
257
+ }
258
+ // The launcher IS the choice, exactly as it is for a session ([[launcher-select]]) when the caller has a
259
+ // project config. Flatcode also works as a global command from an unrelated directory, where no launcher
260
+ // registry exists yet: a regular agent name then names both its one-shot runner and the harness that init
261
+ // persists in the clone. There is no implicit fallback because a non-interactive run must never guess auth.
262
+ function resolveTurnHarness(launcher, root) {
263
+ if (launcher) {
264
+ const configured = launcherList(root);
265
+ if (configured.some((candidate) => candidate.name === launcher))
266
+ return configuredTurnHarness(launcher, root);
267
+ const builtin = builtinTurnHarness(launcher);
268
+ if (builtin)
269
+ return builtin;
270
+ if (configured.length)
271
+ return configuredTurnHarness(launcher, root);
272
+ throw new Error(`spex flat: unknown launcher '${launcher}'. Choose one of: ${FLAT_AGENT_CHOICES.map((choice) => choice.name).join(', ')}.`);
273
+ }
274
+ return configuredTurnHarness(defaultLauncher(root), root);
275
+ }
276
+ async function chooseTurnHarness(root, stdin = process.stdin, stdout = process.stdout) {
277
+ try {
278
+ return resolveTurnHarness(undefined, root);
279
+ }
280
+ catch (error) {
281
+ if (error.message !== MISSING_DEFAULT_LAUNCHER_ERROR)
282
+ throw error;
283
+ }
284
+ if (!stdin.isTTY || !stdout.isTTY) {
285
+ throw new Error(`spex flat: no configured launcher in ${root}. Pass --launcher <name> (${FLAT_AGENT_CHOICES.map((choice) => choice.name).join(', ')}).`);
286
+ }
287
+ stdout.write('Choose the agent that will convert this repository:\n');
288
+ for (const [index, choice] of FLAT_AGENT_CHOICES.entries())
289
+ stdout.write(` ${index + 1}. ${choice.label}\n`);
290
+ const prompt = createInterface({ input: stdin, output: stdout });
291
+ try {
292
+ const answer = (await prompt.question('Agent [1]: ')).trim() || '1';
293
+ const position = Number(answer);
294
+ const choice = Number.isInteger(position) ? FLAT_AGENT_CHOICES[position - 1] : undefined;
295
+ if (!choice)
296
+ throw new Error(`spex flat: expected a number from 1 to ${FLAT_AGENT_CHOICES.length}`);
297
+ return builtinTurnHarness(choice.name);
298
+ }
299
+ finally {
300
+ prompt.close();
301
+ }
302
+ }
303
+ async function git(args, cwd) {
304
+ return run('git', args, cwd);
305
+ }
306
+ async function gitOrThrow(args, cwd, what) {
307
+ const result = await git(args, cwd);
308
+ if (result.code !== 0)
309
+ throw new Error(`spex flat: ${what} failed — ${result.stderr.trim() || result.stdout.trim()}`);
310
+ return result.stdout.trim();
311
+ }
312
+ const isUrl = (target) => /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/.test(target);
313
+ export async function flatNew(options, log = console.log, choose = chooseTurnHarness) {
314
+ const rounds = options.rounds ?? DEFAULT_ROUNDS;
315
+ const coverageFloor = options.coverage ?? DEFAULT_COVERAGE;
316
+ if (!Number.isInteger(rounds) || rounds < 1)
317
+ throw new Error('spex flat: --rounds must be a positive integer');
318
+ if (!Number.isFinite(coverageFloor) || coverageFloor < 0 || coverageFloor > 100)
319
+ throw new Error('spex flat: --coverage must be between 0 and 100');
320
+ const name = basename(options.target.replace(/\.git$/, '').replace(/\/+$/, '')) || 'flat';
321
+ // --- acquire -------------------------------------------------------------------------------------------
322
+ // A local repository IS the intended home for its .spec tree. Flatcode only adds that governed intent; it
323
+ // never makes a throwaway copy of a repository the caller already has, and it refuses dirty state so its
324
+ // commits cannot accidentally absorb unrelated work. A remote URL has no such home, so it remains isolated
325
+ // in <out>/repo.
326
+ let source = options.target;
327
+ let local = null;
328
+ if (!isUrl(options.target)) {
329
+ local = resolve(options.target);
330
+ if (!existsSync(join(local, '.git')))
331
+ throw new Error(`spex flat: ${local} is not a git repository`);
332
+ const dirty = await gitOrThrow(['status', '--porcelain'], local, 'checking the working tree');
333
+ if (dirty)
334
+ throw new Error(`spex flat: ${local} has uncommitted changes — Flatcode commits a spec tree and will not mix it with work it did not write`);
335
+ source = local;
336
+ }
337
+ const out = resolve(options.out ?? (local ? join(dirname(local), `${basename(local)}.flat`) : `${name}.flat`));
338
+ const outputInsideSource = local && (() => {
339
+ const fromSource = relative(local, out);
340
+ return !fromSource || (!fromSource.startsWith(`..${sep}`) && fromSource !== '..' && !isAbsolute(fromSource));
341
+ })();
342
+ if (outputInsideSource)
343
+ throw new Error(`spex flat: ${out} is inside ${local}; Flatcode's output must stay beside the source repository`);
344
+ const repo = local ?? join(out, 'repo');
345
+ const configPath = join(repo, 'spexcode.json');
346
+ const initialized = Boolean(local && existsSync(join(repo, '.spec')) && existsSync(configPath));
347
+ if (local && existsSync(out)) {
348
+ const previous = join(out, 'flat.json');
349
+ const recorded = existsSync(previous) ? JSON.parse(readFileSync(previous, 'utf8')) : null;
350
+ if (recorded?.repo !== local)
351
+ throw new Error(`spex flat: ${out} already exists for another purpose — name a different --out`);
352
+ }
353
+ if (!local && existsSync(repo))
354
+ throw new Error(`spex flat: ${repo} already exists — name a different --out or remove it`);
355
+ // An existing SpexCode project names its own local launcher. A new local repository and a remote conversion
356
+ // inherit the launch boundary from the calling directory, whose configuration is the only one available yet.
357
+ const launcherRoot = local && existsSync(configPath) ? local : process.cwd();
358
+ const { harness, cmd, name: launcherName, harnessId } = options.launcher
359
+ ? resolveTurnHarness(options.launcher, launcherRoot)
360
+ : await choose(launcherRoot);
361
+ if (local) {
362
+ log(`using local ${local}`);
363
+ }
364
+ else {
365
+ mkdirSync(out, { recursive: true });
366
+ log(`cloning ${options.target}`);
367
+ const clone = await run('git', ['clone', '--quiet', options.target, repo], out);
368
+ if (clone.code !== 0)
369
+ throw new Error(`spex flat: clone failed — ${clone.stderr.trim()}`);
370
+ await gitOrThrow(['checkout', '--quiet', '-b', FLAT_BRANCH], repo, `creating the ${FLAT_BRANCH} branch`);
371
+ }
372
+ const revision = await gitOrThrow(['rev-parse', 'HEAD'], repo, 'identifying the cloned revision');
373
+ // --- profile -------------------------------------------------------------------------------------------
374
+ const tracked = (await gitOrThrow(['ls-files'], repo, 'listing tracked files')).split('\n').filter(Boolean);
375
+ const proposed = profileFiles(tracked);
376
+ if (!proposed.fileCount) {
377
+ throw new Error(`spex flat: no recognised source files in ${source}. Flatcode governs a fixed allowlist of source ` +
378
+ `extensions; a repository outside it would gate on an empty governed set, which passes vacuously.`);
379
+ }
380
+ // --- seed or resume ------------------------------------------------------------------------------------
381
+ // An initialized local project already has a .spec tree, policy, and launcher choice. It is not a fresh
382
+ // adopter merely because Flatcode has not run there: preserve those choices exactly and let the gate tell
383
+ // the agent what remains. Every other target receives the ordinary adoption seed before its first turn.
384
+ const writeConfig = (chosen) => {
385
+ const existing = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : {};
386
+ writeFileSync(configPath, `${JSON.stringify({
387
+ ...existing,
388
+ // Name the graph after the repository it reads, not after Flatcode's checkout directory — otherwise every
389
+ // flat in the world renders under the title "repo".
390
+ dashboard: { title: name, ...(existing.dashboard ?? {}) },
391
+ lint: { ...(existing.lint ?? {}), governedRoots: chosen.governedRoots, sourceExtensions: chosen.sourceExtensions },
392
+ }, null, 2)}\n`);
393
+ };
394
+ let gate;
395
+ let profile;
396
+ if (initialized) {
397
+ gate = await gateOf(repo);
398
+ profile = profileFiles(gate.sourceFiles);
399
+ log(`resuming existing .spec · launcher ${launcherName} (${harness.id})`);
400
+ }
401
+ else {
402
+ const init = await spex(['init', '--harness', harnessId], repo);
403
+ if (init.code !== 0)
404
+ throw new Error(`spex flat: seeding .spec failed — ${init.stderr.trim() || init.stdout.trim()}`);
405
+ log(`seeded .spec · launcher ${launcherName} (${harness.id})`);
406
+ // Init must run before the profile write. A fresh clone has no config, and init is the one operation that
407
+ // seeds the selected harness plus its matching named launcher. Writing a partial config first made init
408
+ // preserve that file, leaving the clone with a harness target but no launcher profile for the agent it used.
409
+ writeConfig(proposed);
410
+ await commit(repo, 'flatcode: profile and seed .spec', ['.spec', 'spexcode.json']);
411
+ gate = await gateOf(repo);
412
+ profile = confirmProfile(proposed, gate.sourceFiles);
413
+ const dropped = proposed.governedRoots.filter((root) => !profile.governedRoots.includes(root));
414
+ if (dropped.length) {
415
+ writeConfig(profile);
416
+ await commit(repo, `flatcode: narrow governed roots to ${profile.governedRoots.join(', ')}`, ['spexcode.json']);
417
+ gate = await gateOf(repo);
418
+ log(`dropped ${dropped.join(', ')} — lint's source policy governs nothing there`);
419
+ }
420
+ }
421
+ if (!gate.governed) {
422
+ throw new Error(`spex flat: lint governs no source file in ${source}, so the gate would pass vacuously. The proposed ` +
423
+ `roots were ${proposed.governedRoots.join(', ')}; the product's source policy kept none of them.`);
424
+ }
425
+ log(`governing ${profile.fileCount} source files · ${profile.languages.join(', ')} · roots ${profile.governedRoots.join(', ')}`);
426
+ // --- converge ------------------------------------------------------------------------------------------
427
+ let round = 0;
428
+ while (round < rounds && !gatePassed(gate, coverageFloor)) {
429
+ round += 1;
430
+ const doctor = round === 1 ? '' : (await spex(['doctor'], repo)).stdout;
431
+ const prompt = round === 1 ? surveyPrompt(profile, coverageFloor, options.lang) : repairPrompt(gate, doctor, coverageFloor, options.lang);
432
+ log(`round ${round}/${rounds} — coverage ${gate.coverage}% · ${gate.errors} lint error(s)`);
433
+ const before = await gitOrThrow(['rev-parse', 'HEAD'], repo, 'recording the round start');
434
+ const beforeUntracked = new Set((await gitOrThrow(['ls-files', '--others', '--exclude-standard'], repo, 'recording untracked files')).split('\n').filter(Boolean));
435
+ const code = await runTurn(harness.oneShotTurn(prompt, cmd), repo);
436
+ if (code !== 0)
437
+ log(`round ${round}: the agent turn exited ${code}; measuring anyway`);
438
+ const after = await gitOrThrow(['rev-parse', 'HEAD'], repo, 'checking the agent round');
439
+ if (after !== before)
440
+ await gitOrThrow(['reset', '--soft', before], repo, 'taking ownership of the agent commit');
441
+ await assertOnlyFlatChanges(repo, before, beforeUntracked);
442
+ await commit(repo, `flatcode: round ${round}`, ['.spec']);
443
+ gate = await gateOf(repo);
444
+ }
445
+ const passed = gatePassed(gate, coverageFloor);
446
+ mkdirSync(out, { recursive: true });
447
+ const result = { out, repo, source, revision, rounds: round, gate, passed, profile };
448
+ writeFileSync(join(out, 'flat.json'), `${JSON.stringify({
449
+ schema: 'spexcode.flat/v1', repo,
450
+ source, revision, branch: FLAT_BRANCH, launcher: launcherName, harness: harness.id,
451
+ rounds: round, roundBudget: rounds, coverageFloor, passed, lang: options.lang ?? 'en',
452
+ gate: { errors: gate.errors, governed: gate.governed, uncovered: gate.uncovered, coverage: gate.coverage },
453
+ profile,
454
+ }, null, 2)}\n`);
455
+ return result;
456
+ }
457
+ // The graph-only shell belongs to the dashboard package; its static bytes remain independent from a flat's payload.
458
+ export function publicShellDir() {
459
+ return ensureDashboardArtifact('dist-public');
460
+ }
461
+ // The one spec-root directory under .spec. `spex init` names it for the project, so it is `project` in a
462
+ // fresh flat and `spexcode` in this repository — deriving it beats hardcoding either.
463
+ function specRoot(repo) {
464
+ const entries = readdirSync(join(repo, '.spec'), { withFileTypes: true }).filter((entry) => entry.isDirectory());
465
+ if (entries.length !== 1)
466
+ throw new Error(`spex flat site: expected exactly one spec root under .spec, found ${entries.length}`);
467
+ return entries[0].name;
468
+ }
469
+ export async function flatSite(flatDir, log = console.log) {
470
+ const out = resolve(flatDir);
471
+ const recordPath = join(out, 'flat.json');
472
+ if (!existsSync(recordPath))
473
+ throw new Error(`spex flat site: ${out} is not a flat — no flat.json. Run \`spex flat new\` first.`);
474
+ const record = JSON.parse(readFileSync(recordPath, 'utf8'));
475
+ const repo = record.repo && existsSync(record.repo) ? record.repo : join(out, 'repo');
476
+ const site = join(out, 'site');
477
+ const shell = publicShellDir();
478
+ mkdirSync(site, { recursive: true });
479
+ cpSync(join(shell, 'index.html'), join(site, 'index.html'));
480
+ cpSync(join(shell, 'assets'), join(site, 'assets'), { recursive: true });
481
+ const graphPath = join(site, 'public-graph.json');
482
+ const graph = await spex(['graph', '--public', '--out', graphPath, '--content-dir', join(site, 'specs')], repo);
483
+ if (graph.code !== 0)
484
+ throw new Error(`spex flat site: building the public graph failed — ${graph.stderr.trim() || graph.stdout.trim()}`);
485
+ const root = specRoot(repo);
486
+ // @@@ the publication is the repository's spec, not SpexCode's - `spex init` seeds .plugins with SpexCode's
487
+ // own workflow nodes so the converting agent receives its contract. They are machinery, not a reading of the
488
+ // target: measured on the first three published flats they were 38-48% of every graph, and their bodies ARE
489
+ // the command prompt texts, so a visitor who came to read `requests` met SpexCode's `extract` prompt
490
+ // presented as that repository's specification. The conversion needs them; the publication must not carry
491
+ // them. Filtering here rather than at seed time keeps the agent's contract intact.
492
+ const payload = JSON.parse(readFileSync(graphPath, 'utf8'));
493
+ // The graph's `path` is repo-relative and includes `.spec/`, so the prefix has to as well.
494
+ const seeded = `.spec/${root}/.plugins/`;
495
+ const published = payload.nodes.filter((node) => !node.path.startsWith(seeded));
496
+ if (published.length === payload.nodes.length) {
497
+ throw new Error(`spex flat site: nothing matched the seeded prefix ${seeded} — the publication would carry SpexCode's own workflow nodes as if they were this repository's spec`);
498
+ }
499
+ const dropped = payload.nodes.length - published.length;
500
+ if (dropped) {
501
+ for (const node of payload.nodes) {
502
+ if (published.includes(node))
503
+ continue;
504
+ rmSync(join(site, 'specs', `${node.id}.json`), { force: true });
505
+ }
506
+ writeFileSync(graphPath, `${JSON.stringify({ ...payload, nodes: published }, null, 2)}\n`);
507
+ log(` dropped ${dropped} seeded SpexCode workflow node(s) from the publication`);
508
+ }
509
+ const archiveName = `${root}.spec.zip`;
510
+ // The archive is the same publication, so it excludes the same subtree — a downloader who unzips it must
511
+ // get what the graph showed, not the machinery the graph deliberately left out.
512
+ const topLevel = readdirSync(join(repo, '.spec', root), { withFileTypes: true })
513
+ .map((entry) => entry.name)
514
+ .filter((name) => name !== '.plugins')
515
+ .sort();
516
+ const archive = await run('git', [
517
+ 'archive', '--format=zip', '--prefix=.spec/', `--output=${join(site, archiveName)}`,
518
+ `${payload.revision}:.spec/${root}`, ...topLevel,
519
+ ], repo);
520
+ if (archive.code !== 0)
521
+ throw new Error(`spex flat site: archiving the spec tree failed — ${archive.stderr.trim()}`);
522
+ const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
523
+ const asset = (path) => { const bytes = readFileSync(join(site, path)); return { path, bytes: bytes.byteLength, sha256: sha256(bytes) }; };
524
+ const graphAsset = asset('public-graph.json');
525
+ const archiveAsset = { ...asset(archiveName), name: archiveName };
526
+ const documents = readdirSync(join(site, 'specs')).filter((name) => name.endsWith('.json')).sort();
527
+ // The About panel's facts are the flat's own reading. A visitor should be able to see, without leaving the
528
+ // page, that this tree was produced by a measured conversion and how complete that conversion actually was —
529
+ // publishing the graph while hiding its coverage would present a partial flat as a finished one.
530
+ const metadata = {
531
+ schema: 'spexcode.public-spec-site/v1',
532
+ // A repository link is offered only when the source is one a browser can follow. A local path rendered as
533
+ // a link would promise a destination that does not exist for anyone but the person who ran the flat.
534
+ publication: { id: root, ...(isUrl(record.source) ? { repository: { url: record.source } } : {}) },
535
+ about: {
536
+ title: `About this flat`,
537
+ summary: 'A static, read-only view of a specification graph produced by Flatcode from the repository named below. It exposes committed spec intent and relationships only; sessions, issues, evaluations, settings, and write routes are absent.',
538
+ facts: [
539
+ { label: 'Source', value: record.source },
540
+ // The panel renders the release revision itself; a second one here would read as two different commits.
541
+ { label: 'Source revision', value: record.revision.slice(0, 12) },
542
+ { label: 'Coverage', value: `${record.gate?.coverage ?? 0}% of ${record.gate?.governed ?? 0} governed source files` },
543
+ ],
544
+ },
545
+ release: { revision: payload.revision, graph: graphAsset, archive: archiveAsset },
546
+ };
547
+ const metadataBytes = Buffer.from(`${JSON.stringify(metadata, null, 2)}\n`);
548
+ writeFileSync(join(site, 'public-graph-meta.json'), metadataBytes);
549
+ writeFileSync(join(site, 'public-spec-release.json'), `${JSON.stringify({
550
+ schema: 'spexcode.public-spec-release/v1',
551
+ revision: payload.revision,
552
+ publication: metadata.publication,
553
+ graph: graphAsset,
554
+ metadata: { path: 'public-graph-meta.json', bytes: metadataBytes.byteLength, sha256: sha256(metadataBytes) },
555
+ archive: archiveAsset,
556
+ documents: documents.map((name) => asset(`specs/${name}`)),
557
+ }, null, 2)}\n`);
558
+ log(`site: ${published.length} nodes at ${payload.revision.slice(0, 12)} → ${site}`);
559
+ return { site, nodes: published.length };
560
+ }
561
+ // @@@ gallerySlug - the path a flat is served at. Derived from the SOURCE the flat read, never from the
562
+ // directory Flatcode happened to write into: two people flattening the same repository must land on the same
563
+ // slug, and a local `--out` name is an accident of one machine.
564
+ export function gallerySlug(source) {
565
+ const url = source.replace(/\.git$/, '').replace(/\/+$/, '');
566
+ const forge = /^(?:https?:\/\/|git@|ssh:\/\/|git:\/\/)[^/:]+[:/](.+)$/.exec(url);
567
+ const raw = forge ? forge[1] : basename(url);
568
+ // Sanitize per SEGMENT, not over the whole string. The slug becomes a directory under the gallery root and
569
+ // a path on a public host, and the source it comes from is attacker-controllable; treating it as one string
570
+ // turns `../../etc/passwd` into `/-/etc/passwd`, which is absolute. Splitting first means a traversal
571
+ // segment cannot survive as one: `..` sanitizes to empty and is dropped, as is any empty or dot segment.
572
+ const segments = raw.split('/')
573
+ .map((segment) => segment.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''))
574
+ .filter((segment) => segment && segment !== '.' && segment !== '..');
575
+ return segments.join('/') || 'flat';
576
+ }
577
+ const escapeHtml = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
578
+ // The colours developers already read a language by. Anything unlisted gets the neutral dot rather than a
579
+ // guessed hue — a wrong-but-confident colour is worse than none.
580
+ const LANGUAGE_DOT = {
581
+ TypeScript: '#3178c6', JavaScript: '#f1e05a', Python: '#3572a5', Go: '#00add8', Rust: '#dea584',
582
+ Ruby: '#701516', Java: '#b07219', Kotlin: '#a97bff', Swift: '#f05138', C: '#555555', 'C++': '#f34b7d',
583
+ 'C#': '#178600', PHP: '#4f5d95', Scala: '#c22d40', Elixir: '#6e4a7e', Erlang: '#b83998',
584
+ Haskell: '#5e5086', OCaml: '#ef7a08', Clojure: '#db5855', Lua: '#000080', Dart: '#00b4ab',
585
+ Vue: '#41b883', Svelte: '#ff3e00', Shell: '#89e051', Zig: '#ec915c',
586
+ };
587
+ // The index is hand-written rather than another dashboard build: it is a LIST, not a graph, and giving it the
588
+ // graph bundle would ship a megabyte of react-flow to render eight links. Self-contained and theme-aware for
589
+ // the same reason every published artifact here is — it must survive on a static host with no build step.
590
+ export function galleryIndexHtml(entries) {
591
+ const cards = entries.map((entry) => {
592
+ const [owner, name] = entry.slug.includes('/') ? [entry.slug.split('/')[0], entry.slug.split('/').slice(1).join('/')] : ['', entry.slug];
593
+ const langs = entry.languages.map((language) => `<span class="lang"><i style="background:${LANGUAGE_DOT[language] ?? '#6b7280'}"></i>${escapeHtml(language)}</span>`).join('');
594
+ return ` <a class="card" href="./${escapeHtml(entry.slug)}/">
595
+ <div class="card-top">
596
+ <h3>${owner ? `<span class="owner">${escapeHtml(owner)}/</span>` : ''}${escapeHtml(name)}</h3>
597
+ <svg class="go" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
598
+ </div>
599
+ <div class="langs">${langs}</div>
600
+ <div class="stats">
601
+ <span><b>${entry.nodes}</b> 个节点</span>
602
+ <span><b>${entry.governed}</b> 个文件</span>
603
+ <span class="${entry.passed ? 'ok' : 'partial'}">${entry.coverage}% 覆盖${entry.passed ? '' : '(部分)'}</span>
604
+ </div>
605
+ <div class="rev"><code>${escapeHtml(entry.revision.slice(0, 12))}</code></div>
606
+ </a>`;
607
+ }).join('\n');
608
+ return `<!doctype html>
609
+ <html lang="zh-CN">
610
+ <head>
611
+ <meta charset="utf-8">
612
+ <meta name="viewport" content="width=device-width, initial-scale=1">
613
+ <title>Flatcode 软件二向箔 | 代码库说明</title>
614
+ <meta name="description" content="Flatcode 为代码库生成 .spec 说明,并检查说明的结构和源码覆盖情况。">
615
+ <!-- Inlined, because a static host with no favicon answers a 404 on every single visit. The mark is the
616
+ name: a solid body above, flattened to a line below. -->
617
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%2310b981'/%3E%3Crect x='8' y='7' width='16' height='9' rx='2' fill='%23041b12' opacity='.75'/%3E%3Crect x='6' y='21' width='20' height='3.5' rx='1.75' fill='%23041b12'/%3E%3C/svg%3E">
618
+ <style>
619
+ /* Committed to dark on purpose: the graph pages a visitor clicks into are dark, and a light shell handing
620
+ off to a dark app is the seam that makes a site feel assembled rather than made. Every colour is painted
621
+ explicitly so nothing inherits the host's theme. */
622
+ :root {
623
+ --bg: #08090b; --panel: #0e1013; --line: #1c1f24; --line-hi: #2b3038;
624
+ --fg: #f2f3f5; --muted: #9096a1; --dim: #6b7280;
625
+ --accent: #10b981; --accent-soft: #34d399;
626
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
627
+ --sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue",
628
+ "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
629
+ }
630
+ * { box-sizing: border-box; }
631
+ html { -webkit-text-size-adjust: 100%; }
632
+ body {
633
+ margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans);
634
+ font-size: 16px; line-height: 1.6; -webkit-font-smoothing: antialiased;
635
+ }
636
+ .wrap { max-width: 68rem; margin: 0 auto; padding: 0 1.5rem; }
637
+
638
+ nav { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem 0; }
639
+ .brand { display: flex; align-items: center; gap: .6rem; font-weight: 600; }
640
+ .brand svg { display: block; }
641
+ .nav-links { display: flex; align-items: center; gap: .5rem; }
642
+ nav a.ghost {
643
+ display: inline-flex; align-items: center; gap: .4rem;
644
+ color: var(--muted); text-decoration: none; font-size: .875rem; padding: .45rem .85rem;
645
+ border: 1px solid var(--line); border-radius: 8px; transition: border-color .15s, color .15s;
646
+ }
647
+ nav a.ghost.icon { padding: .45rem .6rem; }
648
+ nav a.ghost:hover { color: var(--fg); border-color: var(--line-hi); }
649
+
650
+ header.hero {
651
+ position: relative; isolation: isolate; overflow: hidden; min-height: min(33rem, calc(100vh - 9rem));
652
+ margin: 0 calc(50% - 50vw) 4rem; padding: 6rem max(1.5rem, calc((100vw - 68rem) / 2)); display: flex;
653
+ align-items: center; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
654
+ }
655
+ .hero-image { position: absolute; z-index: -2; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
656
+ .hero-shade { position: absolute; z-index: -1; inset: 0; background: rgba(4, 6, 7, .56); }
657
+ .hero-content { max-width: 38rem; }
658
+ .eyebrow {
659
+ font-size: .8125rem; color: var(--accent-soft); margin-bottom: 1.25rem;
660
+ }
661
+ h1 {
662
+ font-size: 4.25rem; line-height: 1.02;
663
+ font-weight: 620; margin: 0 0 1.25rem;
664
+ }
665
+ h1 em { font-style: normal; color: var(--accent-soft); }
666
+ .lede { font-size: 1.125rem; color: #c0c5cc; margin: 0; max-width: 35rem; }
667
+
668
+ section.onboarding { padding-bottom: 4.5rem; }
669
+ .section-label { color: var(--accent-soft); font-family: var(--mono); font-size: .75rem; margin: 0 0 .6rem; }
670
+ .section-intro { max-width: 39rem; color: var(--muted); margin: 0 0 2.25rem; }
671
+ .setup-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
672
+ .setup-step { min-height: 15rem; padding: 1.5rem 1.25rem 1.75rem 0; border-right: 1px solid var(--line); }
673
+ .setup-step + .setup-step { padding-left: 1.25rem; }
674
+ .setup-step:last-child { border-right: 0; }
675
+ .setup-step h3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 600; }
676
+ .setup-step p { color: var(--muted); font-size: .875rem; margin: .8rem 0 0; }
677
+ .step-number { color: var(--dim); font-family: var(--mono); font-size: .75rem; display: block; margin-bottom: .6rem; }
678
+ .agent-choice { display: block; color: var(--muted); font-size: .8125rem; margin-bottom: .45rem; }
679
+ .agent-select { position: relative; }
680
+ .agent-select::after {
681
+ content: ""; position: absolute; right: .85rem; top: 50%; width: .48rem; height: .48rem;
682
+ border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
683
+ transform: translateY(-65%) rotate(45deg); pointer-events: none;
684
+ }
685
+ .agent-select select {
686
+ appearance: none; width: 100%; min-height: 2.75rem; color: var(--fg); background: var(--panel);
687
+ border: 1px solid var(--line-hi); border-radius: 8px; padding: .55rem 2.5rem .55rem .75rem;
688
+ font: inherit; cursor: pointer; transition: border-color .15s, box-shadow .15s, background .15s;
689
+ }
690
+ .agent-select select:hover { background: #12151a; border-color: #3b434e; }
691
+ .agent-select select:focus-visible {
692
+ outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(16, 185, 129, .18);
693
+ }
694
+ .agent-select select option { color: var(--fg); background: var(--panel);
695
+ }
696
+ .command-row { display: flex; align-items: center; gap: .5rem; margin-top: .85rem; }
697
+ .cmd {
698
+ display: flex; align-items: center; gap: .5rem; background: var(--panel); border: 1px solid var(--line);
699
+ border-radius: 8px; padding: .75rem .8rem; font-family: var(--mono); font-size: .8125rem;
700
+ min-width: 0; flex: 1; text-align: left;
701
+ }
702
+ .cmd .prompt { color: var(--accent); user-select: none; }
703
+ .cmd code { color: var(--fg); white-space: nowrap; overflow-x: auto; flex: 1; }
704
+ .copy-button {
705
+ flex: none; width: 2.5rem; height: 2.5rem; display: inline-grid; place-items: center; background: var(--panel);
706
+ border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer;
707
+ transition: color .15s, border-color .15s;
708
+ }
709
+ .copy-button:hover { color: var(--fg); border-color: var(--line-hi); }
710
+ .copy-button svg { width: 1rem; height: 1rem; }
711
+
712
+ section.gallery { padding-bottom: 6rem; }
713
+ .sec-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 1.5rem; }
714
+ .sec-head h2 { font-size: 1.0625rem; font-weight: 600; margin: 0; }
715
+ .sec-head span { color: var(--dim); font-size: .875rem; }
716
+
717
+ .grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); }
718
+ .card {
719
+ display: block; padding: 1.25rem; background: var(--panel); border: 1px solid var(--line);
720
+ border-radius: 8px; color: inherit; text-decoration: none;
721
+ transition: border-color .18s, transform .18s, background .18s;
722
+ }
723
+ .card:hover { border-color: var(--line-hi); background: #12151a; transform: translateY(-2px); }
724
+ .card:hover .go { color: var(--accent-soft); transform: translateX(2px); }
725
+ .card-top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
726
+ .card h3 { margin: 0; font-size: 1rem; font-weight: 600; overflow-wrap: anywhere; }
727
+ .card .owner { color: var(--dim); font-weight: 400; }
728
+ .go { color: var(--dim); flex: none; transition: color .18s, transform .18s; }
729
+ .langs { display: flex; flex-wrap: wrap; gap: .5rem; margin: .85rem 0 1rem; }
730
+ .lang { display: inline-flex; align-items: center; gap: .35rem; font-size: .75rem; color: var(--muted); }
731
+ .lang i { width: .5rem; height: .5rem; border-radius: 50%; display: inline-block; }
732
+ .stats { display: flex; flex-wrap: wrap; gap: .9rem; font-size: .8125rem; color: var(--muted); }
733
+ .stats b { color: var(--fg); font-weight: 600; }
734
+ .stats .ok { color: var(--accent-soft); }
735
+ .stats .partial { color: #fbbf24; }
736
+ .rev { margin-top: .8rem; font-family: var(--mono); font-size: .6875rem; color: #4b5563; }
737
+
738
+ footer { border-top: 1px solid var(--line); padding: 2rem 0 3.5rem; color: var(--dim); font-size: .875rem; }
739
+ footer a { color: var(--muted); text-decoration: none; }
740
+ footer a:hover { color: var(--fg); }
741
+ @media (max-width: 640px) {
742
+ nav { padding: 1rem 0; }
743
+ nav a.ghost { padding: .4rem .55rem; }
744
+ header.hero { min-height: 29rem; margin-bottom: 3rem; padding-top: 4rem; padding-bottom: 3rem; }
745
+ .hero-image { object-position: 66% center; }
746
+ .hero-shade { background: rgba(4, 6, 7, .66); }
747
+ h1 { font-size: 2.625rem; }
748
+ .lede { font-size: 1rem; }
749
+ .setup-grid { grid-template-columns: 1fr; }
750
+ .setup-step, .setup-step + .setup-step { min-height: 0; padding: 1.4rem 0; border-right: 0; border-bottom: 1px solid var(--line); }
751
+ .setup-step:last-child { border-bottom: 0; }
752
+ .cmd { font-size: .75rem; }
753
+ }
754
+ @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
755
+ </style>
756
+ </head>
757
+ <body>
758
+ <div class="wrap">
759
+ <nav>
760
+ <div class="brand">
761
+ <svg width="22" height="22" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7" fill="#10b981"/><rect x="8" y="7" width="16" height="9" rx="2" fill="#041b12" opacity=".75"/><rect x="6" y="21" width="20" height="3.5" rx="1.75" fill="#041b12"/></svg>
762
+ Flatcode
763
+ </div>
764
+ <div class="nav-links">
765
+ <a class="ghost" href="https://spexcode.net/zh/flatcode/">文档</a>
766
+ <a class="ghost icon" href="https://github.com/shuxueshuxue/spexcode" target="_blank" rel="noopener noreferrer" aria-label="GitHub">
767
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
768
+ </a>
769
+ </div>
770
+ </nav>
771
+ </div>
772
+
773
+ <header class="hero">
774
+ <img class="hero-image" src="./flatcode-banner.webp" alt="代码库整理为规格图谱">
775
+ <div class="hero-shade"></div>
776
+ <div class="hero-content">
777
+ <div class="eyebrow">软件二向箔,基于 SpexCode</div>
778
+ <h1>代码库的<em>职责说明</em>。</h1>
779
+ <p class="lede">
780
+ Flatcode 分析一个仓库,为其中的功能和模块生成 .spec 说明。
781
+ 它会检查说明的结构和对源码的覆盖情况,未通过检查的结果会标为部分完成。
782
+ </p>
783
+ </div>
784
+ </header>
785
+
786
+ <div class="wrap">
787
+ <section class="onboarding" aria-labelledby="start-title">
788
+ <p class="section-label">开始转换</p>
789
+ <h2 id="start-title">安装后选择本机 agent</h2>
790
+ <p class="section-intro">只需安装一次。仓库 URL 会被克隆并初始化;本地仓库则直接补全 .spec,已有设置会保留。</p>
791
+ <div class="setup-grid">
792
+ <div class="setup-step">
793
+ <span class="step-number">01</span>
794
+ <h3>安装 SpexCode</h3>
795
+ <div class="cmd"><span class="prompt">$</span><code>npm i -g spexcode</code></div>
796
+ <p>需要 Node 22 或更高版本和 git。</p>
797
+ </div>
798
+ <div class="setup-step">
799
+ <span class="step-number">02</span>
800
+ <h3>选择 agent</h3>
801
+ <label class="agent-choice" for="agent">用于读取源码和生成说明</label>
802
+ <div class="agent-select">
803
+ <select id="agent" name="agent">
804
+ <option value="claude">Claude Code</option>
805
+ <option value="codex">Codex</option>
806
+ <option value="opencode">OpenCode</option>
807
+ <option value="pi">Pi</option>
808
+ </select>
809
+ </div>
810
+ <p>也可以在终端直接运行命令后再选择。</p>
811
+ </div>
812
+ <div class="setup-step">
813
+ <span class="step-number">03</span>
814
+ <h3>转换仓库</h3>
815
+ <div class="command-row">
816
+ <div class="cmd"><span class="prompt">$</span><code id="cmd">spex flat new https://github.com/owner/repo --launcher claude</code></div>
817
+ <button type="button" class="copy-button" id="copy" aria-label="复制命令" title="复制命令">
818
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
819
+ </button>
820
+ </div>
821
+ <p>URL 的结果放在独立目录;本地仓库只会提交 .spec,不会推送。</p>
822
+ </div>
823
+ </div>
824
+ </section>
825
+
826
+ <section class="gallery">
827
+ <div class="sec-head">
828
+ <h2>已生成说明的仓库</h2>
829
+ <span>${entries.length} 个</span>
830
+ </div>
831
+ <div class="grid">
832
+ ${cards}
833
+ </div>
834
+ </section>
835
+
836
+ <footer>
837
+ 由 <a href="https://github.com/shuxueshuxue/spexcode" target="_blank" rel="noopener noreferrer">SpexCode</a> 构建。
838
+ <a href="https://spexcode.net/zh/flatcode/">文档</a>。
839
+ 本站仅显示已提交的 spec,不显示会话,也不提供写入功能。
840
+ </footer>
841
+ </div>
842
+ <script>
843
+ var command = document.getElementById('cmd')
844
+ var agent = document.getElementById('agent')
845
+ agent.addEventListener('change', function () {
846
+ command.textContent = 'spex flat new https://github.com/owner/repo --launcher ' + agent.value
847
+ })
848
+ document.getElementById('copy').addEventListener('click', function () {
849
+ var text = command.textContent
850
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text)
851
+ })
852
+ </script>
853
+ </body>
854
+ </html>
855
+ `;
856
+ }
857
+ export async function flatGallery(out, flatDirs, log = console.log) {
858
+ if (!flatDirs.length)
859
+ throw new Error('spex flat gallery: name at least one flat directory');
860
+ const target = resolve(out);
861
+ mkdirSync(target, { recursive: true });
862
+ const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
863
+ const entries = [];
864
+ const receipts = [];
865
+ for (const dir of flatDirs) {
866
+ const flat = resolve(dir);
867
+ const recordPath = join(flat, 'flat.json');
868
+ if (!existsSync(recordPath))
869
+ throw new Error(`spex flat gallery: ${flat} is not a flat — no flat.json`);
870
+ const record = JSON.parse(readFileSync(recordPath, 'utf8'));
871
+ const site = join(flat, 'site');
872
+ if (!existsSync(join(site, 'index.html'))) {
873
+ throw new Error(`spex flat gallery: ${flat} has no site — run \`spex flat site ${dir}\` first`);
874
+ }
875
+ const slug = gallerySlug(record.source);
876
+ const dest = join(target, slug);
877
+ mkdirSync(dest, { recursive: true });
878
+ cpSync(site, dest, { recursive: true });
879
+ const graph = JSON.parse(readFileSync(join(dest, 'public-graph.json'), 'utf8'));
880
+ entries.push({
881
+ slug,
882
+ source: record.source,
883
+ revision: record.revision,
884
+ coverage: record.gate?.coverage ?? 0,
885
+ governed: record.gate?.governed ?? 0,
886
+ nodes: graph.nodes.length,
887
+ passed: record.passed === true,
888
+ languages: record.profile?.languages ?? [],
889
+ lang: record.lang ?? 'en',
890
+ });
891
+ const release = join(dest, 'public-spec-release.json');
892
+ receipts.push({ slug, release: `${slug}/public-spec-release.json`, sha256: sha256(readFileSync(release)) });
893
+ log(` ${slug} — ${graph.nodes.length} nodes, ${record.gate?.coverage ?? 0}% of ${record.gate?.governed ?? 0} governed`);
894
+ }
895
+ // @@@ order - a tree written in the reader's own language goes first. The site defaults to Chinese, so a
896
+ // Chinese-prose flat is the one that demonstrates the product to the visitor it is aimed at; an English tree
897
+ // shown first makes the whole page look like a translation of somebody else's thing.
898
+ const rank = (entry) => (entry.lang.toLowerCase().startsWith('zh') ? 0 : entry.lang.toLowerCase() === 'en' ? 2 : 1);
899
+ entries.sort((a, b) => rank(a) - rank(b) || a.slug.localeCompare(b.slug));
900
+ receipts.sort((a, b) => a.slug.localeCompare(b.slug));
901
+ copyFileSync(join(PKG, 'src', 'flatcode-banner.webp'), join(target, 'flatcode-banner.webp'));
902
+ writeFileSync(join(target, 'index.html'), galleryIndexHtml(entries));
903
+ // The manifest is what makes a publish auditable: it names every entry and hashes each flat's own release
904
+ // manifest, so what landed on a host can be compared with what was built without trusting the transport.
905
+ writeFileSync(join(target, 'gallery.json'), `${JSON.stringify({
906
+ schema: 'spexcode.flat-gallery/v1',
907
+ entries: entries.map((entry) => ({ ...entry })),
908
+ releases: receipts,
909
+ }, null, 2)}\n`);
910
+ log(`gallery: ${entries.length} flat(s) → ${target}`);
911
+ return entries;
912
+ }
913
+ export async function runFlat(argv) {
914
+ const sub = argv[0];
915
+ const { commandHelp } = await import('./help.js');
916
+ if (sub === undefined || sub === '--help' || sub === '-h') {
917
+ console.log(commandHelp('flat'));
918
+ return sub === undefined ? 0 : 0;
919
+ }
920
+ if (sub === 'site') {
921
+ const dir = argv[1];
922
+ if (!dir || argv.length !== 2) {
923
+ console.error('usage: spex flat site <flat-dir>');
924
+ return 2;
925
+ }
926
+ try {
927
+ await flatSite(dir);
928
+ return 0;
929
+ }
930
+ catch (error) {
931
+ console.error(error.message);
932
+ return 1;
933
+ }
934
+ }
935
+ if (sub === 'gallery') {
936
+ const rest = argv.slice(1);
937
+ const at = rest.indexOf('--out');
938
+ const out = at >= 0 ? rest[at + 1] : undefined;
939
+ const dirs = rest.filter((token, index) => token !== '--out' && !(at >= 0 && index === at + 1));
940
+ if (!out || !dirs.length) {
941
+ console.error('usage: spex flat gallery --out <dir> <flat-dir>…');
942
+ return 2;
943
+ }
944
+ try {
945
+ await flatGallery(out, dirs);
946
+ return 0;
947
+ }
948
+ catch (error) {
949
+ console.error(error.message);
950
+ return 1;
951
+ }
952
+ }
953
+ if (sub !== 'new') {
954
+ console.error(`spex flat: unknown subcommand "${sub}". Run \`spex flat\` for the command map.`);
955
+ return 2;
956
+ }
957
+ const known = new Set(['out', 'launcher', 'rounds', 'coverage', 'lang']);
958
+ const flags = new Map();
959
+ const positional = [];
960
+ const rest = argv.slice(1);
961
+ for (let at = 0; at < rest.length; at += 1) {
962
+ const token = rest[at];
963
+ if (!token.startsWith('--')) {
964
+ positional.push(token);
965
+ continue;
966
+ }
967
+ const name = token.slice(2);
968
+ if (!known.has(name)) {
969
+ console.error(`spex flat new: unknown flag --${name} (known: ${[...known].map((k) => `--${k}`).join(', ')})`);
970
+ return 2;
971
+ }
972
+ const value = rest[at + 1];
973
+ if (value === undefined || value.startsWith('--')) {
974
+ console.error(`spex flat new: --${name} expects a value`);
975
+ return 2;
976
+ }
977
+ flags.set(name, value);
978
+ at += 1;
979
+ }
980
+ if (positional.length !== 1) {
981
+ console.error('usage: spex flat new <repo-url|path> [--out <dir>] [--launcher <name>] [--rounds <n>] [--coverage <pct>]');
982
+ return 2;
983
+ }
984
+ const number = (name) => {
985
+ const raw = flags.get(name);
986
+ if (raw === undefined)
987
+ return undefined;
988
+ const value = Number(raw);
989
+ if (!Number.isFinite(value))
990
+ throw new Error(`spex flat: --${name} expects a number, got ${JSON.stringify(raw)}`);
991
+ return value;
992
+ };
993
+ try {
994
+ const result = await flatNew({
995
+ target: positional[0],
996
+ out: flags.get('out'),
997
+ launcher: flags.get('launcher'),
998
+ rounds: number('rounds'),
999
+ coverage: number('coverage'),
1000
+ lang: flags.get('lang'),
1001
+ });
1002
+ const { gate } = result;
1003
+ console.log('');
1004
+ console.log(`${result.passed ? 'converged' : 'PARTIAL'} after ${result.rounds} round(s)`);
1005
+ console.log(` coverage ${gate.coverage}% of ${gate.governed} governed source files`);
1006
+ console.log(` lint ${gate.errors} error(s)`);
1007
+ console.log(` flat ${result.out}`);
1008
+ if (!result.passed) {
1009
+ // A partial flat reports what still fails and exits non-zero. Reporting a converged tree we did not
1010
+ // measure would make the gate decorative.
1011
+ console.log('');
1012
+ console.log('still failing:');
1013
+ for (const finding of gate.errorFindings.slice(0, 10))
1014
+ console.log(` ${finding}`);
1015
+ if (gate.uncovered)
1016
+ console.log(` ${gate.uncovered} source file(s) still unclaimed by any node`);
1017
+ console.log('');
1018
+ console.log('Raise --rounds to keep going, or lower --coverage if this repository has files no spec should claim.');
1019
+ return 1;
1020
+ }
1021
+ return 0;
1022
+ }
1023
+ catch (error) {
1024
+ console.error(error.message);
1025
+ return 1;
1026
+ }
1027
+ }
1028
+ async function gateOf(repo) {
1029
+ const lint = await spex(['spec', 'lint', '--json'], repo);
1030
+ if (!lint.stdout.trim())
1031
+ throw new Error(`spex flat: spec lint produced no report — ${lint.stderr.trim()}`);
1032
+ return readGate(lint.stdout);
1033
+ }
1034
+ // Flatcode's own commits bypass the lint hook on purpose: an intermediate round is EXPECTED to be incomplete,
1035
+ // and a hook that refused it would strand the round's work uncommitted where the next round cannot see it.
1036
+ // The gate reading, not the commit hook, is what decides whether this flat converged. The path list is also
1037
+ // the write boundary for an adopted local repository: Flatcode owns intent, never the source it describes.
1038
+ async function commit(repo, message, paths) {
1039
+ await git(['add', '--', ...paths], repo);
1040
+ const staged = await git(['diff', '--cached', '--quiet'], repo);
1041
+ if (staged.code === 0)
1042
+ return;
1043
+ const result = await run('git', ['-c', 'user.name=Flatcode', '-c', 'user.email=flatcode@spexcode.invalid', 'commit', '--quiet', '--no-verify', '-m', message], repo);
1044
+ if (result.code !== 0)
1045
+ throw new Error(`spex flat: committing "${message}" failed — ${result.stderr.trim()}`);
1046
+ }
1047
+ async function assertOnlyFlatChanges(repo, before, beforeUntracked) {
1048
+ const readings = await Promise.all([
1049
+ gitOrThrow(['diff', '--name-only', `${before}..`], repo, 'checking committed agent changes'),
1050
+ gitOrThrow(['diff', '--name-only'], repo, 'checking unstaged agent changes'),
1051
+ gitOrThrow(['diff', '--cached', '--name-only'], repo, 'checking staged agent changes'),
1052
+ gitOrThrow(['ls-files', '--others', '--exclude-standard'], repo, 'checking untracked agent changes'),
1053
+ ]);
1054
+ const changed = [...new Set(readings.flatMap((reading, index) => reading.split('\n').filter((path) => Boolean(path) && (index !== 3 || !beforeUntracked.has(path)))))];
1055
+ const forbidden = changed.filter((path) => path !== '.spec' && !path.startsWith('.spec/'));
1056
+ if (forbidden.length) {
1057
+ // The repository was clean before the round. Resetting to that exact revision leaves the agent's .spec
1058
+ // edits in the working tree, then restoring only baseline non-spec paths erases every source/config edit
1059
+ // the agent made, including ones it committed itself. New non-spec paths did not exist at the boundary and
1060
+ // are removed by their exact git-reported names. A rejected round therefore cannot leave source changes
1061
+ // behind under the misleading name of a documentation run.
1062
+ const baseline = new Set((await gitOrThrow(['ls-tree', '-r', '--name-only', before], repo, 'loading the round baseline')).split('\n').filter(Boolean));
1063
+ await gitOrThrow(['reset', '--mixed', before], repo, 'discarding the agent commit');
1064
+ for (const path of forbidden) {
1065
+ if (baseline.has(path)) {
1066
+ await gitOrThrow(['checkout', before, '--', path], repo, `restoring ${path}`);
1067
+ continue;
1068
+ }
1069
+ const target = resolve(repo, path);
1070
+ if (!target.startsWith(`${repo}${sep}`))
1071
+ throw new Error(`spex flat: invalid agent path ${JSON.stringify(path)}`);
1072
+ rmSync(target, { force: true, recursive: true });
1073
+ }
1074
+ throw new Error(`spex flat: the agent changed files outside .spec (${forbidden.join(', ')}). Flatcode discarded those changes and will not commit them.`);
1075
+ }
1076
+ }