@cluesmith/codev 2.0.0-rc.8 → 2.0.0

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