@cluesmith/codev 2.0.0-rc.7 → 2.0.0-rc.71

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 (456) 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-C7FtNK6Y.css +32 -0
  5. package/dashboard/dist/assets/index-CDAINZKT.js +131 -0
  6. package/dashboard/dist/assets/index-CDAINZKT.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 +173 -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 +60 -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.d.ts.map +1 -1
  43. package/dist/agent-farm/commands/spawn.js +597 -281
  44. package/dist/agent-farm/commands/spawn.js.map +1 -1
  45. package/dist/agent-farm/commands/start.d.ts +10 -20
  46. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  47. package/dist/agent-farm/commands/start.js +45 -491
  48. package/dist/agent-farm/commands/start.js.map +1 -1
  49. package/dist/agent-farm/commands/status.d.ts +2 -0
  50. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  51. package/dist/agent-farm/commands/status.js +75 -24
  52. package/dist/agent-farm/commands/status.js.map +1 -1
  53. package/dist/agent-farm/commands/stop.d.ts +6 -0
  54. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  55. package/dist/agent-farm/commands/stop.js +49 -109
  56. package/dist/agent-farm/commands/stop.js.map +1 -1
  57. package/dist/agent-farm/commands/tower-cloud.d.ts +48 -0
  58. package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
  59. package/dist/agent-farm/commands/tower-cloud.js +329 -0
  60. package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
  61. package/dist/agent-farm/commands/tower.d.ts +9 -0
  62. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  63. package/dist/agent-farm/commands/tower.js +59 -19
  64. package/dist/agent-farm/commands/tower.js.map +1 -1
  65. package/dist/agent-farm/db/index.d.ts +6 -2
  66. package/dist/agent-farm/db/index.d.ts.map +1 -1
  67. package/dist/agent-farm/db/index.js +246 -18
  68. package/dist/agent-farm/db/index.js.map +1 -1
  69. package/dist/agent-farm/db/migrate.d.ts +0 -4
  70. package/dist/agent-farm/db/migrate.d.ts.map +1 -1
  71. package/dist/agent-farm/db/migrate.js +6 -55
  72. package/dist/agent-farm/db/migrate.js.map +1 -1
  73. package/dist/agent-farm/db/schema.d.ts +3 -3
  74. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  75. package/dist/agent-farm/db/schema.js +25 -19
  76. package/dist/agent-farm/db/schema.js.map +1 -1
  77. package/dist/agent-farm/db/types.d.ts +3 -13
  78. package/dist/agent-farm/db/types.d.ts.map +1 -1
  79. package/dist/agent-farm/db/types.js +3 -11
  80. package/dist/agent-farm/db/types.js.map +1 -1
  81. package/dist/agent-farm/hq-connector.d.ts +2 -6
  82. package/dist/agent-farm/hq-connector.d.ts.map +1 -1
  83. package/dist/agent-farm/hq-connector.js +2 -17
  84. package/dist/agent-farm/hq-connector.js.map +1 -1
  85. package/dist/agent-farm/lib/cloud-config.d.ts +59 -0
  86. package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
  87. package/dist/agent-farm/lib/cloud-config.js +143 -0
  88. package/dist/agent-farm/lib/cloud-config.js.map +1 -0
  89. package/dist/agent-farm/lib/tower-client.d.ts +163 -0
  90. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
  91. package/dist/agent-farm/lib/tower-client.js +233 -0
  92. package/dist/agent-farm/lib/tower-client.js.map +1 -0
  93. package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
  94. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
  95. package/dist/agent-farm/lib/tunnel-client.js +504 -0
  96. package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
  97. package/dist/agent-farm/servers/tower-server.js +2653 -185
  98. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  99. package/dist/agent-farm/state.d.ts +6 -12
  100. package/dist/agent-farm/state.d.ts.map +1 -1
  101. package/dist/agent-farm/state.js +34 -49
  102. package/dist/agent-farm/state.js.map +1 -1
  103. package/dist/agent-farm/types.d.ts +49 -26
  104. package/dist/agent-farm/types.d.ts.map +1 -1
  105. package/dist/agent-farm/utils/config.d.ts +0 -5
  106. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  107. package/dist/agent-farm/utils/config.js +12 -44
  108. package/dist/agent-farm/utils/config.js.map +1 -1
  109. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  110. package/dist/agent-farm/utils/deps.js +0 -32
  111. package/dist/agent-farm/utils/deps.js.map +1 -1
  112. package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
  113. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
  114. package/dist/agent-farm/utils/file-tabs.js +46 -0
  115. package/dist/agent-farm/utils/file-tabs.js.map +1 -0
  116. package/dist/agent-farm/utils/gate-status.d.ts +16 -0
  117. package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
  118. package/dist/agent-farm/utils/gate-status.js +79 -0
  119. package/dist/agent-farm/utils/gate-status.js.map +1 -0
  120. package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
  121. package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
  122. package/dist/agent-farm/utils/gate-watcher.js +122 -0
  123. package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
  124. package/dist/agent-farm/utils/index.d.ts +0 -1
  125. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  126. package/dist/agent-farm/utils/index.js +0 -1
  127. package/dist/agent-farm/utils/index.js.map +1 -1
  128. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  129. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  130. package/dist/agent-farm/utils/notifications.js +121 -0
  131. package/dist/agent-farm/utils/notifications.js.map +1 -0
  132. package/dist/agent-farm/utils/server-utils.d.ts +5 -5
  133. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  134. package/dist/agent-farm/utils/server-utils.js +5 -16
  135. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  136. package/dist/agent-farm/utils/session.d.ts +32 -0
  137. package/dist/agent-farm/utils/session.d.ts.map +1 -0
  138. package/dist/agent-farm/utils/session.js +57 -0
  139. package/dist/agent-farm/utils/session.js.map +1 -0
  140. package/dist/agent-farm/utils/shell.d.ts +9 -22
  141. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  142. package/dist/agent-farm/utils/shell.js +34 -34
  143. package/dist/agent-farm/utils/shell.js.map +1 -1
  144. package/dist/cli.d.ts.map +1 -1
  145. package/dist/cli.js +11 -54
  146. package/dist/cli.js.map +1 -1
  147. package/dist/commands/adopt.d.ts.map +1 -1
  148. package/dist/commands/adopt.js +49 -4
  149. package/dist/commands/adopt.js.map +1 -1
  150. package/dist/commands/consult/index.d.ts +13 -2
  151. package/dist/commands/consult/index.d.ts.map +1 -1
  152. package/dist/commands/consult/index.js +245 -29
  153. package/dist/commands/consult/index.js.map +1 -1
  154. package/dist/commands/doctor.d.ts.map +1 -1
  155. package/dist/commands/doctor.js +96 -79
  156. package/dist/commands/doctor.js.map +1 -1
  157. package/dist/commands/init.d.ts.map +1 -1
  158. package/dist/commands/init.js +41 -2
  159. package/dist/commands/init.js.map +1 -1
  160. package/dist/commands/porch/build-counter.d.ts +5 -0
  161. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  162. package/dist/commands/porch/build-counter.js +5 -0
  163. package/dist/commands/porch/build-counter.js.map +1 -0
  164. package/dist/commands/porch/checks.d.ts +17 -29
  165. package/dist/commands/porch/checks.d.ts.map +1 -1
  166. package/dist/commands/porch/checks.js +96 -144
  167. package/dist/commands/porch/checks.js.map +1 -1
  168. package/dist/commands/porch/index.d.ts +25 -43
  169. package/dist/commands/porch/index.d.ts.map +1 -1
  170. package/dist/commands/porch/index.js +463 -1116
  171. package/dist/commands/porch/index.js.map +1 -1
  172. package/dist/commands/porch/next.d.ts +22 -0
  173. package/dist/commands/porch/next.d.ts.map +1 -0
  174. package/dist/commands/porch/next.js +571 -0
  175. package/dist/commands/porch/next.js.map +1 -0
  176. package/dist/commands/porch/plan.d.ts +70 -0
  177. package/dist/commands/porch/plan.d.ts.map +1 -0
  178. package/dist/commands/porch/plan.js +190 -0
  179. package/dist/commands/porch/plan.js.map +1 -0
  180. package/dist/commands/porch/prompts.d.ts +19 -0
  181. package/dist/commands/porch/prompts.d.ts.map +1 -0
  182. package/dist/commands/porch/prompts.js +277 -0
  183. package/dist/commands/porch/prompts.js.map +1 -0
  184. package/dist/commands/porch/protocol.d.ts +59 -0
  185. package/dist/commands/porch/protocol.d.ts.map +1 -0
  186. package/dist/commands/porch/protocol.js +294 -0
  187. package/dist/commands/porch/protocol.js.map +1 -0
  188. package/dist/commands/porch/state.d.ts +36 -107
  189. package/dist/commands/porch/state.d.ts.map +1 -1
  190. package/dist/commands/porch/state.js +120 -699
  191. package/dist/commands/porch/state.js.map +1 -1
  192. package/dist/commands/porch/types.d.ts +99 -164
  193. package/dist/commands/porch/types.d.ts.map +1 -1
  194. package/dist/commands/porch/types.js +2 -1
  195. package/dist/commands/porch/types.js.map +1 -1
  196. package/dist/commands/porch/verdict.d.ts +31 -0
  197. package/dist/commands/porch/verdict.d.ts.map +1 -0
  198. package/dist/commands/porch/verdict.js +59 -0
  199. package/dist/commands/porch/verdict.js.map +1 -0
  200. package/dist/commands/update.d.ts.map +1 -1
  201. package/dist/commands/update.js +31 -0
  202. package/dist/commands/update.js.map +1 -1
  203. package/dist/lib/scaffold.d.ts +37 -0
  204. package/dist/lib/scaffold.d.ts.map +1 -1
  205. package/dist/lib/scaffold.js +114 -0
  206. package/dist/lib/scaffold.js.map +1 -1
  207. package/dist/terminal/index.d.ts +8 -0
  208. package/dist/terminal/index.d.ts.map +1 -0
  209. package/dist/terminal/index.js +5 -0
  210. package/dist/terminal/index.js.map +1 -0
  211. package/dist/terminal/pty-manager.d.ts +69 -0
  212. package/dist/terminal/pty-manager.d.ts.map +1 -0
  213. package/dist/terminal/pty-manager.js +377 -0
  214. package/dist/terminal/pty-manager.js.map +1 -0
  215. package/dist/terminal/pty-session.d.ts +104 -0
  216. package/dist/terminal/pty-session.d.ts.map +1 -0
  217. package/dist/terminal/pty-session.js +327 -0
  218. package/dist/terminal/pty-session.js.map +1 -0
  219. package/dist/terminal/ring-buffer.d.ts +34 -0
  220. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  221. package/dist/terminal/ring-buffer.js +94 -0
  222. package/dist/terminal/ring-buffer.js.map +1 -0
  223. package/dist/terminal/session-manager.d.ts +115 -0
  224. package/dist/terminal/session-manager.d.ts.map +1 -0
  225. package/dist/terminal/session-manager.js +582 -0
  226. package/dist/terminal/session-manager.js.map +1 -0
  227. package/dist/terminal/shepherd-client.d.ts +66 -0
  228. package/dist/terminal/shepherd-client.d.ts.map +1 -0
  229. package/dist/terminal/shepherd-client.js +234 -0
  230. package/dist/terminal/shepherd-client.js.map +1 -0
  231. package/dist/terminal/shepherd-main.d.ts +19 -0
  232. package/dist/terminal/shepherd-main.d.ts.map +1 -0
  233. package/dist/terminal/shepherd-main.js +153 -0
  234. package/dist/terminal/shepherd-main.js.map +1 -0
  235. package/dist/terminal/shepherd-process.d.ts +75 -0
  236. package/dist/terminal/shepherd-process.d.ts.map +1 -0
  237. package/dist/terminal/shepherd-process.js +279 -0
  238. package/dist/terminal/shepherd-process.js.map +1 -0
  239. package/dist/terminal/shepherd-protocol.d.ts +115 -0
  240. package/dist/terminal/shepherd-protocol.d.ts.map +1 -0
  241. package/dist/terminal/shepherd-protocol.js +214 -0
  242. package/dist/terminal/shepherd-protocol.js.map +1 -0
  243. package/dist/terminal/shepherd-replay-buffer.d.ts +38 -0
  244. package/dist/terminal/shepherd-replay-buffer.d.ts.map +1 -0
  245. package/dist/terminal/shepherd-replay-buffer.js +94 -0
  246. package/dist/terminal/shepherd-replay-buffer.js.map +1 -0
  247. package/dist/terminal/ws-protocol.d.ts +27 -0
  248. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  249. package/dist/terminal/ws-protocol.js +44 -0
  250. package/dist/terminal/ws-protocol.js.map +1 -0
  251. package/package.json +19 -5
  252. package/skeleton/.claude/skills/af/SKILL.md +89 -0
  253. package/skeleton/.claude/skills/codev/SKILL.md +41 -0
  254. package/skeleton/.claude/skills/consult/SKILL.md +81 -0
  255. package/skeleton/.claude/skills/generate-image/SKILL.md +56 -0
  256. package/skeleton/DEPENDENCIES.md +4 -62
  257. package/skeleton/builders.md +1 -1
  258. package/skeleton/consult-types/impl-review.md +18 -9
  259. package/skeleton/consult-types/integration-review.md +1 -1
  260. package/skeleton/consult-types/plan-review.md +1 -1
  261. package/skeleton/consult-types/pr-ready.md +1 -1
  262. package/skeleton/consult-types/spec-review.md +1 -1
  263. package/skeleton/porch/prompts/defend.md +1 -1
  264. package/skeleton/porch/prompts/evaluate.md +2 -2
  265. package/skeleton/porch/prompts/implement.md +1 -1
  266. package/skeleton/porch/prompts/plan.md +1 -1
  267. package/skeleton/porch/prompts/review.md +4 -4
  268. package/skeleton/porch/prompts/specify.md +1 -1
  269. package/skeleton/porch/prompts/understand.md +2 -2
  270. package/skeleton/protocol-schema.json +282 -0
  271. package/skeleton/protocols/bugfix/builder-prompt.md +54 -0
  272. package/skeleton/protocols/bugfix/prompts/fix.md +77 -0
  273. package/skeleton/protocols/bugfix/prompts/investigate.md +77 -0
  274. package/skeleton/protocols/bugfix/prompts/pr.md +84 -0
  275. package/skeleton/protocols/bugfix/protocol.json +20 -33
  276. package/skeleton/protocols/experiment/builder-prompt.md +52 -0
  277. package/skeleton/protocols/experiment/protocol.json +101 -0
  278. package/skeleton/protocols/experiment/protocol.md +3 -3
  279. package/skeleton/protocols/experiment/templates/notes.md +1 -1
  280. package/skeleton/protocols/maintain/builder-prompt.md +46 -0
  281. package/skeleton/protocols/maintain/prompts/audit.md +111 -0
  282. package/skeleton/protocols/maintain/prompts/clean.md +91 -0
  283. package/skeleton/protocols/maintain/prompts/sync.md +113 -0
  284. package/skeleton/protocols/maintain/prompts/verify.md +110 -0
  285. package/skeleton/protocols/maintain/protocol.json +141 -0
  286. package/skeleton/protocols/maintain/protocol.md +17 -11
  287. package/skeleton/protocols/protocol-schema.json +54 -1
  288. package/skeleton/protocols/spir/builder-prompt.md +59 -0
  289. package/skeleton/protocols/spir/prompts/implement.md +208 -0
  290. package/skeleton/protocols/{spider → spir}/prompts/plan.md +6 -70
  291. package/skeleton/protocols/{spider → spir}/prompts/review.md +20 -39
  292. package/skeleton/protocols/{spider → spir}/prompts/specify.md +33 -61
  293. package/skeleton/protocols/spir/protocol.json +156 -0
  294. package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
  295. package/skeleton/protocols/{spider → spir}/templates/plan.md +14 -0
  296. package/skeleton/protocols/spir/templates/review.md +89 -0
  297. package/skeleton/protocols/tick/builder-prompt.md +56 -0
  298. package/skeleton/protocols/tick/protocol.json +7 -2
  299. package/skeleton/protocols/tick/protocol.md +18 -18
  300. package/skeleton/protocols/tick/templates/review.md +1 -1
  301. package/skeleton/resources/commands/agent-farm.md +63 -46
  302. package/skeleton/resources/commands/codev.md +0 -2
  303. package/skeleton/resources/commands/overview.md +7 -17
  304. package/skeleton/resources/workflow-reference.md +4 -4
  305. package/skeleton/roles/architect.md +152 -315
  306. package/skeleton/roles/builder.md +110 -218
  307. package/skeleton/roles/consultant.md +6 -6
  308. package/skeleton/templates/AGENTS.md +2 -2
  309. package/skeleton/templates/CLAUDE.md +2 -2
  310. package/skeleton/templates/cheatsheet.md +7 -5
  311. package/skeleton/templates/projectlist.md +1 -1
  312. package/templates/dashboard/index.html +17 -43
  313. package/templates/dashboard/js/dialogs.js +7 -7
  314. package/templates/dashboard/js/files.js +2 -2
  315. package/templates/dashboard/js/main.js +4 -4
  316. package/templates/dashboard/js/projects.js +3 -3
  317. package/templates/dashboard/js/tabs.js +1 -1
  318. package/templates/dashboard/js/utils.js +22 -87
  319. package/templates/open.html +26 -0
  320. package/templates/tower.html +642 -36
  321. package/dist/agent-farm/commands/kickoff.d.ts +0 -20
  322. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  323. package/dist/agent-farm/commands/kickoff.js +0 -337
  324. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  325. package/dist/agent-farm/commands/rename.d.ts +0 -13
  326. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  327. package/dist/agent-farm/commands/rename.js +0 -33
  328. package/dist/agent-farm/commands/rename.js.map +0 -1
  329. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  330. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  331. package/dist/agent-farm/commands/tutorial.js +0 -49
  332. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  333. package/dist/agent-farm/commands/util.d.ts +0 -15
  334. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  335. package/dist/agent-farm/commands/util.js +0 -108
  336. package/dist/agent-farm/commands/util.js.map +0 -1
  337. package/dist/agent-farm/servers/dashboard-server.d.ts +0 -7
  338. package/dist/agent-farm/servers/dashboard-server.d.ts.map +0 -1
  339. package/dist/agent-farm/servers/dashboard-server.js +0 -1872
  340. package/dist/agent-farm/servers/dashboard-server.js.map +0 -1
  341. package/dist/agent-farm/servers/open-server.d.ts +0 -7
  342. package/dist/agent-farm/servers/open-server.d.ts.map +0 -1
  343. package/dist/agent-farm/servers/open-server.js +0 -315
  344. package/dist/agent-farm/servers/open-server.js.map +0 -1
  345. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  346. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  347. package/dist/agent-farm/tutorial/index.js +0 -8
  348. package/dist/agent-farm/tutorial/index.js.map +0 -1
  349. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  350. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  351. package/dist/agent-farm/tutorial/prompts.js +0 -147
  352. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  353. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  354. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  355. package/dist/agent-farm/tutorial/runner.js +0 -204
  356. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  357. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  358. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  359. package/dist/agent-farm/tutorial/state.js +0 -89
  360. package/dist/agent-farm/tutorial/state.js.map +0 -1
  361. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  362. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  363. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  364. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  365. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  366. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  367. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  368. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  369. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  370. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  371. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  372. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  373. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  374. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  375. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  376. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  377. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  378. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  379. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  380. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  381. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  382. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  383. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  384. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  385. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  386. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  387. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  388. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  389. package/dist/agent-farm/utils/orphan-handler.d.ts +0 -27
  390. package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
  391. package/dist/agent-farm/utils/orphan-handler.js +0 -149
  392. package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
  393. package/dist/agent-farm/utils/port-registry.d.ts +0 -58
  394. package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
  395. package/dist/agent-farm/utils/port-registry.js +0 -166
  396. package/dist/agent-farm/utils/port-registry.js.map +0 -1
  397. package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
  398. package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
  399. package/dist/agent-farm/utils/terminal-ports.js +0 -35
  400. package/dist/agent-farm/utils/terminal-ports.js.map +0 -1
  401. package/dist/commands/pcheck/cache.d.ts +0 -48
  402. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  403. package/dist/commands/pcheck/cache.js +0 -170
  404. package/dist/commands/pcheck/cache.js.map +0 -1
  405. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  406. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  407. package/dist/commands/pcheck/evaluator.js +0 -246
  408. package/dist/commands/pcheck/evaluator.js.map +0 -1
  409. package/dist/commands/pcheck/index.d.ts +0 -12
  410. package/dist/commands/pcheck/index.d.ts.map +0 -1
  411. package/dist/commands/pcheck/index.js +0 -249
  412. package/dist/commands/pcheck/index.js.map +0 -1
  413. package/dist/commands/pcheck/parser.d.ts +0 -39
  414. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  415. package/dist/commands/pcheck/parser.js +0 -155
  416. package/dist/commands/pcheck/parser.js.map +0 -1
  417. package/dist/commands/pcheck/types.d.ts +0 -82
  418. package/dist/commands/pcheck/types.d.ts.map +0 -1
  419. package/dist/commands/pcheck/types.js +0 -5
  420. package/dist/commands/pcheck/types.js.map +0 -1
  421. package/dist/commands/porch/consultation.d.ts +0 -56
  422. package/dist/commands/porch/consultation.d.ts.map +0 -1
  423. package/dist/commands/porch/consultation.js +0 -330
  424. package/dist/commands/porch/consultation.js.map +0 -1
  425. package/dist/commands/porch/notifications.d.ts +0 -99
  426. package/dist/commands/porch/notifications.d.ts.map +0 -1
  427. package/dist/commands/porch/notifications.js +0 -223
  428. package/dist/commands/porch/notifications.js.map +0 -1
  429. package/dist/commands/porch/plan-parser.d.ts +0 -38
  430. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  431. package/dist/commands/porch/plan-parser.js +0 -166
  432. package/dist/commands/porch/plan-parser.js.map +0 -1
  433. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  434. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  435. package/dist/commands/porch/protocol-loader.js +0 -253
  436. package/dist/commands/porch/protocol-loader.js.map +0 -1
  437. package/dist/commands/porch/signal-parser.d.ts +0 -88
  438. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  439. package/dist/commands/porch/signal-parser.js +0 -148
  440. package/dist/commands/porch/signal-parser.js.map +0 -1
  441. package/dist/commands/tower.d.ts +0 -16
  442. package/dist/commands/tower.d.ts.map +0 -1
  443. package/dist/commands/tower.js +0 -21
  444. package/dist/commands/tower.js.map +0 -1
  445. package/skeleton/config.json +0 -7
  446. package/skeleton/porch/protocols/bugfix.json +0 -85
  447. package/skeleton/porch/protocols/spider.json +0 -135
  448. package/skeleton/porch/protocols/tick.json +0 -76
  449. package/skeleton/protocols/spider/prompts/defend.md +0 -215
  450. package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
  451. package/skeleton/protocols/spider/prompts/implement.md +0 -149
  452. package/skeleton/protocols/spider/protocol.json +0 -210
  453. package/skeleton/protocols/spider/templates/review.md +0 -207
  454. package/templates/dashboard/css/activity.css +0 -151
  455. package/templates/dashboard/js/activity.js +0 -112
  456. /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
