@cluesmith/codev 2.0.0-rc.9 → 2.0.1

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 (537) hide show
  1. package/bin/af.js +2 -2
  2. package/bin/consult.js +1 -1
  3. package/dashboard/dist/assets/index-4n9zpWLY.css +32 -0
  4. package/dashboard/dist/assets/index-b38SaXk5.js +136 -0
  5. package/dashboard/dist/assets/index-b38SaXk5.js.map +1 -0
  6. package/dashboard/dist/index.html +14 -0
  7. package/dist/agent-farm/cli.d.ts.map +1 -1
  8. package/dist/agent-farm/cli.js +179 -104
  9. package/dist/agent-farm/cli.js.map +1 -1
  10. package/dist/agent-farm/commands/architect.d.ts +3 -3
  11. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  12. package/dist/agent-farm/commands/architect.js +20 -147
  13. package/dist/agent-farm/commands/architect.js.map +1 -1
  14. package/dist/agent-farm/commands/attach.d.ts +13 -0
  15. package/dist/agent-farm/commands/attach.d.ts.map +1 -0
  16. package/dist/agent-farm/commands/attach.js +144 -0
  17. package/dist/agent-farm/commands/attach.js.map +1 -0
  18. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  19. package/dist/agent-farm/commands/cleanup.js +35 -19
  20. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  21. package/dist/agent-farm/commands/consult.d.ts +3 -4
  22. package/dist/agent-farm/commands/consult.d.ts.map +1 -1
  23. package/dist/agent-farm/commands/consult.js +27 -37
  24. package/dist/agent-farm/commands/consult.js.map +1 -1
  25. package/dist/agent-farm/commands/index.d.ts +2 -2
  26. package/dist/agent-farm/commands/index.d.ts.map +1 -1
  27. package/dist/agent-farm/commands/index.js +2 -2
  28. package/dist/agent-farm/commands/index.js.map +1 -1
  29. package/dist/agent-farm/commands/open.d.ts +4 -2
  30. package/dist/agent-farm/commands/open.d.ts.map +1 -1
  31. package/dist/agent-farm/commands/open.js +33 -83
  32. package/dist/agent-farm/commands/open.js.map +1 -1
  33. package/dist/agent-farm/commands/send.d.ts +1 -1
  34. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  35. package/dist/agent-farm/commands/send.js +70 -79
  36. package/dist/agent-farm/commands/send.js.map +1 -1
  37. package/dist/agent-farm/commands/shell.d.ts +15 -0
  38. package/dist/agent-farm/commands/shell.d.ts.map +1 -0
  39. package/dist/agent-farm/commands/shell.js +50 -0
  40. package/dist/agent-farm/commands/shell.js.map +1 -0
  41. package/dist/agent-farm/commands/spawn-roles.d.ts +80 -0
  42. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -0
  43. package/dist/agent-farm/commands/spawn-roles.js +278 -0
  44. package/dist/agent-farm/commands/spawn-roles.js.map +1 -0
  45. package/dist/agent-farm/commands/spawn-worktree.d.ts +96 -0
  46. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -0
  47. package/dist/agent-farm/commands/spawn-worktree.js +305 -0
  48. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -0
  49. package/dist/agent-farm/commands/spawn.d.ts +5 -1
  50. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  51. package/dist/agent-farm/commands/spawn.js +241 -561
  52. package/dist/agent-farm/commands/spawn.js.map +1 -1
  53. package/dist/agent-farm/commands/start.d.ts +10 -20
  54. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  55. package/dist/agent-farm/commands/start.js +45 -449
  56. package/dist/agent-farm/commands/start.js.map +1 -1
  57. package/dist/agent-farm/commands/status.d.ts +2 -0
  58. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  59. package/dist/agent-farm/commands/status.js +75 -24
  60. package/dist/agent-farm/commands/status.js.map +1 -1
  61. package/dist/agent-farm/commands/stop.d.ts +6 -0
  62. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  63. package/dist/agent-farm/commands/stop.js +49 -109
  64. package/dist/agent-farm/commands/stop.js.map +1 -1
  65. package/dist/agent-farm/commands/tower-cloud.d.ts +48 -0
  66. package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
  67. package/dist/agent-farm/commands/tower-cloud.js +293 -0
  68. package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
  69. package/dist/agent-farm/commands/tower.d.ts +9 -0
  70. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  71. package/dist/agent-farm/commands/tower.js +59 -19
  72. package/dist/agent-farm/commands/tower.js.map +1 -1
  73. package/dist/agent-farm/db/index.d.ts +6 -2
  74. package/dist/agent-farm/db/index.d.ts.map +1 -1
  75. package/dist/agent-farm/db/index.js +301 -19
  76. package/dist/agent-farm/db/index.js.map +1 -1
  77. package/dist/agent-farm/db/migrate.d.ts +0 -4
  78. package/dist/agent-farm/db/migrate.d.ts.map +1 -1
  79. package/dist/agent-farm/db/migrate.js +6 -55
  80. package/dist/agent-farm/db/migrate.js.map +1 -1
  81. package/dist/agent-farm/db/schema.d.ts +3 -3
  82. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  83. package/dist/agent-farm/db/schema.js +25 -19
  84. package/dist/agent-farm/db/schema.js.map +1 -1
  85. package/dist/agent-farm/db/types.d.ts +3 -13
  86. package/dist/agent-farm/db/types.d.ts.map +1 -1
  87. package/dist/agent-farm/db/types.js +3 -11
  88. package/dist/agent-farm/db/types.js.map +1 -1
  89. package/dist/agent-farm/hq-connector.d.ts +2 -6
  90. package/dist/agent-farm/hq-connector.d.ts.map +1 -1
  91. package/dist/agent-farm/hq-connector.js +2 -17
  92. package/dist/agent-farm/hq-connector.js.map +1 -1
  93. package/dist/agent-farm/lib/cloud-config.d.ts +59 -0
  94. package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
  95. package/dist/agent-farm/lib/cloud-config.js +143 -0
  96. package/dist/agent-farm/lib/cloud-config.js.map +1 -0
  97. package/dist/agent-farm/lib/device-name.d.ts +25 -0
  98. package/dist/agent-farm/lib/device-name.d.ts.map +1 -0
  99. package/dist/agent-farm/lib/device-name.js +46 -0
  100. package/dist/agent-farm/lib/device-name.js.map +1 -0
  101. package/dist/agent-farm/lib/nonce-store.d.ts +28 -0
  102. package/dist/agent-farm/lib/nonce-store.d.ts.map +1 -0
  103. package/dist/agent-farm/lib/nonce-store.js +60 -0
  104. package/dist/agent-farm/lib/nonce-store.js.map +1 -0
  105. package/dist/agent-farm/lib/token-exchange.d.ts +18 -0
  106. package/dist/agent-farm/lib/token-exchange.d.ts.map +1 -0
  107. package/dist/agent-farm/lib/token-exchange.js +48 -0
  108. package/dist/agent-farm/lib/token-exchange.js.map +1 -0
  109. package/dist/agent-farm/lib/tower-client.d.ts +163 -0
  110. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
  111. package/dist/agent-farm/lib/tower-client.js +233 -0
  112. package/dist/agent-farm/lib/tower-client.js.map +1 -0
  113. package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
  114. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
  115. package/dist/agent-farm/lib/tunnel-client.js +504 -0
  116. package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
  117. package/dist/agent-farm/servers/tower-instances.d.ts +82 -0
  118. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -0
  119. package/dist/agent-farm/servers/tower-instances.js +454 -0
  120. package/dist/agent-farm/servers/tower-instances.js.map +1 -0
  121. package/dist/agent-farm/servers/tower-routes.d.ts +34 -0
  122. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -0
  123. package/dist/agent-farm/servers/tower-routes.js +1445 -0
  124. package/dist/agent-farm/servers/tower-routes.js.map +1 -0
  125. package/dist/agent-farm/servers/tower-server.d.ts +5 -2
  126. package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
  127. package/dist/agent-farm/servers/tower-server.js +157 -475
  128. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  129. package/dist/agent-farm/servers/tower-terminals.d.ts +119 -0
  130. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -0
  131. package/dist/agent-farm/servers/tower-terminals.js +629 -0
  132. package/dist/agent-farm/servers/tower-terminals.js.map +1 -0
  133. package/dist/agent-farm/servers/tower-tunnel.d.ts +34 -0
  134. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -0
  135. package/dist/agent-farm/servers/tower-tunnel.js +480 -0
  136. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -0
  137. package/dist/agent-farm/servers/tower-types.d.ts +86 -0
  138. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -0
  139. package/dist/agent-farm/servers/tower-types.js +6 -0
  140. package/dist/agent-farm/servers/tower-types.js.map +1 -0
  141. package/dist/agent-farm/servers/tower-utils.d.ts +58 -0
  142. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -0
  143. package/dist/agent-farm/servers/tower-utils.js +182 -0
  144. package/dist/agent-farm/servers/tower-utils.js.map +1 -0
  145. package/dist/agent-farm/servers/tower-websocket.d.ts +25 -0
  146. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -0
  147. package/dist/agent-farm/servers/tower-websocket.js +171 -0
  148. package/dist/agent-farm/servers/tower-websocket.js.map +1 -0
  149. package/dist/agent-farm/state.d.ts +6 -2
  150. package/dist/agent-farm/state.d.ts.map +1 -1
  151. package/dist/agent-farm/state.js +34 -25
  152. package/dist/agent-farm/state.js.map +1 -1
  153. package/dist/agent-farm/types.d.ts +49 -26
  154. package/dist/agent-farm/types.d.ts.map +1 -1
  155. package/dist/agent-farm/utils/config.d.ts +0 -5
  156. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  157. package/dist/agent-farm/utils/config.js +12 -44
  158. package/dist/agent-farm/utils/config.js.map +1 -1
  159. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  160. package/dist/agent-farm/utils/deps.js +0 -32
  161. package/dist/agent-farm/utils/deps.js.map +1 -1
  162. package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
  163. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
  164. package/dist/agent-farm/utils/file-tabs.js +46 -0
  165. package/dist/agent-farm/utils/file-tabs.js.map +1 -0
  166. package/dist/agent-farm/utils/gate-status.d.ts +16 -0
  167. package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
  168. package/dist/agent-farm/utils/gate-status.js +79 -0
  169. package/dist/agent-farm/utils/gate-status.js.map +1 -0
  170. package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
  171. package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
  172. package/dist/agent-farm/utils/gate-watcher.js +122 -0
  173. package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
  174. package/dist/agent-farm/utils/index.d.ts +0 -1
  175. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  176. package/dist/agent-farm/utils/index.js +0 -1
  177. package/dist/agent-farm/utils/index.js.map +1 -1
  178. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  179. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  180. package/dist/agent-farm/utils/notifications.js +121 -0
  181. package/dist/agent-farm/utils/notifications.js.map +1 -0
  182. package/dist/agent-farm/utils/server-utils.d.ts +5 -5
  183. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  184. package/dist/agent-farm/utils/server-utils.js +5 -16
  185. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  186. package/dist/agent-farm/utils/session.d.ts +32 -0
  187. package/dist/agent-farm/utils/session.d.ts.map +1 -0
  188. package/dist/agent-farm/utils/session.js +57 -0
  189. package/dist/agent-farm/utils/session.js.map +1 -0
  190. package/dist/agent-farm/utils/shell.d.ts +9 -22
  191. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  192. package/dist/agent-farm/utils/shell.js +34 -34
  193. package/dist/agent-farm/utils/shell.js.map +1 -1
  194. package/dist/cli.d.ts.map +1 -1
  195. package/dist/cli.js +6 -37
  196. package/dist/cli.js.map +1 -1
  197. package/dist/commands/adopt.d.ts.map +1 -1
  198. package/dist/commands/adopt.js +33 -4
  199. package/dist/commands/adopt.js.map +1 -1
  200. package/dist/commands/consult/index.d.ts +13 -2
  201. package/dist/commands/consult/index.d.ts.map +1 -1
  202. package/dist/commands/consult/index.js +244 -29
  203. package/dist/commands/consult/index.js.map +1 -1
  204. package/dist/commands/doctor.d.ts.map +1 -1
  205. package/dist/commands/doctor.js +96 -79
  206. package/dist/commands/doctor.js.map +1 -1
  207. package/dist/commands/init.d.ts.map +1 -1
  208. package/dist/commands/init.js +36 -3
  209. package/dist/commands/init.js.map +1 -1
  210. package/dist/commands/porch/build-counter.d.ts +5 -0
  211. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  212. package/dist/commands/porch/build-counter.js +5 -0
  213. package/dist/commands/porch/build-counter.js.map +1 -0
  214. package/dist/commands/porch/checks.d.ts +3 -2
  215. package/dist/commands/porch/checks.d.ts.map +1 -1
  216. package/dist/commands/porch/checks.js +8 -2
  217. package/dist/commands/porch/checks.js.map +1 -1
  218. package/dist/commands/porch/index.d.ts +4 -0
  219. package/dist/commands/porch/index.d.ts.map +1 -1
  220. package/dist/commands/porch/index.js +109 -70
  221. package/dist/commands/porch/index.js.map +1 -1
  222. package/dist/commands/porch/next.d.ts +22 -0
  223. package/dist/commands/porch/next.d.ts.map +1 -0
  224. package/dist/commands/porch/next.js +571 -0
  225. package/dist/commands/porch/next.js.map +1 -0
  226. package/dist/commands/porch/plan.d.ts +11 -1
  227. package/dist/commands/porch/plan.d.ts.map +1 -1
  228. package/dist/commands/porch/plan.js +33 -5
  229. package/dist/commands/porch/plan.js.map +1 -1
  230. package/dist/commands/porch/prompts.d.ts.map +1 -1
  231. package/dist/commands/porch/prompts.js +44 -26
  232. package/dist/commands/porch/prompts.js.map +1 -1
  233. package/dist/commands/porch/protocol.d.ts +6 -4
  234. package/dist/commands/porch/protocol.d.ts.map +1 -1
  235. package/dist/commands/porch/protocol.js +59 -15
  236. package/dist/commands/porch/protocol.js.map +1 -1
  237. package/dist/commands/porch/state.d.ts +29 -2
  238. package/dist/commands/porch/state.d.ts.map +1 -1
  239. package/dist/commands/porch/state.js +71 -3
  240. package/dist/commands/porch/state.js.map +1 -1
  241. package/dist/commands/porch/types.d.ts +45 -2
  242. package/dist/commands/porch/types.d.ts.map +1 -1
  243. package/dist/commands/porch/verdict.d.ts +31 -0
  244. package/dist/commands/porch/verdict.d.ts.map +1 -0
  245. package/dist/commands/porch/verdict.js +59 -0
  246. package/dist/commands/porch/verdict.js.map +1 -0
  247. package/dist/commands/update.d.ts.map +1 -1
  248. package/dist/commands/update.js +18 -6
  249. package/dist/commands/update.js.map +1 -1
  250. package/dist/lib/scaffold.d.ts +13 -0
  251. package/dist/lib/scaffold.d.ts.map +1 -1
  252. package/dist/lib/scaffold.js +36 -0
  253. package/dist/lib/scaffold.js.map +1 -1
  254. package/dist/terminal/index.d.ts +8 -0
  255. package/dist/terminal/index.d.ts.map +1 -0
  256. package/dist/terminal/index.js +5 -0
  257. package/dist/terminal/index.js.map +1 -0
  258. package/dist/terminal/pty-manager.d.ts +69 -0
  259. package/dist/terminal/pty-manager.d.ts.map +1 -0
  260. package/dist/terminal/pty-manager.js +377 -0
  261. package/dist/terminal/pty-manager.js.map +1 -0
  262. package/dist/terminal/pty-session.d.ts +104 -0
  263. package/dist/terminal/pty-session.d.ts.map +1 -0
  264. package/dist/terminal/pty-session.js +327 -0
  265. package/dist/terminal/pty-session.js.map +1 -0
  266. package/dist/terminal/ring-buffer.d.ts +34 -0
  267. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  268. package/dist/terminal/ring-buffer.js +94 -0
  269. package/dist/terminal/ring-buffer.js.map +1 -0
  270. package/dist/terminal/session-manager.d.ts +115 -0
  271. package/dist/terminal/session-manager.d.ts.map +1 -0
  272. package/dist/terminal/session-manager.js +582 -0
  273. package/dist/terminal/session-manager.js.map +1 -0
  274. package/dist/terminal/shellper-client.d.ts +66 -0
  275. package/dist/terminal/shellper-client.d.ts.map +1 -0
  276. package/dist/terminal/shellper-client.js +234 -0
  277. package/dist/terminal/shellper-client.js.map +1 -0
  278. package/dist/terminal/shellper-main.d.ts +19 -0
  279. package/dist/terminal/shellper-main.d.ts.map +1 -0
  280. package/dist/terminal/shellper-main.js +153 -0
  281. package/dist/terminal/shellper-main.js.map +1 -0
  282. package/dist/terminal/shellper-process.d.ts +75 -0
  283. package/dist/terminal/shellper-process.d.ts.map +1 -0
  284. package/dist/terminal/shellper-process.js +279 -0
  285. package/dist/terminal/shellper-process.js.map +1 -0
  286. package/dist/terminal/shellper-protocol.d.ts +115 -0
  287. package/dist/terminal/shellper-protocol.d.ts.map +1 -0
  288. package/dist/terminal/shellper-protocol.js +214 -0
  289. package/dist/terminal/shellper-protocol.js.map +1 -0
  290. package/dist/terminal/shellper-replay-buffer.d.ts +38 -0
  291. package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -0
  292. package/dist/terminal/shellper-replay-buffer.js +94 -0
  293. package/dist/terminal/shellper-replay-buffer.js.map +1 -0
  294. package/dist/terminal/ws-protocol.d.ts +27 -0
  295. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  296. package/dist/terminal/ws-protocol.js +44 -0
  297. package/dist/terminal/ws-protocol.js.map +1 -0
  298. package/package.json +17 -5
  299. package/skeleton/.claude/skills/af/SKILL.md +89 -0
  300. package/skeleton/.claude/skills/codev/SKILL.md +41 -0
  301. package/skeleton/.claude/skills/consult/SKILL.md +81 -0
  302. package/skeleton/.claude/skills/generate-image/SKILL.md +56 -0
  303. package/skeleton/DEPENDENCIES.md +4 -62
  304. package/skeleton/builders.md +1 -1
  305. package/skeleton/consult-types/impl-review.md +18 -9
  306. package/skeleton/consult-types/integration-review.md +1 -1
  307. package/skeleton/consult-types/plan-review.md +1 -1
  308. package/skeleton/consult-types/pr-ready.md +1 -1
  309. package/skeleton/consult-types/spec-review.md +1 -1
  310. package/skeleton/porch/prompts/defend.md +1 -1
  311. package/skeleton/porch/prompts/evaluate.md +2 -2
  312. package/skeleton/porch/prompts/implement.md +1 -1
  313. package/skeleton/porch/prompts/plan.md +1 -1
  314. package/skeleton/porch/prompts/review.md +4 -4
  315. package/skeleton/porch/prompts/specify.md +1 -1
  316. package/skeleton/porch/prompts/understand.md +2 -2
  317. package/skeleton/protocol-schema.json +282 -0
  318. package/skeleton/protocols/bugfix/builder-prompt.md +60 -0
  319. package/skeleton/protocols/bugfix/prompts/fix.md +77 -0
  320. package/skeleton/protocols/bugfix/prompts/investigate.md +77 -0
  321. package/skeleton/protocols/bugfix/prompts/pr.md +84 -0
  322. package/skeleton/protocols/bugfix/protocol.json +20 -33
  323. package/skeleton/protocols/experiment/builder-prompt.md +52 -0
  324. package/skeleton/protocols/experiment/protocol.json +101 -0
  325. package/skeleton/protocols/experiment/protocol.md +3 -3
  326. package/skeleton/protocols/experiment/templates/notes.md +1 -1
  327. package/skeleton/protocols/maintain/builder-prompt.md +46 -0
  328. package/skeleton/protocols/maintain/prompts/audit.md +111 -0
  329. package/skeleton/protocols/maintain/prompts/clean.md +91 -0
  330. package/skeleton/protocols/maintain/prompts/sync.md +113 -0
  331. package/skeleton/protocols/maintain/prompts/verify.md +110 -0
  332. package/skeleton/protocols/maintain/protocol.json +141 -0
  333. package/skeleton/protocols/maintain/protocol.md +17 -11
  334. package/skeleton/protocols/protocol-schema.json +54 -1
  335. package/skeleton/protocols/spir/builder-prompt.md +66 -0
  336. package/skeleton/protocols/spir/prompts/implement.md +208 -0
  337. package/skeleton/protocols/{spider → spir}/prompts/plan.md +6 -70
  338. package/skeleton/protocols/{spider → spir}/prompts/review.md +20 -39
  339. package/skeleton/protocols/{spider → spir}/prompts/specify.md +24 -59
  340. package/skeleton/protocols/{spider → spir}/protocol.json +30 -10
  341. package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
  342. package/skeleton/protocols/spir/templates/review.md +89 -0
  343. package/skeleton/protocols/tick/builder-prompt.md +56 -0
  344. package/skeleton/protocols/tick/protocol.json +7 -2
  345. package/skeleton/protocols/tick/protocol.md +18 -18
  346. package/skeleton/protocols/tick/templates/review.md +1 -1
  347. package/skeleton/resources/commands/agent-farm.md +63 -46
  348. package/skeleton/resources/commands/codev.md +0 -2
  349. package/skeleton/resources/commands/overview.md +7 -17
  350. package/skeleton/resources/workflow-reference.md +4 -4
  351. package/skeleton/roles/architect.md +151 -306
  352. package/skeleton/roles/builder.md +115 -332
  353. package/skeleton/roles/consultant.md +6 -6
  354. package/skeleton/templates/AGENTS.md +2 -2
  355. package/skeleton/templates/CLAUDE.md +2 -2
  356. package/skeleton/templates/cheatsheet.md +7 -5
  357. package/skeleton/templates/projectlist.md +1 -1
  358. package/templates/dashboard/index.html +17 -16
  359. package/templates/dashboard/js/dialogs.js +7 -7
  360. package/templates/dashboard/js/files.js +2 -2
  361. package/templates/dashboard/js/main.js +4 -4
  362. package/templates/dashboard/js/projects.js +3 -3
  363. package/templates/dashboard/js/tabs.js +1 -1
  364. package/templates/dashboard/js/utils.js +22 -1
  365. package/templates/open.html +26 -0
  366. package/templates/tower.html +731 -91
  367. package/dist/agent-farm/commands/kickoff.d.ts +0 -20
  368. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  369. package/dist/agent-farm/commands/kickoff.js +0 -273
  370. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  371. package/dist/agent-farm/commands/rename.d.ts +0 -13
  372. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  373. package/dist/agent-farm/commands/rename.js +0 -33
  374. package/dist/agent-farm/commands/rename.js.map +0 -1
  375. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  376. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  377. package/dist/agent-farm/commands/tutorial.js +0 -49
  378. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  379. package/dist/agent-farm/commands/util.d.ts +0 -15
  380. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  381. package/dist/agent-farm/commands/util.js +0 -108
  382. package/dist/agent-farm/commands/util.js.map +0 -1
  383. package/dist/agent-farm/servers/dashboard-server.d.ts +0 -7
  384. package/dist/agent-farm/servers/dashboard-server.d.ts.map +0 -1
  385. package/dist/agent-farm/servers/dashboard-server.js +0 -1858
  386. package/dist/agent-farm/servers/dashboard-server.js.map +0 -1
  387. package/dist/agent-farm/servers/open-server.d.ts +0 -7
  388. package/dist/agent-farm/servers/open-server.d.ts.map +0 -1
  389. package/dist/agent-farm/servers/open-server.js +0 -315
  390. package/dist/agent-farm/servers/open-server.js.map +0 -1
  391. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  392. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  393. package/dist/agent-farm/tutorial/index.js +0 -8
  394. package/dist/agent-farm/tutorial/index.js.map +0 -1
  395. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  396. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  397. package/dist/agent-farm/tutorial/prompts.js +0 -147
  398. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  399. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  400. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  401. package/dist/agent-farm/tutorial/runner.js +0 -204
  402. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  403. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  404. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  405. package/dist/agent-farm/tutorial/state.js +0 -89
  406. package/dist/agent-farm/tutorial/state.js.map +0 -1
  407. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  408. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  409. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  410. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  411. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  412. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  413. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  414. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  415. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  416. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  417. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  418. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  419. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  420. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  421. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  422. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  423. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  424. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  425. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  426. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  427. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  428. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  429. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  430. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  431. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  432. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  433. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  434. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  435. package/dist/agent-farm/utils/orphan-handler.d.ts +0 -27
  436. package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
  437. package/dist/agent-farm/utils/orphan-handler.js +0 -149
  438. package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
  439. package/dist/agent-farm/utils/port-registry.d.ts +0 -58
  440. package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
  441. package/dist/agent-farm/utils/port-registry.js +0 -166
  442. package/dist/agent-farm/utils/port-registry.js.map +0 -1
  443. package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
  444. package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
  445. package/dist/agent-farm/utils/terminal-ports.js +0 -35
  446. package/dist/agent-farm/utils/terminal-ports.js.map +0 -1
  447. package/dist/commands/pcheck/cache.d.ts +0 -48
  448. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  449. package/dist/commands/pcheck/cache.js +0 -170
  450. package/dist/commands/pcheck/cache.js.map +0 -1
  451. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  452. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  453. package/dist/commands/pcheck/evaluator.js +0 -246
  454. package/dist/commands/pcheck/evaluator.js.map +0 -1
  455. package/dist/commands/pcheck/index.d.ts +0 -12
  456. package/dist/commands/pcheck/index.d.ts.map +0 -1
  457. package/dist/commands/pcheck/index.js +0 -249
  458. package/dist/commands/pcheck/index.js.map +0 -1
  459. package/dist/commands/pcheck/parser.d.ts +0 -39
  460. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  461. package/dist/commands/pcheck/parser.js +0 -155
  462. package/dist/commands/pcheck/parser.js.map +0 -1
  463. package/dist/commands/pcheck/types.d.ts +0 -82
  464. package/dist/commands/pcheck/types.d.ts.map +0 -1
  465. package/dist/commands/pcheck/types.js +0 -5
  466. package/dist/commands/pcheck/types.js.map +0 -1
  467. package/dist/commands/porch/claude.d.ts +0 -29
  468. package/dist/commands/porch/claude.d.ts.map +0 -1
  469. package/dist/commands/porch/claude.js +0 -79
  470. package/dist/commands/porch/claude.js.map +0 -1
  471. package/dist/commands/porch/consultation.d.ts +0 -56
  472. package/dist/commands/porch/consultation.d.ts.map +0 -1
  473. package/dist/commands/porch/consultation.js +0 -330
  474. package/dist/commands/porch/consultation.js.map +0 -1
  475. package/dist/commands/porch/notifications.d.ts +0 -99
  476. package/dist/commands/porch/notifications.d.ts.map +0 -1
  477. package/dist/commands/porch/notifications.js +0 -223
  478. package/dist/commands/porch/notifications.js.map +0 -1
  479. package/dist/commands/porch/plan-parser.d.ts +0 -38
  480. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  481. package/dist/commands/porch/plan-parser.js +0 -166
  482. package/dist/commands/porch/plan-parser.js.map +0 -1
  483. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  484. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  485. package/dist/commands/porch/protocol-loader.js +0 -262
  486. package/dist/commands/porch/protocol-loader.js.map +0 -1
  487. package/dist/commands/porch/repl.d.ts +0 -33
  488. package/dist/commands/porch/repl.d.ts.map +0 -1
  489. package/dist/commands/porch/repl.js +0 -206
  490. package/dist/commands/porch/repl.js.map +0 -1
  491. package/dist/commands/porch/run.d.ts +0 -15
  492. package/dist/commands/porch/run.d.ts.map +0 -1
  493. package/dist/commands/porch/run.js +0 -551
  494. package/dist/commands/porch/run.js.map +0 -1
  495. package/dist/commands/porch/signal-parser.d.ts +0 -102
  496. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  497. package/dist/commands/porch/signal-parser.js +0 -199
  498. package/dist/commands/porch/signal-parser.js.map +0 -1
  499. package/dist/commands/porch/signals.d.ts +0 -35
  500. package/dist/commands/porch/signals.d.ts.map +0 -1
  501. package/dist/commands/porch/signals.js +0 -76
  502. package/dist/commands/porch/signals.js.map +0 -1
  503. package/dist/commands/porch2/checks.d.ts +0 -29
  504. package/dist/commands/porch2/checks.d.ts.map +0 -1
  505. package/dist/commands/porch2/checks.js +0 -141
  506. package/dist/commands/porch2/checks.js.map +0 -1
  507. package/dist/commands/porch2/index.d.ts +0 -38
  508. package/dist/commands/porch2/index.d.ts.map +0 -1
  509. package/dist/commands/porch2/index.js +0 -483
  510. package/dist/commands/porch2/index.js.map +0 -1
  511. package/dist/commands/porch2/plan.d.ts +0 -70
  512. package/dist/commands/porch2/plan.d.ts.map +0 -1
  513. package/dist/commands/porch2/plan.js +0 -227
  514. package/dist/commands/porch2/plan.js.map +0 -1
  515. package/dist/commands/porch2/protocol.d.ts +0 -37
  516. package/dist/commands/porch2/protocol.d.ts.map +0 -1
  517. package/dist/commands/porch2/protocol.js +0 -183
  518. package/dist/commands/porch2/protocol.js.map +0 -1
  519. package/dist/commands/porch2/state.d.ts +0 -35
  520. package/dist/commands/porch2/state.d.ts.map +0 -1
  521. package/dist/commands/porch2/state.js +0 -124
  522. package/dist/commands/porch2/state.js.map +0 -1
  523. package/dist/commands/porch2/types.d.ts +0 -79
  524. package/dist/commands/porch2/types.d.ts.map +0 -1
  525. package/dist/commands/porch2/types.js +0 -8
  526. package/dist/commands/porch2/types.js.map +0 -1
  527. package/dist/commands/tower.d.ts +0 -16
  528. package/dist/commands/tower.d.ts.map +0 -1
  529. package/dist/commands/tower.js +0 -21
  530. package/dist/commands/tower.js.map +0 -1
  531. package/skeleton/config.json +0 -7
  532. package/skeleton/protocols/spider/prompts/defend.md +0 -215
  533. package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
  534. package/skeleton/protocols/spider/prompts/implement.md +0 -149
  535. package/skeleton/protocols/spider/templates/review.md +0 -207
  536. /package/skeleton/protocols/{spider → spir}/templates/plan.md +0 -0
  537. /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
