@cluesmith/codev 2.0.0-rc.6 → 2.0.0-rc.61

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