@@ -7,33 +7,142 @@
7
7
  * - protocol: --protocol Spawn to run a protocol (cleanup, experiment, etc.)
8
8
  * - shell: --shell Bare Claude session (no prompt, no worktree)
9
9
  */
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';
10
+ import { resolve, basename } from 'node:path';
11
+ import { existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, symlinkSync } from 'node:fs';
14
12
  import { readdir } from 'node:fs/promises';
15
13
  import { getConfig, ensureDirectories, getResolvedCommands } from '../utils/index.js';
16
14
  import { logger, fatal } from '../utils/logger.js';
17
- import { run, commandExists, findAvailablePort, spawnTtyd } from '../utils/shell.js';
18
- import { loadState, upsertBuilder } from '../state.js';
15
+ import { run, commandExists } from '../utils/shell.js';
16
+ import { upsertBuilder } from '../state.js';
19
17
  import { loadRolePrompt } from '../utils/roles.js';
18
+ // Tower port — the single HTTP server since Spec 0090
19
+ const DEFAULT_TOWER_PORT = 4100;
20
20
  /**
21
- * Generate a short 4-character base64-encoded ID
22
- * Uses URL-safe base64 (a-z, A-Z, 0-9, -, _) for filesystem-safe IDs
21
+ * Simple Handlebars-like template renderer
22
+ * Supports: {{variable}}, {{#if condition}}...{{/if}}, {{object.property}}
23
+ */
24
+ function renderTemplate(template, context) {
25
+ let result = template;
26
+ // Process {{#if condition}}...{{/if}} blocks
27
+ // eslint-disable-next-line no-constant-condition
28
+ while (true) {
29
+ const ifMatch = result.match(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/);
30
+ if (!ifMatch)
31
+ break;
32
+ const [fullMatch, condition, content] = ifMatch;
33
+ const value = getNestedValue(context, condition);
34
+ result = result.replace(fullMatch, value ? content : '');
35
+ }
36
+ // Process {{variable}} and {{object.property}} substitutions
37
+ result = result.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
38
+ const value = getNestedValue(context, path);
39
+ if (value === undefined || value === null)
40
+ return '';
41
+ return String(value);
42
+ });
43
+ // Clean up any double newlines left from removed sections
44
+ result = result.replace(/\n{3,}/g, '\n\n');
45
+ return result.trim();
46
+ }
47
+ /**
48
+ * Get nested value from object using dot notation
23
49
  */