@@ -1,37 +1,32 @@
1
1
  /**
2
- * Spawn command - creates a new builder in various modes
2
+ * Spawn command orchestrator module.
3
+ * Spec 0105: Tower Server Decomposition — Phase 7
3
4
  *
4
5
  * Modes:
5
6
  * - spec: --project/-p Spawn for a spec file (existing behavior)
6
7
  * - task: --task Spawn with an ad-hoc task description
7
8
  * - protocol: --protocol Spawn to run a protocol (cleanup, experiment, etc.)
8
9
  * - shell: --shell Bare Claude session (no prompt, no worktree)
10
+ *
11
+ * Role/prompt logic extracted to spawn-roles.ts.
12
+ * Worktree/git logic extracted to spawn-worktree.ts.
9
13
  */
10
14
  import { resolve, basename } from 'node:path';
11
- import { existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, symlinkSync } from 'node:fs';
12
- import { readdir } from 'node:fs/promises';
15
+ import { existsSync, writeFileSync } from 'node:fs';
13
16
  import { getConfig, ensureDirectories, getResolvedCommands } from '../utils/index.js';
14
17
  import { logger, fatal } from '../utils/logger.js';
15
- import { run, commandExists, findAvailablePort, spawnTtyd } from '../utils/shell.js';
16
- import { loadState, upsertBuilder } from '../state.js';
18
+ import { run } from '../utils/shell.js';
19
+ import { upsertBuilder } from '../state.js';
17
20
  import { loadRolePrompt } from '../utils/roles.js';
21
+ import { buildPromptFromTemplate, buildResumeNotice, loadProtocolRole, findSpecFile, validateProtocol, loadProtocol, resolveProtocol, resolveMode, } from './spawn-roles.js';
22
+ import { DEFAULT_TOWER_PORT, checkDependencies, createWorktree, initPorchInWorktree, checkBugfixCollisions, fetchGitHubIssue, executePreSpawnHooks, slugify, validateResumeWorktree, createPtySession, startBuilderSession, startShellSession, buildWorktreeLaunchScript, } from './spawn-worktree.js';
23
+ // =============================================================================
24
+ // ID and Session Management
25
+ // =============================================================================
18
26
  /**
19
27
  * Generate a short 4-character base64-encoded ID
20
28
  * Uses URL-safe base64 (a-z, A-Z, 0-9, -, _) for filesystem-safe IDs
21
29
  */
22
- /**
23
- * Get the project name from config (basename of projectRoot)
24
- * Used to namespace tmux sessions and prevent cross-project collisions
25
- */
26
- function getProjectName(config) {
27
- return basename(config.projectRoot);
28
- }
29
- /**
30
- * Get a namespaced tmux session name: builder-{project}-{id}
31
- */
32
- function getSessionName(config, builderId) {
33
- return `builder-${getProjectName(config)}-${builderId}`;
34
- }
35
30
  function generateShortId() {
36
31
  // Generate random 24-bit number and base64 encode to 4 chars
37
32
  const num = Math.floor(Math.random() * 0xFFFFFF);
@@ -43,22 +38,27 @@ function generateShortId() {
43
38
  .substring(0, 4);
44
39
  }
45
40
  /**
46
- * Validate spawn options - ensure exactly one mode is selected
41
+ * Validate spawn options - ensure exactly one input mode is selected
42
+ * Note: --protocol serves dual purpose:
43
+ * 1. As an input mode when used alone (e.g., `af spawn --protocol experiment`)
44
+ * 2. As a protocol override when combined with other input modes (e.g., `af spawn -p 0001 --protocol tick`)
47
45
  */
48
46
  function validateSpawnOptions(options) {
49
- const modes = [
47
+ // Count input modes (excluding --protocol which can be used as override)
48
+ const inputModes = [
50
49
  options.project,
51
50
  options.task,
52
- options.protocol,
53
51
  options.shell,
54
52
  options.worktree,
55
53
  options.issue,
56
54
  ].filter(Boolean);
57
- if (modes.length === 0) {
55
+ // --protocol alone is a valid input mode
56
+ const protocolAlone = options.protocol && inputModes.length === 0;
57
+ if (inputModes.length === 0 && !protocolAlone) {
58
58
  fatal('Must specify one of: --project (-p), --issue (-i), --task, --protocol, --shell, --worktree\n\nRun "af spawn --help" for examples.');
59
59
  }
60
- if (modes.length > 1) {
61
- fatal('Flags --project, --issue, --task, --protocol, --shell, --worktree are mutually exclusive');
60
+ if (inputModes.length > 1) {
61
+ fatal('Flags --project, --issue, --task, --shell, --worktree are mutually exclusive');
62
62
  }
63
63
  if (options.files && !options.task) {
64
64
  fatal('--files requires --task');
@@ -66,246 +66,37 @@ function validateSpawnOptions(options) {
66
66
  if ((options.noComment || options.force) && !options.issue) {
67
67
  fatal('--no-comment and --force require --issue');
68
68
  }
69
+ // --protocol as override cannot be used with --shell or --worktree
70
+ if (options.protocol && inputModes.length > 0 && (options.shell || options.worktree)) {
71
+ fatal('--protocol cannot be used with --shell or --worktree (no protocol applies)');
72
+ }
73
+ // --use-protocol is now deprecated in favor of --protocol as universal override
74
+ // Keep for backwards compatibility but prefer --protocol
75
+ if (options.useProtocol && (options.shell || options.worktree)) {
76
+ fatal('--use-protocol cannot be used with --shell or --worktree (no protocol applies)');
77
+ }
69
78
  }
70
79
  /**
71
80
  * Determine the spawn mode from options
81
+ * Note: --protocol can be used as both an input mode (alone) or an override (with other modes)
72
82
  */
73
83
  function getSpawnMode(options) {
84
+ // Primary input modes take precedence over --protocol as override
74
85
  if (options.project)
75
86
  return 'spec';
76
87
  if (options.issue)
77
88
  return 'bugfix';
78
89
  if (options.task)
79
90
  return 'task';
80
- if (options.protocol)
81
- return 'protocol';
82
91
  if (options.shell)
83
92
  return 'shell';
84
93
  if (options.worktree)
85
94
  return 'worktree';
95
+ // --protocol alone is the protocol input mode
96
+ if (options.protocol)
97
+ return 'protocol';
86
98
  throw new Error('No mode specified');
87
99
  }
88
- // loadRolePrompt imported from ../utils/roles.js
89
- /**
90
- * Load a protocol-specific role if it exists
91
- */
92
- function loadProtocolRole(config, protocolName) {
93
- const protocolRolePath = resolve(config.codevDir, 'protocols', protocolName, 'role.md');
94
- if (existsSync(protocolRolePath)) {
95
- return { content: readFileSync(protocolRolePath, 'utf-8'), source: 'protocol' };
96
- }
97
- // Fall back to builder role
98
- return loadRolePrompt(config, 'builder');
99
- }
100
- /**
101
- * Find a spec file by project ID
102
- */
103
- async function findSpecFile(codevDir, projectId) {
104
- const specsDir = resolve(codevDir, 'specs');
105
- if (!existsSync(specsDir)) {
106
- return null;
107
- }
108
- const files = await readdir(specsDir);
109
- // Try exact match first (e.g., "0001-feature.md")
110
- for (const file of files) {
111
- if (file.startsWith(projectId) && file.endsWith('.md')) {
112
- return resolve(specsDir, file);
113
- }
114
- }
115
- // Try partial match (e.g., just "0001")
116
- for (const file of files) {
117
- if (file.startsWith(projectId + '-') && file.endsWith('.md')) {
118
- return resolve(specsDir, file);
119
- }
120
- }
121
- return null;
122
- }
123
- /**
124
- * Validate that a protocol exists
125
- */
126
- function validateProtocol(config, protocolName) {
127
- const protocolDir = resolve(config.codevDir, 'protocols', protocolName);
128
- const protocolFile = resolve(protocolDir, 'protocol.md');
129
- if (!existsSync(protocolDir)) {
130
- // List available protocols
131
- const protocolsDir = resolve(config.codevDir, 'protocols');
132
- let available = '';
133
- if (existsSync(protocolsDir)) {
134
- const dirs = readdirSync(protocolsDir, { withFileTypes: true })
135
- .filter((d) => d.isDirectory())
136
- .map((d) => d.name);
137
- if (dirs.length > 0) {
138
- available = `\n\nAvailable protocols: ${dirs.join(', ')}`;
139
- }
140
- }
141
- fatal(`Protocol not found: ${protocolName}${available}`);
142
- }
143
- if (!existsSync(protocolFile)) {
144
- fatal(`Protocol ${protocolName} exists but has no protocol.md file`);
145
- }
146
- }
147
- /**
148
- * Check for required dependencies
149
- */
150
- async function checkDependencies() {
151
- if (!(await commandExists('git'))) {
152
- fatal('git not found');
153
- }
154
- if (!(await commandExists('ttyd'))) {
155
- fatal('ttyd not found. Install with: brew install ttyd');
156
- }
157
- }
158
- /**
159
- * Find an available port, avoiding ports already in use by other builders
160
- */
161
- async function findFreePort(config) {
162
- const state = loadState();
163
- const usedPorts = new Set();
164
- for (const b of state.builders || []) {
165
- if (b.port)
166
- usedPorts.add(b.port);
167
- }
168
- let port = config.builderPortRange[0];
169
- while (usedPorts.has(port)) {
170
- port++;
171
- }
172
- return findAvailablePort(port);
173
- }
174
- /**
175
- * Create git branch and worktree
176
- */
177
- async function createWorktree(config, branchName, worktreePath) {
178
- logger.info('Creating branch...');
179
- try {
180
- await run(`git branch ${branchName}`, { cwd: config.projectRoot });
181
- }
182
- catch (error) {
183
- // Branch might already exist, that's OK
184
- logger.debug(`Branch creation: ${error}`);
185
- }
186
- logger.info('Creating worktree...');
187
- try {
188
- await run(`git worktree add "${worktreePath}" ${branchName}`, { cwd: config.projectRoot });
189
- }
190
- catch (error) {
191
- fatal(`Failed to create worktree: ${error}`);
192
- }
193
- // Symlink .env from project root into worktree (if it exists)
194
- const rootEnvPath = resolve(config.projectRoot, '.env');
195
- const worktreeEnvPath = resolve(worktreePath, '.env');
196
- if (existsSync(rootEnvPath) && !existsSync(worktreeEnvPath)) {
197
- try {
198
- symlinkSync(rootEnvPath, worktreeEnvPath);
199
- logger.info('Linked .env from project root');
200
- }
201
- catch (error) {
202
- logger.debug(`Failed to symlink .env: ${error}`);
203
- }
204
- }
205
- }
206
- /**
207
- * Start tmux session and ttyd for a builder
208
- */
209
- async function startBuilderSession(config, builderId, worktreePath, baseCmd, prompt, roleContent, roleSource) {
210
- const port = await findFreePort(config);
211
- const sessionName = getSessionName(config, builderId);
212
- logger.info('Creating tmux session...');
213
- // Write initial prompt to a file for reference
214
- const promptFile = resolve(worktreePath, '.builder-prompt.txt');
215
- writeFileSync(promptFile, prompt);
216
- // Build the start script with role if provided
217
- const scriptPath = resolve(worktreePath, '.builder-start.sh');
218
- let scriptContent;
219
- if (roleContent) {
220
- // Write role to a file and use $(cat) to avoid shell escaping issues
221
- const roleFile = resolve(worktreePath, '.builder-role.md');
222
- // Inject the actual dashboard port into the role prompt
223
- const roleWithPort = roleContent.replace(/\{PORT\}/g, String(config.dashboardPort));
224
- writeFileSync(roleFile, roleWithPort);
225
- logger.info(`Loaded role (${roleSource})`);
226
- scriptContent = `#!/bin/bash
227
- cd "${worktreePath}"
228
- while true; do
229
- ${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
230
- echo ""
231
- echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
232
- sleep 2
233
- done
234
- `;
235
- }
236
- else {
237
- scriptContent = `#!/bin/bash
238
- cd "${worktreePath}"
239
- while true; do
240
- ${baseCmd} "$(cat '${promptFile}')"
241
- echo ""
242
- echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
243
- sleep 2
244
- done
245
- `;
246
- }
247
- writeFileSync(scriptPath, scriptContent);
248
- chmodSync(scriptPath, '755');
249
- // Create tmux session running the script
250
- await run(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${worktreePath}" "${scriptPath}"`);
251
- await run(`tmux set-option -t "${sessionName}" status off`);
252
- // Enable mouse scrolling in tmux
253
- await run('tmux set -g mouse on');
254
- await run('tmux set -g set-clipboard on');
255
- await run('tmux set -g allow-passthrough on');
256
- // Copy selection to clipboard when mouse is released (pbcopy for macOS)
257
- await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
258
- await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
259
- // Start ttyd connecting to the tmux session
260
- logger.info('Starting builder terminal...');
261
- const customIndexPath = resolve(config.templatesDir, 'ttyd-index.html');
262
- const hasCustomIndex = existsSync(customIndexPath);
263
- if (hasCustomIndex) {
264
- logger.info('Using custom terminal with file click support');
265
- }
266
- const ttydProcess = spawnTtyd({
267
- port,
268
- sessionName,
269
- cwd: worktreePath,
270
- customIndexPath: hasCustomIndex ? customIndexPath : undefined,
271
- });
272
- if (!ttydProcess?.pid) {
273
- fatal('Failed to start ttyd process for builder');
274
- }
275
- return { port, pid: ttydProcess.pid, sessionName };
276
- }
277
- /**
278
- * Start a shell session (no worktree, just tmux + ttyd)
279
- */
280
- async function startShellSession(config, shellId, baseCmd) {
281
- const port = await findFreePort(config);
282
- const sessionName = `shell-${shellId}`;
283
- logger.info('Creating tmux session...');
284
- // Shell mode: just launch Claude with no prompt
285
- await run(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${config.projectRoot}" "${baseCmd}"`);
286
- await run(`tmux set-option -t "${sessionName}" status off`);
287
- // Enable mouse scrolling in tmux
288
- await run('tmux set -g mouse on');
289
- await run('tmux set -g set-clipboard on');
290
- await run('tmux set -g allow-passthrough on');
291
- // Copy selection to clipboard when mouse is released (pbcopy for macOS)
292
- await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
293
- await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
294
- // Start ttyd connecting to the tmux session
295
- logger.info('Starting shell terminal...');
296
- const customIndexPath = resolve(config.templatesDir, 'ttyd-index.html');
297
- const hasCustomIndex = existsSync(customIndexPath);
298
- const ttydProcess = spawnTtyd({
299
- port,
300
- sessionName,
301
- cwd: config.projectRoot,
302
- customIndexPath: hasCustomIndex ? customIndexPath : undefined,
303
- });
304
- if (!ttydProcess?.pid) {
305
- fatal('Failed to start ttyd process for shell');
306
- }
307
- return { port, pid: ttydProcess.pid, sessionName };
308
- }
309
100
  // =============================================================================
310
101
  // Mode-specific spawn implementations
311
102
  // =============================================================================
@@ -326,56 +117,53 @@ async function spawnSpec(options, config) {
326
117
  // Check for corresponding plan file
327
118
  const planFile = resolve(config.codevDir, 'plans', `${specName}.md`);
328
119
  const hasPlan = existsSync(planFile);
329
- logger.header(`Spawning Builder ${builderId} (spec)`);
120
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (spec)`);
330
121
  logger.kv('Spec', specFile);
331
122
  logger.kv('Branch', branchName);
332
123
  logger.kv('Worktree', worktreePath);
333
124
  await ensureDirectories(config);
334
125
  await checkDependencies();
335
- await createWorktree(config, branchName, worktreePath);
336
- // Parse protocol from spec file metadata
337
- const specContent = readFileSync(specFile, 'utf-8');
338
- const protocolMatch = specContent.match(/\*\*Protocol\*\*:\s*(\w+)/i);
339
- const protocol = protocolMatch ? protocolMatch[1].toUpperCase() : 'SPIDER';
340
- const protocolPath = `codev/protocols/${protocol.toLowerCase()}/protocol.md`;
341
- // Build the prompt
126
+ if (options.resume) {
127
+ validateResumeWorktree(worktreePath);
128
+ }
129
+ else {
130
+ await createWorktree(config, branchName, worktreePath);
131
+ }
132
+ const protocol = await resolveProtocol(options, config);
133
+ const protocolDef = loadProtocol(config, protocol);
134
+ const mode = resolveMode(options, protocolDef);
135
+ logger.kv('Protocol', protocol.toUpperCase());
136
+ logger.kv('Mode', mode.toUpperCase());
137
+ // Pre-initialize porch so the builder doesn't need to figure out project ID
138
+ if (!options.resume) {
139
+ const porchProjectName = specName.replace(new RegExp(`^${projectId}-`), '');
140
+ await initPorchInWorktree(worktreePath, protocol, projectId, porchProjectName);
141
+ }
342
142
  const specRelPath = `codev/specs/${specName}.md`;
343
143
  const planRelPath = `codev/plans/${specName}.md`;
344
- let initialPrompt = `## Protocol
345
- Follow the ${protocol} protocol STRICTLY: ${protocolPath}
346
- Read and internalize the protocol before starting any work.
347
-
348
- ## Task
349
- Implement the feature specified in ${specRelPath}.`;
350
- if (hasPlan) {
351
- initialPrompt += ` Follow the implementation plan in ${planRelPath}.`;
352
- }
353
- initialPrompt += `
354
-
355
- Start by reading the protocol, spec${hasPlan ? ', and plan' : ''}, then begin implementation.`;
356
- const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
357
-
358
- ${initialPrompt}`;
359
- // Load role
144
+ const templateContext = {
145
+ protocol_name: protocol.toUpperCase(), mode,
146
+ mode_soft: mode === 'soft', mode_strict: mode === 'strict',
147
+ project_id: projectId,
148
+ input_description: `the feature specified in ${specRelPath}`,
149
+ spec: { path: specRelPath, name: specName },
150
+ };
151
+ if (hasPlan)
152
+ templateContext.plan = { path: planRelPath, name: specName };
153
+ const initialPrompt = buildPromptFromTemplate(config, protocol, templateContext);
154
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(projectId)}\n` : '';
155
+ const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${initialPrompt}`;
360
156
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
361
157
  const commands = getResolvedCommands();
362
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
363
- const builder = {
364
- id: builderId,
365
- name: specName,
366
- port,
367
- pid,
368
- status: 'spawning',
369
- phase: 'init',
370
- worktree: worktreePath,
371
- branch: branchName,
372
- tmuxSession: sessionName,
373
- type: 'spec',
374
- };
375
- upsertBuilder(builder);
158
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
159
+ upsertBuilder({
160
+ id: builderId, name: specName, status: 'implementing', phase: 'init',
161
+ worktree: worktreePath, branch: branchName, type: 'spec', terminalId,
162
+ });
376
163
  logger.blank();
377
164
  logger.success(`Builder ${builderId} spawned!`);
378
- logger.kv('Terminal', `http://localhost:${port}`);
165
+ logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
166
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
379
167
  }
380
168
  /**
381
169
  * Spawn builder for an ad-hoc task
@@ -386,7 +174,7 @@ async function spawnTask(options, config) {
386
174
  const builderId = `task-${shortId}`;
387
175
  const branchName = `builder/task-${shortId}`;
388
176
  const worktreePath = resolve(config.buildersDir, builderId);
389
- logger.header(`Spawning Builder ${builderId} (task)`);
177
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (task)`);
390
178
  logger.kv('Task', taskText.substring(0, 60) + (taskText.length > 60 ? '...' : ''));
391
179
  logger.kv('Branch', branchName);
392
180
  logger.kv('Worktree', worktreePath);
@@ -395,34 +183,46 @@ async function spawnTask(options, config) {
395
183
  }
396
184
  await ensureDirectories(config);
397
185
  await checkDependencies();
398
- await createWorktree(config, branchName, worktreePath);
399
- // Build the prompt
400
- let prompt = taskText;
186
+ if (options.resume) {
187
+ validateResumeWorktree(worktreePath);
188
+ }
189
+ else {
190
+ await createWorktree(config, branchName, worktreePath);
191
+ }
192
+ let taskDescription = taskText;
401
193
  if (options.files && options.files.length > 0) {
402
- prompt += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
194
+ taskDescription += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
195
+ }
196
+ const hasExplicitProtocol = options.protocol || options.useProtocol;
197
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
198
+ let builderPrompt;
199
+ if (hasExplicitProtocol) {
200
+ const protocol = await resolveProtocol(options, config);
201
+ const protocolDef = loadProtocol(config, protocol);
202
+ const mode = resolveMode(options, protocolDef);
203
+ const templateContext = {
204
+ protocol_name: protocol.toUpperCase(), mode,
205
+ mode_soft: mode === 'soft', mode_strict: mode === 'strict',
206
+ project_id: builderId, input_description: 'an ad-hoc task', task_text: taskDescription,
207
+ };
208
+ const prompt = buildPromptFromTemplate(config, protocol, templateContext);
209
+ builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
210
+ }
211
+ else {
212
+ builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n# Task\n\n${taskDescription}`;
403
213
  }
404
- const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition. ${prompt}`;
405
- // Load role
406
214
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
407
215
  const commands = getResolvedCommands();
408
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
409
- const builder = {
216
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
217
+ upsertBuilder({
410
218
  id: builderId,
411
219
  name: `Task: ${taskText.substring(0, 30)}${taskText.length > 30 ? '...' : ''}`,
412
- port,
413
- pid,
414
- status: 'spawning',
415
- phase: 'init',
416
- worktree: worktreePath,
417
- branch: branchName,
418
- tmuxSession: sessionName,
419
- type: 'task',
420
- taskText,
421
- };
422
- upsertBuilder(builder);
220
+ status: 'implementing', phase: 'init',
221
+ worktree: worktreePath, branch: branchName, type: 'task', taskText, terminalId,
222
+ });
423
223
  logger.blank();
424
224
  logger.success(`Builder ${builderId} spawned!`);
425
- logger.kv('Terminal', `http://localhost:${port}`);
225
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
426
226
  }
427
227
  /**
428
228
  * Spawn builder to run a protocol
@@ -434,36 +234,41 @@ async function spawnProtocol(options, config) {
434
234
  const builderId = `${protocolName}-${shortId}`;
435
235
  const branchName = `builder/${protocolName}-${shortId}`;
436
236
  const worktreePath = resolve(config.buildersDir, builderId);
437
- logger.header(`Spawning Builder ${builderId} (protocol)`);
237
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (protocol)`);
438
238
  logger.kv('Protocol', protocolName);
439
239
  logger.kv('Branch', branchName);
440
240
  logger.kv('Worktree', worktreePath);
441
241
  await ensureDirectories(config);
442
242
  await checkDependencies();
443
- await createWorktree(config, branchName, worktreePath);
444
- // Build the prompt
445
- const prompt = `You are running the ${protocolName} protocol. Start by reading codev/protocols/${protocolName}/protocol.md and follow its instructions.`;
446
- // Load protocol-specific role or fall back to builder role
243
+ if (options.resume) {
244
+ validateResumeWorktree(worktreePath);
245
+ }
246
+ else {
247
+ await createWorktree(config, branchName, worktreePath);
248
+ }
249
+ const protocolDef = loadProtocol(config, protocolName);
250
+ const mode = resolveMode(options, protocolDef);
251
+ logger.kv('Mode', mode.toUpperCase());
252
+ const templateContext = {
253
+ protocol_name: protocolName.toUpperCase(), mode,
254
+ mode_soft: mode === 'soft', mode_strict: mode === 'strict',
255
+ project_id: builderId,
256
+ input_description: `running the ${protocolName.toUpperCase()} protocol`,
257
+ };
258
+ const promptContent = buildPromptFromTemplate(config, protocolName, templateContext);
259
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
260
+ const prompt = resumeNotice ? `${resumeNotice}\n${promptContent}` : promptContent;
447
261
  const role = options.noRole ? null : loadProtocolRole(config, protocolName);
448
262
  const commands = getResolvedCommands();
449
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
450
- const builder = {
451
- id: builderId,
452
- name: `Protocol: ${protocolName}`,
453
- port,
454
- pid,
455
- status: 'spawning',
456
- phase: 'init',
457
- worktree: worktreePath,
458
- branch: branchName,
459
- tmuxSession: sessionName,
460
- type: 'protocol',
461
- protocolName,
462
- };
463
- upsertBuilder(builder);
263
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
264
+ upsertBuilder({
265
+ id: builderId, name: `Protocol: ${protocolName}`,
266
+ status: 'implementing', phase: 'init',
267
+ worktree: worktreePath, branch: branchName, type: 'protocol', protocolName, terminalId,
268
+ });
464
269
  logger.blank();
465
270
  logger.success(`Builder ${builderId} spawned!`);
466
- logger.kv('Terminal', `http://localhost:${port}`);
271
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
467
272
  }
468
273
  /**
469
274
  * Spawn a bare shell session (no worktree, no prompt)
@@ -475,25 +280,15 @@ async function spawnShell(options, config) {
475
280
  await ensureDirectories(config);
476
281
  await checkDependencies();
477
282
  const commands = getResolvedCommands();
478
- const { port, pid, sessionName } = await startShellSession(config, shortId, commands.builder);
479
- // Shell sessions are tracked as builders with type 'shell'
480
- // They don't have worktrees or branches
481
- const builder = {
482
- id: shellId,
483
- name: 'Shell session',
484
- port,
485
- pid,
486
- status: 'spawning',
487
- phase: 'interactive',
488
- worktree: '',
489
- branch: '',
490
- tmuxSession: sessionName,
491
- type: 'shell',
492
- };
493
- upsertBuilder(builder);
283
+ const { terminalId } = await startShellSession(config, shortId, commands.builder);
284
+ upsertBuilder({
285
+ id: shellId, name: 'Shell session',
286
+ status: 'implementing', phase: 'interactive',
287
+ worktree: '', branch: '', type: 'shell', terminalId,
288
+ });
494
289
  logger.blank();
495
290
  logger.success(`Shell ${shellId} spawned!`);
496
- logger.kv('Terminal', `http://localhost:${port}`);
291
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
497
292
  }
498
293
  /**
499
294
  * Spawn a worktree session (has worktree/branch, but no initial prompt)
@@ -504,167 +299,42 @@ async function spawnWorktree(options, config) {
504
299
  const builderId = `worktree-${shortId}`;
505
300
  const branchName = `builder/worktree-${shortId}`;
506
301
  const worktreePath = resolve(config.buildersDir, builderId);
507
- logger.header(`Spawning Worktree ${builderId}`);
302
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Worktree ${builderId}`);
508
303
  logger.kv('Branch', branchName);
509
304
  logger.kv('Worktree', worktreePath);
510
305
  await ensureDirectories(config);
511
306
  await checkDependencies();
512
- await createWorktree(config, branchName, worktreePath);
513
- // Load builder role
514
- const role = options.noRole ? null : loadRolePrompt(config, 'builder');
515
- const commands = getResolvedCommands();
516
- // Worktree mode: launch Claude with no prompt, but in the worktree directory
517
- const port = await findFreePort(config);
518
- const sessionName = getSessionName(config, builderId);
519
- logger.info('Creating tmux session...');
520
- // Build launch script (with role if provided) to avoid shell escaping issues
521
- const scriptPath = resolve(worktreePath, '.builder-start.sh');
522
- let scriptContent;
523
- if (role) {
524
- const roleFile = resolve(worktreePath, '.builder-role.md');
525
- // Inject the actual dashboard port into the role prompt
526
- const roleWithPort = role.content.replace(/\{PORT\}/g, String(config.dashboardPort));
527
- writeFileSync(roleFile, roleWithPort);
528
- logger.info(`Loaded role (${role.source})`);
529
- scriptContent = `#!/bin/bash
530
- cd "${worktreePath}"
531
- while true; do
532
- ${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
533
- echo ""
534
- echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
535
- sleep 2
536
- done
537
- `;
307
+ if (options.resume) {
308
+ validateResumeWorktree(worktreePath);
538
309
  }
539
310
  else {
540
- scriptContent = `#!/bin/bash
541
- cd "${worktreePath}"
542
- while true; do
543
- ${commands.builder}
544
- echo ""
545
- echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
546
- sleep 2
547
- done
548
- `;
311
+ await createWorktree(config, branchName, worktreePath);
549
312
  }
313
+ const role = options.noRole ? null : loadRolePrompt(config, 'builder');
314
+ const commands = getResolvedCommands();
315
+ logger.info('Creating terminal session...');
316
+ const scriptContent = buildWorktreeLaunchScript(worktreePath, commands.builder, role);
317
+ const scriptPath = resolve(worktreePath, '.builder-start.sh');
550
318
  writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
551
- // Create tmux session running the launch script
552
- await run(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${worktreePath}" "${scriptPath}"`);
553
- await run(`tmux set-option -t "${sessionName}" status off`);
554
- // Enable mouse scrolling in tmux
555
- await run('tmux set -g mouse on');
556
- await run('tmux set -g set-clipboard on');
557
- await run('tmux set -g allow-passthrough on');
558
- // Copy selection to clipboard when mouse is released (pbcopy for macOS)
559
- await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
560
- await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
561
- // Start ttyd connecting to the tmux session
562
- logger.info('Starting worktree terminal...');
563
- const customIndexPath = resolve(config.codevDir, 'templates', 'ttyd-index.html');
564
- const hasCustomIndex = existsSync(customIndexPath);
565
- if (hasCustomIndex) {
566
- logger.info('Using custom terminal with file click support');
567
- }
568
- const ttydProcess = spawnTtyd({
569
- port,
570
- sessionName,
571
- cwd: worktreePath,
572
- customIndexPath: hasCustomIndex ? customIndexPath : undefined,
319
+ logger.info('Creating PTY terminal session for worktree...');
320
+ const { terminalId: worktreeTerminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId });
321
+ logger.info(`Worktree terminal session created: ${worktreeTerminalId}`);
322
+ upsertBuilder({
323
+ id: builderId, name: 'Worktree session',
324
+ status: 'implementing', phase: 'interactive',
325
+ worktree: worktreePath, branch: branchName, type: 'worktree',
326
+ terminalId: worktreeTerminalId,
573
327
  });
574
- if (!ttydProcess?.pid) {
575
- fatal('Failed to start ttyd process for worktree');
576
- }
577
- const builder = {
578
- id: builderId,
579
- name: 'Worktree session',
580
- port,
581
- pid: ttydProcess.pid,
582
- status: 'spawning',
583
- phase: 'interactive',
584
- worktree: worktreePath,
585
- branch: branchName,
586
- tmuxSession: sessionName,
587
- type: 'worktree',
588
- };
589
- upsertBuilder(builder);
590
328
  logger.blank();
591
329
  logger.success(`Worktree ${builderId} spawned!`);
592
- logger.kv('Terminal', `http://localhost:${port}`);
593
- }
594
- /**
595
- * Generate a slug from an issue title (max 30 chars, lowercase, alphanumeric + hyphens)
596
- */
597
- function slugify(title) {
598
- return title
599
- .toLowerCase()
600
- .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
601
- .replace(/-+/g, '-') // Collapse multiple hyphens
602
- .replace(/^-|-$/g, '') // Trim leading/trailing hyphens
603
- .slice(0, 30); // Max 30 chars
604
- }
605
- /**
606
- * Fetch a GitHub issue via gh CLI
607
- */
608
- async function fetchGitHubIssue(issueNumber) {
609
- try {
610
- const result = await run(`gh issue view ${issueNumber} --json title,body,state,comments`);
611
- return JSON.parse(result.stdout);
612
- }
613
- catch (error) {
614
- fatal(`Failed to fetch issue #${issueNumber}. Ensure 'gh' CLI is installed and authenticated.`);
615
- throw error; // TypeScript doesn't know fatal() never returns
616
- }
617
- }
618
- /**
619
- * Check for collision conditions before spawning bugfix
620
- */
621
- async function checkBugfixCollisions(issueNumber, worktreePath, issue, force) {
622
- // 1. Check if worktree already exists
623
- if (existsSync(worktreePath)) {
624
- fatal(`Worktree already exists at ${worktreePath}\nRun: af cleanup --issue ${issueNumber}`);
625
- }
626
- // 2. Check for recent "On it" comments (< 24h old)
627
- const onItComments = issue.comments.filter((c) => c.body.toLowerCase().includes('on it'));
628
- if (onItComments.length > 0) {
629
- const lastComment = onItComments[onItComments.length - 1];
630
- const age = Date.now() - new Date(lastComment.createdAt).getTime();
631
- const hoursAgo = Math.round(age / (1000 * 60 * 60));
632
- if (hoursAgo < 24) {
633
- if (!force) {
634
- fatal(`Issue #${issueNumber} has "On it" comment from ${hoursAgo}h ago (by @${lastComment.author.login}).\nSomeone may already be working on this. Use --force to override.`);
635
- }
636
- logger.warn(`Warning: "On it" comment from ${hoursAgo}h ago - proceeding with --force`);
637
- }
638
- else {
639
- logger.warn(`Warning: Stale "On it" comment (${hoursAgo}h ago). Proceeding.`);
640
- }
641
- }
642
- // 3. Check for open PRs referencing this issue
643
- try {
644
- const prResult = await run(`gh pr list --search "in:body #${issueNumber}" --json number,title --limit 5`);
645
- const openPRs = JSON.parse(prResult.stdout);
646
- if (openPRs.length > 0) {
647
- if (!force) {
648
- const prList = openPRs.map((pr) => ` - PR #${pr.number}: ${pr.title}`).join('\n');
649
- fatal(`Found ${openPRs.length} open PR(s) referencing issue #${issueNumber}:\n${prList}\nUse --force to proceed anyway.`);
650
- }
651
- logger.warn(`Warning: Found ${openPRs.length} open PR(s) referencing issue - proceeding with --force`);
652
- }
653
- }
654
- catch {
655
- // Non-fatal: continue if PR check fails
656
- }
657
- // 4. Warn if issue is already closed
658
- if (issue.state === 'CLOSED') {
659
- logger.warn(`Warning: Issue #${issueNumber} is already closed`);
660
- }
330
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${worktreeTerminalId}`);
661
331
  }
662
332
  /**
663
333
  * Spawn builder for a GitHub issue (bugfix mode)
664
334
  */
665
335
  async function spawnBugfix(options, config) {
666
336
  const issueNumber = options.issue;
667
- logger.header(`Spawning Bugfix Builder for Issue #${issueNumber}`);
337
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Bugfix Builder for Issue #${issueNumber}`);
668
338
  // Fetch issue from GitHub
669
339
  logger.info('Fetching issue from GitHub...');
670
340
  const issue = await fetchGitHubIssue(issueNumber);
@@ -672,70 +342,72 @@ async function spawnBugfix(options, config) {
672
342
  const builderId = `bugfix-${issueNumber}`;
673
343
  const branchName = `builder/bugfix-${issueNumber}-${slug}`;
674
344
  const worktreePath = resolve(config.buildersDir, builderId);
345
+ const protocol = await resolveProtocol(options, config);
346
+ const protocolDef = loadProtocol(config, protocol);
347
+ const mode = resolveMode(options, protocolDef);
675
348
  logger.kv('Title', issue.title);
676
349
  logger.kv('Branch', branchName);
677
350
  logger.kv('Worktree', worktreePath);
678
- // Check for collisions
679
- await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
680
- await ensureDirectories(config);
681
- await checkDependencies();
682
- await createWorktree(config, branchName, worktreePath);
683
- // Comment on the issue (unless --no-comment)
684
- if (!options.noComment) {
685
- logger.info('Commenting on issue...');
686
- try {
687
- await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
351
+ logger.kv('Protocol', protocol.toUpperCase());
352
+ logger.kv('Mode', mode.toUpperCase());
353
+ // Execute pre-spawn hooks (skip in resume mode)
354
+ if (!options.resume) {
355
+ if (protocolDef?.hooks?.['pre-spawn']) {
356
+ await executePreSpawnHooks(protocolDef, {
357
+ issueNumber,
358
+ issue,
359
+ worktreePath,
360
+ force: options.force,
361
+ noComment: options.noComment,
362
+ });
688
363
  }
689
- catch {
690
- logger.warn('Warning: Failed to comment on issue (continuing anyway)');
364
+ else {
365
+ // Fallback: hardcoded behavior for backwards compatibility
366
+ await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
367
+ if (!options.noComment) {
368
+ logger.info('Commenting on issue...');
369
+ try {
370
+ await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
371
+ }
372
+ catch {
373
+ logger.warn('Warning: Failed to comment on issue (continuing anyway)');
374
+ }
375
+ }
691
376
  }
692
377
  }
693
- // Build the prompt with issue context
694
- const prompt = `You are a Builder working on a BUGFIX task.
695
-
696
- ## Protocol
697
- Follow the BUGFIX protocol: codev/protocols/bugfix/protocol.md
698
-
699
- ## Issue #${issueNumber}
700
- **Title**: ${issue.title}
701
-
702
- **Description**:
703
- ${issue.body || '(No description provided)'}
704
-
705
- ## Your Mission
706
- 1. Reproduce the bug
707
- 2. Identify root cause
708
- 3. Implement fix (< 300 LOC)
709
- 4. Add regression test
710
- 5. Run CMAP review (3-way parallel: Gemini, Codex, Claude)
711
- 6. Create PR with "Fixes #${issueNumber}" in body
712
-
713
- If the fix is too complex (> 300 LOC or architectural changes), notify the Architect via:
714
- af send architect "Issue #${issueNumber} is more complex than expected. [Reason]. Recommend escalating to SPIDER/TICK."
715
-
716
- Start by reading the issue and reproducing the bug.`;
717
- const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n\n${prompt}`;
718
- // Load role
378
+ await ensureDirectories(config);
379
+ await checkDependencies();
380
+ if (options.resume) {
381
+ validateResumeWorktree(worktreePath);
382
+ }
383
+ else {
384
+ await createWorktree(config, branchName, worktreePath);
385
+ // Pre-initialize porch so the builder doesn't need to figure out project ID
386
+ await initPorchInWorktree(worktreePath, protocol, builderId, slug);
387
+ }
388
+ const templateContext = {
389
+ protocol_name: protocol.toUpperCase(), mode,
390
+ mode_soft: mode === 'soft', mode_strict: mode === 'strict',
391
+ project_id: builderId,
392
+ input_description: `a fix for GitHub Issue #${issueNumber}`,
393
+ issue: { number: issueNumber, title: issue.title, body: issue.body || '(No description provided)' },
394
+ };
395
+ const prompt = buildPromptFromTemplate(config, protocol, templateContext);
396
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
397
+ const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
719
398
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
720
399
  const commands = getResolvedCommands();
721
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
722
- const builder = {
400
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
401
+ upsertBuilder({
723
402
  id: builderId,
724
403
  name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`,
725
- port,
726
- pid,
727
- status: 'spawning',
728
- phase: 'init',
729
- worktree: worktreePath,
730
- branch: branchName,
731
- tmuxSession: sessionName,
732
- type: 'bugfix',
733
- issueNumber,
734
- };
735
- upsertBuilder(builder);
404
+ status: 'implementing', phase: 'init',
405
+ worktree: worktreePath, branch: branchName, type: 'bugfix', issueNumber, terminalId,
406
+ });
736
407
  logger.blank();
737
408
  logger.success(`Bugfix builder for issue #${issueNumber} spawned!`);
738
- logger.kv('Terminal', `http://localhost:${port}`);
409
+ logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
410
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
739
411
  }
740
412
  // =============================================================================
741
413
  // Main entry point
@@ -746,6 +418,25 @@ Start by reading the issue and reproducing the bug.`;
746
418
  export async function spawn(options) {
747
419
  validateSpawnOptions(options);
748
420
  const config = getConfig();
421
+ // Refuse to spawn if the main worktree has uncommitted changes.
422
+ // Builders work in git worktrees branched from HEAD — uncommitted changes
423
+ // (specs, plans, codev updates) won't be visible to the builder.
424
+ // Skip this check in resume mode — the worktree already exists with its own branch.
425
+ if (!options.force && !options.resume) {
426
+ try {
427
+ const { stdout } = await run('git status --porcelain', { cwd: config.projectRoot });
428
+ if (stdout.trim().length > 0) {
429
+ fatal('Uncommitted changes detected in main worktree.\n\n' +
430
+ ' Builders branch from HEAD, so uncommitted files (specs, plans,\n' +
431
+ ' codev updates) will NOT be visible to the builder.\n\n' +
432
+ ' Please commit or stash your changes first, then retry.\n' +
433
+ ' Use --force to skip this check.');
434
+ }
435
+ }
436
+ catch {
437
+ // Non-fatal — if git status fails, allow spawn to continue
438
+ }
439
+ }
749
440
  // Prune stale worktrees before spawning to prevent "can't find session" errors
750
441
  // This catches orphaned worktrees from crashes, manual kills, or incomplete cleanups
751
442
  try {
@@ -755,25 +446,14 @@ export async function spawn(options) {
755
446
  // Non-fatal - continue with spawn even if prune fails
756
447
  }
757
448
  const mode = getSpawnMode(options);
758
- switch (mode) {
759
- case 'spec':
760
- await spawnSpec(options, config);
761
- break;
762
- case 'bugfix':
763
- await spawnBugfix(options, config);
764
- break;
765
- case 'task':
766
- await spawnTask(options, config);
767
- break;
768
- case 'protocol':
769
- await spawnProtocol(options, config);
770
- break;
771
- case 'shell':
772
- await spawnShell(options, config);
773
- break;
774
- case 'worktree':
775
- await spawnWorktree(options, config);
776
- break;
777
- }
449
+ const handlers = {
450
+ spec: () => spawnSpec(options, config),
451
+ bugfix: () => spawnBugfix(options, config),
452
+ task: () => spawnTask(options, config),
453
+ protocol: () => spawnProtocol(options, config),
454
+ shell: () => spawnShell(options, config),
455
+ worktree: () => spawnWorktree(options, config),
456
+ };
457
+ await handlers[mode]();
778
458
  }
779
459
  //# sourceMappingURL=spawn.js.map