50
+ function getNestedValue(obj, path) {
51
+ const parts = path.split('.');
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ let current = obj;
54
+ for (const part of parts) {
55
+ if (current === null || current === undefined)
56
+ return undefined;
57
+ current = current[part];
58
+ }
59
+ return current;
60
+ }
24
61
  /**
25
- * Get the project name from config (basename of projectRoot)
26
- * Used to namespace tmux sessions and prevent cross-project collisions
62
+ * Load builder-prompt.md template for a protocol
27
63
  */
28
- function getProjectName(config) {
29
- return basename(config.projectRoot);
64
+ function loadBuilderPromptTemplate(config, protocolName) {
65
+ const templatePath = resolve(config.codevDir, 'protocols', protocolName, 'builder-prompt.md');
66
+ if (existsSync(templatePath)) {
67
+ return readFileSync(templatePath, 'utf-8');
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Build the prompt using protocol template or fallback to inline prompt
73
+ */
74
+ function buildPromptFromTemplate(config, protocolName, context) {
75
+ const template = loadBuilderPromptTemplate(config, protocolName);
76
+ if (template) {
77
+ logger.info(`Using template: protocols/${protocolName}/builder-prompt.md`);
78
+ return renderTemplate(template, context);
79
+ }
80
+ // Fallback: no template found, return a basic prompt
81
+ logger.debug(`No template found for ${protocolName}, using inline prompt`);
82
+ return buildFallbackPrompt(protocolName, context);
83
+ }
84
+ /**
85
+ * Build a fallback prompt when no template exists
86
+ */
87
+ function buildFallbackPrompt(protocolName, context) {
88
+ const modeInstructions = context.mode === 'strict'
89
+ ? `## Mode: STRICT
90
+ Porch orchestrates your work. Run: \`porch next\` to get your next tasks.`
91
+ : `## Mode: SOFT
92
+ You follow the protocol yourself. The architect monitors your work and verifies compliance.`;
93
+ let prompt = `# ${protocolName.toUpperCase()} Builder (${context.mode} mode)
94
+
95
+ You are implementing ${context.input_description}.
96
+
97
+ ${modeInstructions}
98
+
99
+ ## Protocol
100
+ Follow the ${protocolName.toUpperCase()} protocol: \`codev/protocols/${protocolName}/protocol.md\`
101
+ Read and internalize the protocol before starting any work.
102
+ `;
103
+ if (context.spec) {
104
+ prompt += `\n## Spec\nRead the specification at: \`${context.spec.path}\`\n`;
105
+ }
106
+ if (context.plan) {
107
+ prompt += `\n## Plan\nFollow the implementation plan at: \`${context.plan.path}\`\n`;
108
+ }
109
+ if (context.issue) {
110
+ prompt += `\n## Issue #${context.issue.number}
111
+ **Title**: ${context.issue.title}
112
+
113
+ **Description**:
114
+ ${context.issue.body || '(No description provided)'}
115
+ `;
116
+ }
117
+ if (context.task_text) {
118
+ prompt += `\n## Task\n${context.task_text}\n`;
119
+ }
120
+ return prompt;
30
121
  }
122
+ // =============================================================================
123
+ // Resume Context
124
+ // =============================================================================
31
125
  /**
32
- * Get a namespaced tmux session name: builder-{project}-{id}
126
+ * Build a resume notice to prepend to the builder prompt.
127
+ * Tells the builder this is a resumed session and to check existing porch state.
33
128
  */
34
- function getSessionName(config, builderId) {
35
- return `builder-${getProjectName(config)}-${builderId}`;
129
+ function buildResumeNotice(_projectId) {
130
+ return `## RESUME SESSION
131
+
132
+ This is a **resumed** builder session. A previous session was working in this worktree.
133
+
134
+ Start by running \`porch next\` to check your current state and get next tasks.
135
+ If porch state exists, continue from where the previous session left off.
136
+ If porch reports "not found", run \`porch init\` to re-initialize.
137
+ `;
36
138
  }
139
+ // =============================================================================
140
+ // ID and Session Management
141
+ // =============================================================================
142
+ /**
143
+ * Generate a short 4-character base64-encoded ID
144
+ * Uses URL-safe base64 (a-z, A-Z, 0-9, -, _) for filesystem-safe IDs
145
+ */
37
146
  function generateShortId() {
38
147
  // Generate random 24-bit number and base64 encode to 4 chars
39
148
  const num = Math.floor(Math.random() * 0xFFFFFF);
@@ -45,64 +154,27 @@ function generateShortId() {
45
154
  .substring(0, 4);
46
155
  }
47
156
  /**
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
157
+ * Validate spawn options - ensure exactly one input mode is selected
158
+ * Note: --protocol serves dual purpose:
159
+ * 1. As an input mode when used alone (e.g., `af spawn --protocol experiment`)
160
+ * 2. As a protocol override when combined with other input modes (e.g., `af spawn -p 0001 --protocol tick`)
91
161
  */
92
162
  function validateSpawnOptions(options) {
93
- const modes = [
163
+ // Count input modes (excluding --protocol which can be used as override)
164
+ const inputModes = [
94
165
  options.project,
95
166
  options.task,
96
- options.protocol,
97
167
  options.shell,
98
168
  options.worktree,
99
169
  options.issue,
100
170
  ].filter(Boolean);
101
- if (modes.length === 0) {
171
+ // --protocol alone is a valid input mode
172
+ const protocolAlone = options.protocol && inputModes.length === 0;
173
+ if (inputModes.length === 0 && !protocolAlone) {
102
174
  fatal('Must specify one of: --project (-p), --issue (-i), --task, --protocol, --shell, --worktree\n\nRun "af spawn --help" for examples.');
103
175
  }
104
- if (modes.length > 1) {
105
- fatal('Flags --project, --issue, --task, --protocol, --shell, --worktree are mutually exclusive');
176
+ if (inputModes.length > 1) {
177
+ fatal('Flags --project, --issue, --task, --shell, --worktree are mutually exclusive');
106
178
  }
107
179
  if (options.files && !options.task) {
108
180
  fatal('--files requires --task');
@@ -110,23 +182,35 @@ function validateSpawnOptions(options) {
110
182
  if ((options.noComment || options.force) && !options.issue) {
111
183
  fatal('--no-comment and --force require --issue');
112
184
  }
185
+ // --protocol as override cannot be used with --shell or --worktree
186
+ if (options.protocol && inputModes.length > 0 && (options.shell || options.worktree)) {
187
+ fatal('--protocol cannot be used with --shell or --worktree (no protocol applies)');
188
+ }
189
+ // --use-protocol is now deprecated in favor of --protocol as universal override
190
+ // Keep for backwards compatibility but prefer --protocol
191
+ if (options.useProtocol && (options.shell || options.worktree)) {
192
+ fatal('--use-protocol cannot be used with --shell or --worktree (no protocol applies)');
193
+ }
113
194
  }
114
195
  /**
115
196
  * Determine the spawn mode from options
197
+ * Note: --protocol can be used as both an input mode (alone) or an override (with other modes)
116
198
  */
117
199
  function getSpawnMode(options) {
200
+ // Primary input modes take precedence over --protocol as override
118
201
  if (options.project)
119
202
  return 'spec';
120
203
  if (options.issue)
121
204
  return 'bugfix';
122
205
  if (options.task)
123
206
  return 'task';
124
- if (options.protocol)
125
- return 'protocol';
126
207
  if (options.shell)
127
208
  return 'shell';
128
209
  if (options.worktree)
129
210
  return 'worktree';
211
+ // --protocol alone is the protocol input mode
212
+ if (options.protocol)
213
+ return 'protocol';
130
214
  throw new Error('No mode specified');
131
215
  }
132
216
  // loadRolePrompt imported from ../utils/roles.js
@@ -189,31 +273,142 @@ function validateProtocol(config, protocolName) {
189
273
  }
190
274
  }
191
275
  /**
192
- * Check for required dependencies
276
+ * Load and parse a protocol.json file
193
277
  */
194
- async function checkDependencies() {
195
- if (!(await commandExists('git'))) {
196
- fatal('git not found');
278
+ function loadProtocol(config, protocolName) {
279
+ const protocolJsonPath = resolve(config.codevDir, 'protocols', protocolName, 'protocol.json');
280
+ if (!existsSync(protocolJsonPath)) {
281
+ return null;
282
+ }
283
+ try {
284
+ const content = readFileSync(protocolJsonPath, 'utf-8');
285
+ return JSON.parse(content);
286
+ }
287
+ catch {
288
+ logger.warn(`Warning: Failed to parse ${protocolJsonPath}`);
289
+ return null;
290
+ }
291
+ }
292
+ /**
293
+ * Resolve which protocol to use based on precedence:
294
+ * 1. Explicit --protocol flag when used as override (with other input modes)
295
+ * 2. Explicit --use-protocol flag (backwards compatibility)
296
+ * 3. Spec file **Protocol**: header (for --project mode)
297
+ * 4. Hardcoded defaults (spir for specs, bugfix for issues)
298
+ */
299
+ async function resolveProtocol(options, config) {
300
+ // Count input modes to determine if --protocol is being used as override
301
+ const inputModes = [
302
+ options.project,
303
+ options.task,
304
+ options.shell,
305
+ options.worktree,
306
+ options.issue,
307
+ ].filter(Boolean);
308
+ const protocolAsOverride = options.protocol && inputModes.length > 0;
309
+ // 1. --protocol as override always wins when combined with other input modes
310
+ if (protocolAsOverride) {
311
+ validateProtocol(config, options.protocol);
312
+ return options.protocol.toLowerCase();
313
+ }
314
+ // 2. Explicit --use-protocol override (backwards compatibility)
315
+ if (options.useProtocol) {
316
+ validateProtocol(config, options.useProtocol);
317
+ return options.useProtocol.toLowerCase();
318
+ }
319
+ // 3. For spec mode, check spec file header (preserves existing behavior)
320
+ if (options.project) {
321
+ const specFile = await findSpecFile(config.codevDir, options.project);
322
+ if (specFile) {
323
+ const specContent = readFileSync(specFile, 'utf-8');
324
+ const match = specContent.match(/\*\*Protocol\*\*:\s*(\w+)/i);
325
+ if (match) {
326
+ const protocolFromSpec = match[1].toLowerCase();
327
+ // Validate the protocol exists
328
+ try {
329
+ validateProtocol(config, protocolFromSpec);
330
+ return protocolFromSpec;
331
+ }
332
+ catch {
333
+ // If protocol from spec doesn't exist, fall through to defaults
334
+ logger.warn(`Warning: Protocol "${match[1]}" from spec not found, using default`);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ // 4. Hardcoded defaults based on input type
340
+ if (options.project)
341
+ return 'spir';
342
+ if (options.issue)
343
+ return 'bugfix';
344
+ // --protocol alone (not as override) uses the protocol name itself
345
+ if (options.protocol)
346
+ return options.protocol.toLowerCase();
347
+ if (options.task)
348
+ return 'spir';
349
+ return 'spir'; // Final fallback
350
+ }
351
+ // Note: GitHubIssue interface is defined later in the file
352
+ /**
353
+ * Resolve the builder mode (strict vs soft)
354
+ * Precedence:
355
+ * 1. Explicit --strict or --soft flags (always win)
356
+ * 2. Protocol defaults from protocol.json
357
+ * 3. Input type defaults (spec = strict, all others = soft)
358
+ */
359
+ function resolveMode(options, protocol) {
360
+ // 1. Explicit flags always win
361
+ if (options.strict && options.soft) {
362
+ fatal('--strict and --soft are mutually exclusive');
363
+ }
364
+ if (options.strict) {
365
+ return 'strict';
366
+ }
367
+ if (options.soft) {
368
+ return 'soft';
197
369
  }
198
- if (!(await commandExists('ttyd'))) {
199
- fatal('ttyd not found. Install with: brew install ttyd');
370
+ // 2. Protocol defaults from protocol.json
371
+ if (protocol?.defaults?.mode) {
372
+ return protocol.defaults.mode;
200
373
  }
374
+ // 3. Input type defaults: only spec mode defaults to strict
375
+ if (options.project) {
376
+ return 'strict';
377
+ }
378
+ // All other modes default to soft
379
+ return 'soft';
201
380
  }
202
381
  /**
203
- * Find an available port, avoiding ports already in use by other builders
382
+ * Execute pre-spawn hooks defined in protocol.json
383
+ * Hooks are data-driven but reuse existing implementation logic
204
384
  */
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);
385
+ async function executePreSpawnHooks(protocol, context) {
386
+ if (!protocol?.hooks?.['pre-spawn'])
387
+ return;
388
+ const hooks = protocol.hooks['pre-spawn'];
389
+ // collision-check: reuses existing checkBugfixCollisions() logic
390
+ if (hooks['collision-check'] && context.issueNumber && context.issue && context.worktreePath) {
391
+ await checkBugfixCollisions(context.issueNumber, context.worktreePath, context.issue, !!context.force);
211
392
  }
212
- let port = config.builderPortRange[0];
213
- while (usedPorts.has(port)) {
214
- port++;
393
+ // comment-on-issue: posts comment to GitHub issue
394
+ if (hooks['comment-on-issue'] && context.issueNumber && !context.noComment) {
395
+ const message = hooks['comment-on-issue'];
396
+ logger.info('Commenting on issue...');
397
+ try {
398
+ await run(`gh issue comment ${context.issueNumber} --body "${message}"`);
399
+ }
400
+ catch {
401
+ logger.warn('Warning: Failed to comment on issue (continuing anyway)');
402
+ }
403
+ }
404
+ }
405
+ /**
406
+ * Check for required dependencies
407
+ */
408
+ async function checkDependencies() {
409
+ if (!(await commandExists('git'))) {
410
+ fatal('git not found');
215
411
  }
216
- return findAvailablePort(port);
217
412
  }
218
413
  /**
219
414
  * Create git branch and worktree
@@ -248,12 +443,33 @@ async function createWorktree(config, branchName, worktreePath) {
248
443
  }
249
444
  }
250
445
  /**
251
- * Start tmux session and ttyd for a builder
446
+ * Create a terminal session via the Tower REST API.
447
+ * The Tower server must be running (port 4100).
448
+ */
449
+ async function createPtySession(config, command, args, cwd, registration) {
450
+ const body = { command, args, cwd, cols: 200, rows: 50, persistent: true };
451
+ if (registration) {
452
+ body.projectPath = registration.projectPath;
453
+ body.type = registration.type;
454
+ body.roleId = registration.roleId;
455
+ }
456
+ const response = await fetch(`http://localhost:${DEFAULT_TOWER_PORT}/api/terminals`, {
457
+ method: 'POST',
458
+ headers: { 'Content-Type': 'application/json' },
459
+ body: JSON.stringify(body),
460
+ });
461
+ if (!response.ok) {
462
+ const text = await response.text();
463
+ throw new Error(`Failed to create PTY session: ${response.status} ${text}`);
464
+ }
465
+ const result = await response.json();
466
+ return { terminalId: result.id };
467
+ }
468
+ /**
469
+ * Start a terminal session for a builder
252
470
  */
253
471
  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...');
472
+ logger.info('Creating terminal session...');
257
473
  // Write initial prompt to a file for reference
258
474
  const promptFile = resolve(worktreePath, '.builder-prompt.txt');
259
475
  writeFileSync(promptFile, prompt);
@@ -264,83 +480,66 @@ async function startBuilderSession(config, builderId, worktreePath, baseCmd, pro
264
480
  // Write role to a file and use $(cat) to avoid shell escaping issues
265
481
  const roleFile = resolve(worktreePath, '.builder-role.md');
266
482
  // Inject the actual dashboard port into the role prompt
267
- const roleWithPort = roleContent.replace(/\{PORT\}/g, String(config.dashboardPort));
483
+ const roleWithPort = roleContent.replace(/\{PORT\}/g, String(DEFAULT_TOWER_PORT));
268
484
  writeFileSync(roleFile, roleWithPort);
269
485
  logger.info(`Loaded role (${roleSource})`);
270
486
  scriptContent = `#!/bin/bash
271
- exec ${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
487
+ cd "${worktreePath}"
488
+ while true; do
489
+ ${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
490
+ echo ""
491
+ echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
492
+ sleep 2
493
+ done
272
494
  `;
273
495
  }
274
496
  else {
275
497
  scriptContent = `#!/bin/bash
276
- exec ${baseCmd} "$(cat '${promptFile}')"
498
+ cd "${worktreePath}"
499
+ while true; do
500
+ ${baseCmd} "$(cat '${promptFile}')"
501
+ echo ""
502
+ echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
503
+ sleep 2
504
+ done
277
505
  `;
278
506
  }
279
507
  writeFileSync(scriptPath, scriptContent);
280
508
  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 };
509
+ // Create PTY session via Tower REST API (shepherd for persistence)
510
+ logger.info('Creating PTY terminal session...');
511
+ const { terminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId });
512
+ logger.info(`Terminal session created: ${terminalId}`);
513
+ return { terminalId };
310
514
  }
311
515
  /**
312
- * Start a shell session (no worktree, just tmux + ttyd)
516
+ * Start a shell session (no worktree, just node-pty)
313
517
  */
314
518
  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');
519
+ // Create PTY session via REST API
520
+ logger.info('Creating PTY terminal session for shell...');
521
+ const { terminalId } = await createPtySession(config, '/bin/bash', ['-c', baseCmd], config.projectRoot, { projectPath: config.projectRoot, type: 'shell', roleId: shellId });
522
+ logger.info(`Shell terminal session created: ${terminalId}`);
523
+ return { terminalId };
524
+ }
525
+ /**
526
+ * Pre-initialize porch in a worktree so the builder doesn't need to self-correct.
527
+ * Non-fatal: logs a warning on failure since the builder can still init manually.
528
+ */
529
+ async function initPorchInWorktree(worktreePath, protocol, projectId, projectName) {
530
+ logger.info('Initializing porch...');
531
+ try {
532
+ // Sanitize inputs to prevent shell injection (defense-in-depth;
533
+ // callers already use slugified names, but be safe)
534
+ const safeName = projectName.replace(/[^a-z0-9_-]/gi, '-');
535
+ const safeProto = protocol.replace(/[^a-z0-9_-]/gi, '');
536
+ const safeId = projectId.replace(/[^a-z0-9_-]/gi, '');
537
+ await run(`porch init ${safeProto} ${safeId} "${safeName}"`, { cwd: worktreePath });
538
+ logger.info(`Porch initialized: ${projectId}`);
539
+ }
540
+ catch (error) {
541
+ logger.warn(`Warning: Failed to initialize porch (builder can init manually): ${error}`);
340
542
  }
341
- // Rename Claude session for better history tracking
342
- renameClaudeSession(sessionName, `Shell ${shellId}`);
343
- return { port, pid: ttydProcess.pid, sessionName };
344
543
  }
345
544
  // =============================================================================
346
545
  // Mode-specific spawn implementations
@@ -362,56 +561,77 @@ async function spawnSpec(options, config) {
362
561
  // Check for corresponding plan file
363
562
  const planFile = resolve(config.codevDir, 'plans', `${specName}.md`);
364
563
  const hasPlan = existsSync(planFile);
365
- logger.header(`Spawning Builder ${builderId} (spec)`);
564
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (spec)`);
366
565
  logger.kv('Spec', specFile);
367
566
  logger.kv('Branch', branchName);
368
567
  logger.kv('Worktree', worktreePath);
369
568
  await ensureDirectories(config);
370
569
  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
570
+ if (options.resume) {
571
+ if (!existsSync(worktreePath)) {
572
+ fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
573
+ }
574
+ if (!existsSync(resolve(worktreePath, '.git'))) {
575
+ fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
576
+ }
577
+ logger.info('Resuming existing worktree (skipping creation)');
578
+ }
579
+ else {
580
+ await createWorktree(config, branchName, worktreePath);
581
+ }
582
+ // Resolve protocol using precedence: --use-protocol > spec header > default
583
+ const protocol = await resolveProtocol(options, config);
584
+ const protocolPath = `codev/protocols/${protocol}/protocol.md`;
585
+ // Load protocol definition for potential hooks/config
586
+ const protocolDef = loadProtocol(config, protocol);
587
+ // Resolve mode: --soft flag > protocol defaults > input type defaults
588
+ const mode = resolveMode(options, protocolDef);
589
+ logger.kv('Protocol', protocol.toUpperCase());
590
+ logger.kv('Mode', mode.toUpperCase());
591
+ // Pre-initialize porch so the builder doesn't need to figure out project ID
592
+ if (!options.resume) {
593
+ const porchProjectName = specName.replace(new RegExp(`^${projectId}-`), '');
594
+ await initPorchInWorktree(worktreePath, protocol, projectId, porchProjectName);
595
+ }
596
+ // Build the prompt using template
378
597
  const specRelPath = `codev/specs/${specName}.md`;
379
598
  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}.`;
599
+ const templateContext = {
600
+ protocol_name: protocol.toUpperCase(),
601
+ mode,
602
+ mode_soft: mode === 'soft',
603
+ mode_strict: mode === 'strict',
604
+ project_id: projectId,
605
+ input_description: `the feature specified in ${specRelPath}`,
606
+ spec: { path: specRelPath, name: specName },
607
+ };
386
608
  if (hasPlan) {
387
- initialPrompt += ` Follow the implementation plan in ${planRelPath}.`;
609
+ templateContext.plan = { path: planRelPath, name: specName };
388
610
  }
389
- initialPrompt += `
390
-
391
- Start by reading the protocol, spec${hasPlan ? ', and plan' : ''}, then begin implementation.`;
611
+ const initialPrompt = buildPromptFromTemplate(config, protocol, templateContext);
612
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(projectId)}\n` : '';
392
613
  const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
393
-
614
+ ${resumeNotice}
394
615
  ${initialPrompt}`;
395
616
  // Load role
396
617
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
397
618
  const commands = getResolvedCommands();
398
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
619
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
399
620
  const builder = {
400
621
  id: builderId,
401
622
  name: specName,
402
- port,
403
- pid,
404
- status: 'spawning',
623
+ status: 'implementing',
405
624
  phase: 'init',
406
625
  worktree: worktreePath,
407
626
  branch: branchName,
408
- tmuxSession: sessionName,
409
627
  type: 'spec',
628
+ terminalId,
410
629
  };
411
630
  upsertBuilder(builder);
412
631
  logger.blank();
413
632
  logger.success(`Builder ${builderId} spawned!`);
414
- logger.kv('Terminal', `http://localhost:${port}`);
633
+ logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
634
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
415
635
  }
416
636
  /**
417
637
  * Spawn builder for an ad-hoc task
@@ -422,7 +642,7 @@ async function spawnTask(options, config) {
422
642
  const builderId = `task-${shortId}`;
423
643
  const branchName = `builder/task-${shortId}`;
424
644
  const worktreePath = resolve(config.buildersDir, builderId);
425
- logger.header(`Spawning Builder ${builderId} (task)`);
645
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (task)`);
426
646
  logger.kv('Task', taskText.substring(0, 60) + (taskText.length > 60 ? '...' : ''));
427
647
  logger.kv('Branch', branchName);
428
648
  logger.kv('Worktree', worktreePath);
@@ -431,34 +651,68 @@ async function spawnTask(options, config) {
431
651
  }
432
652
  await ensureDirectories(config);
433
653
  await checkDependencies();
434
- await createWorktree(config, branchName, worktreePath);
435
- // Build the prompt
436
- let prompt = taskText;
654
+ if (options.resume) {
655
+ if (!existsSync(worktreePath)) {
656
+ fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
657
+ }
658
+ if (!existsSync(resolve(worktreePath, '.git'))) {
659
+ fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
660
+ }
661
+ logger.info('Resuming existing worktree (skipping creation)');
662
+ }
663
+ else {
664
+ await createWorktree(config, branchName, worktreePath);
665
+ }
666
+ // Build the prompt — only include protocol if explicitly requested
667
+ let taskDescription = taskText;
437
668
  if (options.files && options.files.length > 0) {
438
- prompt += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
669
+ taskDescription += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
670
+ }
671
+ const hasExplicitProtocol = options.protocol || options.useProtocol;
672
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
673
+ let builderPrompt;
674
+ if (hasExplicitProtocol) {
675
+ const protocol = await resolveProtocol(options, config);
676
+ const protocolDef = loadProtocol(config, protocol);
677
+ const mode = resolveMode(options, protocolDef);
678
+ const templateContext = {
679
+ protocol_name: protocol.toUpperCase(),
680
+ mode,
681
+ mode_soft: mode === 'soft',
682
+ mode_strict: mode === 'strict',
683
+ project_id: builderId,
684
+ input_description: 'an ad-hoc task',
685
+ task_text: taskDescription,
686
+ };
687
+ const prompt = buildPromptFromTemplate(config, protocol, templateContext);
688
+ builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
689
+ }
690
+ else {
691
+ builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
692
+ ${resumeNotice}
693
+ # Task
694
+
695
+ ${taskDescription}`;
439
696
  }
440
- const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition. ${prompt}`;
441
697
  // Load role
442
698
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
443
699
  const commands = getResolvedCommands();
444
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
700
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
445
701
  const builder = {
446
702
  id: builderId,
447
703
  name: `Task: ${taskText.substring(0, 30)}${taskText.length > 30 ? '...' : ''}`,
448
- port,
449
- pid,
450
- status: 'spawning',
704
+ status: 'implementing',
451
705
  phase: 'init',
452
706
  worktree: worktreePath,
453
707
  branch: branchName,
454
- tmuxSession: sessionName,
455
708
  type: 'task',
456
709
  taskText,
710
+ terminalId,
457
711
  };
458
712
  upsertBuilder(builder);
459
713
  logger.blank();
460
714
  logger.success(`Builder ${builderId} spawned!`);
461
- logger.kv('Terminal', `http://localhost:${port}`);
715
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
462
716
  }
463
717
  /**
464
718
  * Spawn builder to run a protocol
@@ -470,36 +724,59 @@ async function spawnProtocol(options, config) {
470
724
  const builderId = `${protocolName}-${shortId}`;
471
725
  const branchName = `builder/${protocolName}-${shortId}`;
472
726
  const worktreePath = resolve(config.buildersDir, builderId);
473
- logger.header(`Spawning Builder ${builderId} (protocol)`);
727
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (protocol)`);
474
728
  logger.kv('Protocol', protocolName);
475
729
  logger.kv('Branch', branchName);
476
730
  logger.kv('Worktree', worktreePath);
477
731
  await ensureDirectories(config);
478
732
  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.`;
733
+ if (options.resume) {
734
+ if (!existsSync(worktreePath)) {
735
+ fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
736
+ }
737
+ if (!existsSync(resolve(worktreePath, '.git'))) {
738
+ fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
739
+ }
740
+ logger.info('Resuming existing worktree (skipping creation)');
741
+ }
742
+ else {
743
+ await createWorktree(config, branchName, worktreePath);
744
+ }
745
+ // Load protocol definition and resolve mode
746
+ const protocolDef = loadProtocol(config, protocolName);
747
+ const mode = resolveMode(options, protocolDef);
748
+ logger.kv('Mode', mode.toUpperCase());
749
+ // Build the prompt using template
750
+ const templateContext = {
751
+ protocol_name: protocolName.toUpperCase(),
752
+ mode,
753
+ mode_soft: mode === 'soft',
754
+ mode_strict: mode === 'strict',
755
+ project_id: builderId,
756
+ input_description: `running the ${protocolName.toUpperCase()} protocol`,
757
+ };
758
+ const promptContent = buildPromptFromTemplate(config, protocolName, templateContext);
759
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
760
+ const prompt = resumeNotice ? `${resumeNotice}\n${promptContent}` : promptContent;
482
761
  // Load protocol-specific role or fall back to builder role
483
762
  const role = options.noRole ? null : loadProtocolRole(config, protocolName);
484
763
  const commands = getResolvedCommands();
485
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
764
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
486
765
  const builder = {
487
766
  id: builderId,
488
767
  name: `Protocol: ${protocolName}`,
489
- port,
490
- pid,
491
- status: 'spawning',
768
+ status: 'implementing',
492
769
  phase: 'init',
493
770
  worktree: worktreePath,
494
771
  branch: branchName,
495
- tmuxSession: sessionName,
496
772
  type: 'protocol',
497
773
  protocolName,
774
+ terminalId,
498
775
  };
499
776
  upsertBuilder(builder);
500
777
  logger.blank();
501
778
  logger.success(`Builder ${builderId} spawned!`);
502
- logger.kv('Terminal', `http://localhost:${port}`);
779
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
503
780
  }
504
781
  /**
505
782
  * Spawn a bare shell session (no worktree, no prompt)
@@ -511,25 +788,23 @@ async function spawnShell(options, config) {
511
788
  await ensureDirectories(config);
512
789
  await checkDependencies();
513
790
  const commands = getResolvedCommands();
514
- const { port, pid, sessionName } = await startShellSession(config, shortId, commands.builder);
791
+ const { terminalId } = await startShellSession(config, shortId, commands.builder);
515
792
  // Shell sessions are tracked as builders with type 'shell'
516
793
  // They don't have worktrees or branches
517
794
  const builder = {
518
795
  id: shellId,
519
796
  name: 'Shell session',
520
- port,
521
- pid,
522
- status: 'spawning',
797
+ status: 'implementing',
523
798
  phase: 'interactive',
524
799
  worktree: '',
525
800
  branch: '',
526
- tmuxSession: sessionName,
527
801
  type: 'shell',
802
+ terminalId,
528
803
  };
529
804
  upsertBuilder(builder);
530
805
  logger.blank();
531
806
  logger.success(`Shell ${shellId} spawned!`);
532
- logger.kv('Terminal', `http://localhost:${port}`);
807
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
533
808
  }
534
809
  /**
535
810
  * Spawn a worktree session (has worktree/branch, but no initial prompt)
@@ -540,80 +815,77 @@ async function spawnWorktree(options, config) {
540
815
  const builderId = `worktree-${shortId}`;
541
816
  const branchName = `builder/worktree-${shortId}`;
542
817
  const worktreePath = resolve(config.buildersDir, builderId);
543
- logger.header(`Spawning Worktree ${builderId}`);
818
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Worktree ${builderId}`);
544
819
  logger.kv('Branch', branchName);
545
820
  logger.kv('Worktree', worktreePath);
546
821
  await ensureDirectories(config);
547
822
  await checkDependencies();
548
- await createWorktree(config, branchName, worktreePath);
823
+ if (options.resume) {
824
+ if (!existsSync(worktreePath)) {
825
+ fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
826
+ }
827
+ if (!existsSync(resolve(worktreePath, '.git'))) {
828
+ fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
829
+ }
830
+ logger.info('Resuming existing worktree (skipping creation)');
831
+ }
832
+ else {
833
+ await createWorktree(config, branchName, worktreePath);
834
+ }
549
835
  // Load builder role
550
836
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
551
837
  const commands = getResolvedCommands();
552
838
  // 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...');
839
+ logger.info('Creating terminal session...');
556
840
  // Build launch script (with role if provided) to avoid shell escaping issues
557
841
  const scriptPath = resolve(worktreePath, '.builder-start.sh');
558
842
  let scriptContent;
559
843
  if (role) {
560
844
  const roleFile = resolve(worktreePath, '.builder-role.md');
561
845
  // Inject the actual dashboard port into the role prompt
562
- const roleWithPort = role.content.replace(/\{PORT\}/g, String(config.dashboardPort));
846
+ const roleWithPort = role.content.replace(/\{PORT\}/g, String(DEFAULT_TOWER_PORT));
563
847
  writeFileSync(roleFile, roleWithPort);
564
848
  logger.info(`Loaded role (${role.source})`);
565
849
  scriptContent = `#!/bin/bash
566
- exec ${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
850
+ cd "${worktreePath}"
851
+ while true; do
852
+ ${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
853
+ echo ""
854
+ echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
855
+ sleep 2
856
+ done
567
857
  `;
568
858
  }
569
859
  else {
570
860
  scriptContent = `#!/bin/bash
571
- exec ${commands.builder}
861
+ cd "${worktreePath}"
862
+ while true; do
863
+ ${commands.builder}
864
+ echo ""
865
+ echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
866
+ sleep 2
867
+ done
572
868
  `;
573
869
  }
574
870
  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,
597
- });
598
- if (!ttydProcess?.pid) {
599
- fatal('Failed to start ttyd process for worktree');
600
- }
871
+ // Create PTY session via REST API
872
+ logger.info('Creating PTY terminal session for worktree...');
873
+ const { terminalId: worktreeTerminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId });
874
+ logger.info(`Worktree terminal session created: ${worktreeTerminalId}`);
601
875
  const builder = {
602
876
  id: builderId,
603
877
  name: 'Worktree session',
604
- port,
605
- pid: ttydProcess.pid,
606
- status: 'spawning',
878
+ status: 'implementing',
607
879
  phase: 'interactive',
608
880
  worktree: worktreePath,
609
881
  branch: branchName,
610
- tmuxSession: sessionName,
611
882
  type: 'worktree',
883
+ terminalId: worktreeTerminalId,
612
884
  };
613
885
  upsertBuilder(builder);
614
886
  logger.blank();
615
887
  logger.success(`Worktree ${builderId} spawned!`);
616
- logger.kv('Terminal', `http://localhost:${port}`);
888
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${worktreeTerminalId}`);
617
889
  }
618
890
  /**
619
891
  * Generate a slug from an issue title (max 30 chars, lowercase, alphanumeric + hyphens)
@@ -688,7 +960,7 @@ async function checkBugfixCollisions(issueNumber, worktreePath, issue, force) {
688
960
  */
689
961
  async function spawnBugfix(options, config) {
690
962
  const issueNumber = options.issue;
691
- logger.header(`Spawning Bugfix Builder for Issue #${issueNumber}`);
963
+ logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Bugfix Builder for Issue #${issueNumber}`);
692
964
  // Fetch issue from GitHub
693
965
  logger.info('Fetching issue from GitHub...');
694
966
  const issue = await fetchGitHubIssue(issueNumber);
@@ -696,70 +968,95 @@ async function spawnBugfix(options, config) {
696
968
  const builderId = `bugfix-${issueNumber}`;
697
969
  const branchName = `builder/bugfix-${issueNumber}-${slug}`;
698
970
  const worktreePath = resolve(config.buildersDir, builderId);
971
+ // Resolve protocol (allows --use-protocol override)
972
+ const protocol = await resolveProtocol(options, config);
973
+ const protocolDef = loadProtocol(config, protocol);
974
+ // Resolve mode: --soft flag > protocol defaults > input type defaults (bugfix defaults to soft)
975
+ const mode = resolveMode(options, protocolDef);
699
976
  logger.kv('Title', issue.title);
700
977
  logger.kv('Branch', branchName);
701
978
  logger.kv('Worktree', worktreePath);
702
- // Check for collisions
703
- await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
979
+ logger.kv('Protocol', protocol.toUpperCase());
980
+ logger.kv('Mode', mode.toUpperCase());
981
+ // Execute pre-spawn hooks from protocol.json (collision check, issue comment)
982
+ // Skip collision checks in resume mode — the worktree is expected to exist
983
+ if (!options.resume) {
984
+ if (protocolDef?.hooks?.['pre-spawn']) {
985
+ await executePreSpawnHooks(protocolDef, {
986
+ issueNumber,
987
+ issue,
988
+ worktreePath,
989
+ force: options.force,
990
+ noComment: options.noComment,
991
+ });
992
+ }
993
+ else {
994
+ // Fallback: hardcoded behavior for backwards compatibility
995
+ await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
996
+ if (!options.noComment) {
997
+ logger.info('Commenting on issue...');
998
+ try {
999
+ await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
1000
+ }
1001
+ catch {
1002
+ logger.warn('Warning: Failed to comment on issue (continuing anyway)');
1003
+ }
1004
+ }
1005
+ }
1006
+ }
704
1007
  await ensureDirectories(config);
705
1008
  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."`);
1009
+ if (options.resume) {
1010
+ if (!existsSync(worktreePath)) {
1011
+ fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
712
1012
  }
713
- catch {
714
- logger.warn('Warning: Failed to comment on issue (continuing anyway)');
1013
+ if (!existsSync(resolve(worktreePath, '.git'))) {
1014
+ fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
715
1015
  }
1016
+ logger.info('Resuming existing worktree (skipping creation)');
716
1017
  }
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}`;
1018
+ else {
1019
+ await createWorktree(config, branchName, worktreePath);
1020
+ // Pre-initialize porch so the builder doesn't need to figure out project ID
1021
+ await initPorchInWorktree(worktreePath, protocol, builderId, slug);
1022
+ }
1023
+ // Build the prompt using template
1024
+ const templateContext = {
1025
+ protocol_name: protocol.toUpperCase(),
1026
+ mode,
1027
+ mode_soft: mode === 'soft',
1028
+ mode_strict: mode === 'strict',
1029
+ project_id: builderId,
1030
+ input_description: `a fix for GitHub Issue #${issueNumber}`,
1031
+ issue: {
1032
+ number: issueNumber,
1033
+ title: issue.title,
1034
+ body: issue.body || '(No description provided)',
1035
+ },
1036
+ };
1037
+ const prompt = buildPromptFromTemplate(config, protocol, templateContext);
1038
+ const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
1039
+ const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
742
1040
  // Load role
743
1041
  const role = options.noRole ? null : loadRolePrompt(config, 'builder');
744
1042
  const commands = getResolvedCommands();
745
- const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
1043
+ const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
746
1044
  const builder = {
747
1045
  id: builderId,
748
1046
  name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`,
749
- port,
750
- pid,
751
- status: 'spawning',
1047
+ status: 'implementing',
752
1048
  phase: 'init',
753
1049
  worktree: worktreePath,
754
1050
  branch: branchName,
755
- tmuxSession: sessionName,
756
1051
  type: 'bugfix',
757
1052
  issueNumber,
1053
+ terminalId,
758
1054
  };
759
1055
  upsertBuilder(builder);
760
1056
  logger.blank();
761
1057
  logger.success(`Bugfix builder for issue #${issueNumber} spawned!`);
762
- logger.kv('Terminal', `http://localhost:${port}`);
1058
+ logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
1059
+ logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
763
1060
  }
764
1061
  // =============================================================================
765
1062
  // Main entry point
@@ -770,6 +1067,25 @@ Start by reading the issue and reproducing the bug.`;
770
1067
  export async function spawn(options) {
771
1068
  validateSpawnOptions(options);
772
1069
  const config = getConfig();
1070
+ // Refuse to spawn if the main worktree has uncommitted changes.
1071
+ // Builders work in git worktrees branched from HEAD — uncommitted changes
1072
+ // (specs, plans, codev updates) won't be visible to the builder.
1073
+ // Skip this check in resume mode — the worktree already exists with its own branch.
1074
+ if (!options.force && !options.resume) {
1075
+ try {
1076
+ const { stdout } = await run('git status --porcelain', { cwd: config.projectRoot });
1077
+ if (stdout.trim().length > 0) {
1078
+ fatal('Uncommitted changes detected in main worktree.\n\n' +
1079
+ ' Builders branch from HEAD, so uncommitted files (specs, plans,\n' +
1080
+ ' codev updates) will NOT be visible to the builder.\n\n' +
1081
+ ' Please commit or stash your changes first, then retry.\n' +
1082
+ ' Use --force to skip this check.');
1083
+ }
1084
+ }
1085
+ catch {
1086
+ // Non-fatal — if git status fails, allow spawn to continue
1087
+ }
1088
+ }
773
1089
  // Prune stale worktrees before spawning to prevent "can't find session" errors
774
1090
  // This catches orphaned worktrees from crashes, manual kills, or incomplete cleanups
775
1091
  try {