@cluesmith/codev 2.0.0-rc.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/bin/af.js +2 -2
  2. package/bin/consult.js +1 -1
  3. package/bin/porch.js +6 -35
  4. package/dashboard/dist/assets/index-4n9zpWLY.css +32 -0
  5. package/dashboard/dist/assets/index-b38SaXk5.js +136 -0
  6. package/dashboard/dist/assets/index-b38SaXk5.js.map +1 -0
  7. package/dashboard/dist/index.html +14 -0
  8. package/dist/agent-farm/cli.d.ts.map +1 -1
  9. package/dist/agent-farm/cli.js +179 -118
  10. package/dist/agent-farm/cli.js.map +1 -1
  11. package/dist/agent-farm/commands/architect.d.ts +3 -3
  12. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/architect.js +20 -147
  14. package/dist/agent-farm/commands/architect.js.map +1 -1
  15. package/dist/agent-farm/commands/attach.d.ts +13 -0
  16. package/dist/agent-farm/commands/attach.d.ts.map +1 -0
  17. package/dist/agent-farm/commands/attach.js +144 -0
  18. package/dist/agent-farm/commands/attach.js.map +1 -0
  19. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  20. package/dist/agent-farm/commands/cleanup.js +35 -19
  21. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  22. package/dist/agent-farm/commands/consult.d.ts +3 -4
  23. package/dist/agent-farm/commands/consult.d.ts.map +1 -1
  24. package/dist/agent-farm/commands/consult.js +27 -37
  25. package/dist/agent-farm/commands/consult.js.map +1 -1
  26. package/dist/agent-farm/commands/index.d.ts +2 -2
  27. package/dist/agent-farm/commands/index.d.ts.map +1 -1
  28. package/dist/agent-farm/commands/index.js +2 -2
  29. package/dist/agent-farm/commands/index.js.map +1 -1
  30. package/dist/agent-farm/commands/open.d.ts +4 -2
  31. package/dist/agent-farm/commands/open.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/open.js +33 -83
  33. package/dist/agent-farm/commands/open.js.map +1 -1
  34. package/dist/agent-farm/commands/send.d.ts +1 -1
  35. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  36. package/dist/agent-farm/commands/send.js +70 -79
  37. package/dist/agent-farm/commands/send.js.map +1 -1
  38. package/dist/agent-farm/commands/shell.d.ts +15 -0
  39. package/dist/agent-farm/commands/shell.d.ts.map +1 -0
  40. package/dist/agent-farm/commands/shell.js +50 -0
  41. package/dist/agent-farm/commands/shell.js.map +1 -0
  42. package/dist/agent-farm/commands/spawn-roles.d.ts +80 -0
  43. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -0
  44. package/dist/agent-farm/commands/spawn-roles.js +278 -0
  45. package/dist/agent-farm/commands/spawn-roles.js.map +1 -0
  46. package/dist/agent-farm/commands/spawn-worktree.d.ts +96 -0
  47. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -0
  48. package/dist/agent-farm/commands/spawn-worktree.js +305 -0
  49. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -0
  50. package/dist/agent-farm/commands/spawn.d.ts +5 -1
  51. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  52. package/dist/agent-farm/commands/spawn.js +242 -586
  53. package/dist/agent-farm/commands/spawn.js.map +1 -1
  54. package/dist/agent-farm/commands/start.d.ts +10 -20
  55. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  56. package/dist/agent-farm/commands/start.js +45 -491
  57. package/dist/agent-farm/commands/start.js.map +1 -1
  58. package/dist/agent-farm/commands/status.d.ts +2 -0
  59. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  60. package/dist/agent-farm/commands/status.js +75 -24
  61. package/dist/agent-farm/commands/status.js.map +1 -1
  62. package/dist/agent-farm/commands/stop.d.ts +6 -0
  63. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  64. package/dist/agent-farm/commands/stop.js +49 -109
  65. package/dist/agent-farm/commands/stop.js.map +1 -1
  66. package/dist/agent-farm/commands/tower-cloud.d.ts +48 -0
  67. package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
  68. package/dist/agent-farm/commands/tower-cloud.js +293 -0
  69. package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
  70. package/dist/agent-farm/commands/tower.d.ts +9 -0
  71. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  72. package/dist/agent-farm/commands/tower.js +59 -19
  73. package/dist/agent-farm/commands/tower.js.map +1 -1
  74. package/dist/agent-farm/db/index.d.ts +6 -2
  75. package/dist/agent-farm/db/index.d.ts.map +1 -1
  76. package/dist/agent-farm/db/index.js +301 -19
  77. package/dist/agent-farm/db/index.js.map +1 -1
  78. package/dist/agent-farm/db/migrate.d.ts +0 -4
  79. package/dist/agent-farm/db/migrate.d.ts.map +1 -1
  80. package/dist/agent-farm/db/migrate.js +6 -55
  81. package/dist/agent-farm/db/migrate.js.map +1 -1
  82. package/dist/agent-farm/db/schema.d.ts +3 -3
  83. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  84. package/dist/agent-farm/db/schema.js +25 -19
  85. package/dist/agent-farm/db/schema.js.map +1 -1
  86. package/dist/agent-farm/db/types.d.ts +3 -13
  87. package/dist/agent-farm/db/types.d.ts.map +1 -1
  88. package/dist/agent-farm/db/types.js +3 -11
  89. package/dist/agent-farm/db/types.js.map +1 -1
  90. package/dist/agent-farm/hq-connector.d.ts +2 -6
  91. package/dist/agent-farm/hq-connector.d.ts.map +1 -1
  92. package/dist/agent-farm/hq-connector.js +2 -17
  93. package/dist/agent-farm/hq-connector.js.map +1 -1
  94. package/dist/agent-farm/lib/cloud-config.d.ts +59 -0
  95. package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
  96. package/dist/agent-farm/lib/cloud-config.js +143 -0
  97. package/dist/agent-farm/lib/cloud-config.js.map +1 -0
  98. package/dist/agent-farm/lib/device-name.d.ts +25 -0
  99. package/dist/agent-farm/lib/device-name.d.ts.map +1 -0
  100. package/dist/agent-farm/lib/device-name.js +46 -0
  101. package/dist/agent-farm/lib/device-name.js.map +1 -0
  102. package/dist/agent-farm/lib/nonce-store.d.ts +28 -0
  103. package/dist/agent-farm/lib/nonce-store.d.ts.map +1 -0
  104. package/dist/agent-farm/lib/nonce-store.js +60 -0
  105. package/dist/agent-farm/lib/nonce-store.js.map +1 -0
  106. package/dist/agent-farm/lib/token-exchange.d.ts +18 -0
  107. package/dist/agent-farm/lib/token-exchange.d.ts.map +1 -0
  108. package/dist/agent-farm/lib/token-exchange.js +48 -0
  109. package/dist/agent-farm/lib/token-exchange.js.map +1 -0
  110. package/dist/agent-farm/lib/tower-client.d.ts +163 -0
  111. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
  112. package/dist/agent-farm/lib/tower-client.js +233 -0
  113. package/dist/agent-farm/lib/tower-client.js.map +1 -0
  114. package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
  115. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
  116. package/dist/agent-farm/lib/tunnel-client.js +504 -0
  117. package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
  118. package/dist/agent-farm/servers/tower-instances.d.ts +82 -0
  119. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -0
  120. package/dist/agent-farm/servers/tower-instances.js +454 -0
  121. package/dist/agent-farm/servers/tower-instances.js.map +1 -0
  122. package/dist/agent-farm/servers/tower-routes.d.ts +34 -0
  123. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -0
  124. package/dist/agent-farm/servers/tower-routes.js +1445 -0
  125. package/dist/agent-farm/servers/tower-routes.js.map +1 -0
  126. package/dist/agent-farm/servers/tower-server.d.ts +5 -2
  127. package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
  128. package/dist/agent-farm/servers/tower-server.js +157 -475
  129. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  130. package/dist/agent-farm/servers/tower-terminals.d.ts +119 -0
  131. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -0
  132. package/dist/agent-farm/servers/tower-terminals.js +629 -0
  133. package/dist/agent-farm/servers/tower-terminals.js.map +1 -0
  134. package/dist/agent-farm/servers/tower-tunnel.d.ts +34 -0
  135. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -0
  136. package/dist/agent-farm/servers/tower-tunnel.js +473 -0
  137. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -0
  138. package/dist/agent-farm/servers/tower-types.d.ts +86 -0
  139. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -0
  140. package/dist/agent-farm/servers/tower-types.js +6 -0
  141. package/dist/agent-farm/servers/tower-types.js.map +1 -0
  142. package/dist/agent-farm/servers/tower-utils.d.ts +58 -0
  143. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -0
  144. package/dist/agent-farm/servers/tower-utils.js +182 -0
  145. package/dist/agent-farm/servers/tower-utils.js.map +1 -0
  146. package/dist/agent-farm/servers/tower-websocket.d.ts +25 -0
  147. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -0
  148. package/dist/agent-farm/servers/tower-websocket.js +171 -0
  149. package/dist/agent-farm/servers/tower-websocket.js.map +1 -0
  150. package/dist/agent-farm/state.d.ts +6 -12
  151. package/dist/agent-farm/state.d.ts.map +1 -1
  152. package/dist/agent-farm/state.js +34 -49
  153. package/dist/agent-farm/state.js.map +1 -1
  154. package/dist/agent-farm/types.d.ts +49 -26
  155. package/dist/agent-farm/types.d.ts.map +1 -1
  156. package/dist/agent-farm/utils/config.d.ts +0 -5
  157. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  158. package/dist/agent-farm/utils/config.js +12 -44
  159. package/dist/agent-farm/utils/config.js.map +1 -1
  160. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  161. package/dist/agent-farm/utils/deps.js +0 -32
  162. package/dist/agent-farm/utils/deps.js.map +1 -1
  163. package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
  164. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
  165. package/dist/agent-farm/utils/file-tabs.js +46 -0
  166. package/dist/agent-farm/utils/file-tabs.js.map +1 -0
  167. package/dist/agent-farm/utils/gate-status.d.ts +16 -0
  168. package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
  169. package/dist/agent-farm/utils/gate-status.js +79 -0
  170. package/dist/agent-farm/utils/gate-status.js.map +1 -0
  171. package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
  172. package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
  173. package/dist/agent-farm/utils/gate-watcher.js +122 -0
  174. package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
  175. package/dist/agent-farm/utils/index.d.ts +0 -1
  176. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  177. package/dist/agent-farm/utils/index.js +0 -1
  178. package/dist/agent-farm/utils/index.js.map +1 -1
  179. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  180. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  181. package/dist/agent-farm/utils/notifications.js +121 -0
  182. package/dist/agent-farm/utils/notifications.js.map +1 -0
  183. package/dist/agent-farm/utils/server-utils.d.ts +5 -5
  184. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  185. package/dist/agent-farm/utils/server-utils.js +5 -16
  186. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  187. package/dist/agent-farm/utils/session.d.ts +32 -0
  188. package/dist/agent-farm/utils/session.d.ts.map +1 -0
  189. package/dist/agent-farm/utils/session.js +57 -0
  190. package/dist/agent-farm/utils/session.js.map +1 -0
  191. package/dist/agent-farm/utils/shell.d.ts +9 -22
  192. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  193. package/dist/agent-farm/utils/shell.js +34 -34
  194. package/dist/agent-farm/utils/shell.js.map +1 -1
  195. package/dist/cli.d.ts.map +1 -1
  196. package/dist/cli.js +11 -54
  197. package/dist/cli.js.map +1 -1
  198. package/dist/commands/adopt.d.ts.map +1 -1
  199. package/dist/commands/adopt.js +49 -4
  200. package/dist/commands/adopt.js.map +1 -1
  201. package/dist/commands/consult/index.d.ts +13 -2
  202. package/dist/commands/consult/index.d.ts.map +1 -1
  203. package/dist/commands/consult/index.js +245 -29
  204. package/dist/commands/consult/index.js.map +1 -1
  205. package/dist/commands/doctor.d.ts.map +1 -1
  206. package/dist/commands/doctor.js +96 -79
  207. package/dist/commands/doctor.js.map +1 -1
  208. package/dist/commands/init.d.ts.map +1 -1
  209. package/dist/commands/init.js +52 -3
  210. package/dist/commands/init.js.map +1 -1
  211. package/dist/commands/porch/build-counter.d.ts +5 -0
  212. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  213. package/dist/commands/porch/build-counter.js +5 -0
  214. package/dist/commands/porch/build-counter.js.map +1 -0
  215. package/dist/commands/porch/checks.d.ts +17 -29
  216. package/dist/commands/porch/checks.d.ts.map +1 -1
  217. package/dist/commands/porch/checks.js +96 -144
  218. package/dist/commands/porch/checks.js.map +1 -1
  219. package/dist/commands/porch/index.d.ts +25 -43
  220. package/dist/commands/porch/index.d.ts.map +1 -1
  221. package/dist/commands/porch/index.js +466 -1238
  222. package/dist/commands/porch/index.js.map +1 -1
  223. package/dist/commands/porch/next.d.ts +22 -0
  224. package/dist/commands/porch/next.d.ts.map +1 -0
  225. package/dist/commands/porch/next.js +571 -0
  226. package/dist/commands/porch/next.js.map +1 -0
  227. package/dist/commands/porch/plan.d.ts +70 -0
  228. package/dist/commands/porch/plan.d.ts.map +1 -0
  229. package/dist/commands/porch/plan.js +190 -0
  230. package/dist/commands/porch/plan.js.map +1 -0
  231. package/dist/commands/porch/prompts.d.ts +19 -0
  232. package/dist/commands/porch/prompts.d.ts.map +1 -0
  233. package/dist/commands/porch/prompts.js +277 -0
  234. package/dist/commands/porch/prompts.js.map +1 -0
  235. package/dist/commands/porch/protocol.d.ts +59 -0
  236. package/dist/commands/porch/protocol.d.ts.map +1 -0
  237. package/dist/commands/porch/protocol.js +294 -0
  238. package/dist/commands/porch/protocol.js.map +1 -0
  239. package/dist/commands/porch/state.d.ts +36 -107
  240. package/dist/commands/porch/state.d.ts.map +1 -1
  241. package/dist/commands/porch/state.js +120 -699
  242. package/dist/commands/porch/state.js.map +1 -1
  243. package/dist/commands/porch/types.d.ts +99 -164
  244. package/dist/commands/porch/types.d.ts.map +1 -1
  245. package/dist/commands/porch/types.js +2 -1
  246. package/dist/commands/porch/types.js.map +1 -1
  247. package/dist/commands/porch/verdict.d.ts +31 -0
  248. package/dist/commands/porch/verdict.d.ts.map +1 -0
  249. package/dist/commands/porch/verdict.js +59 -0
  250. package/dist/commands/porch/verdict.js.map +1 -0
  251. package/dist/commands/update.d.ts.map +1 -1
  252. package/dist/commands/update.js +31 -0
  253. package/dist/commands/update.js.map +1 -1
  254. package/dist/lib/scaffold.d.ts +37 -0
  255. package/dist/lib/scaffold.d.ts.map +1 -1
  256. package/dist/lib/scaffold.js +114 -0
  257. package/dist/lib/scaffold.js.map +1 -1
  258. package/dist/terminal/index.d.ts +8 -0
  259. package/dist/terminal/index.d.ts.map +1 -0
  260. package/dist/terminal/index.js +5 -0
  261. package/dist/terminal/index.js.map +1 -0
  262. package/dist/terminal/pty-manager.d.ts +69 -0
  263. package/dist/terminal/pty-manager.d.ts.map +1 -0
  264. package/dist/terminal/pty-manager.js +377 -0
  265. package/dist/terminal/pty-manager.js.map +1 -0
  266. package/dist/terminal/pty-session.d.ts +104 -0
  267. package/dist/terminal/pty-session.d.ts.map +1 -0
  268. package/dist/terminal/pty-session.js +327 -0
  269. package/dist/terminal/pty-session.js.map +1 -0
  270. package/dist/terminal/ring-buffer.d.ts +34 -0
  271. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  272. package/dist/terminal/ring-buffer.js +94 -0
  273. package/dist/terminal/ring-buffer.js.map +1 -0
  274. package/dist/terminal/session-manager.d.ts +115 -0
  275. package/dist/terminal/session-manager.d.ts.map +1 -0
  276. package/dist/terminal/session-manager.js +582 -0
  277. package/dist/terminal/session-manager.js.map +1 -0
  278. package/dist/terminal/shellper-client.d.ts +66 -0
  279. package/dist/terminal/shellper-client.d.ts.map +1 -0
  280. package/dist/terminal/shellper-client.js +234 -0
  281. package/dist/terminal/shellper-client.js.map +1 -0
  282. package/dist/terminal/shellper-main.d.ts +19 -0
  283. package/dist/terminal/shellper-main.d.ts.map +1 -0
  284. package/dist/terminal/shellper-main.js +153 -0
  285. package/dist/terminal/shellper-main.js.map +1 -0
  286. package/dist/terminal/shellper-process.d.ts +75 -0
  287. package/dist/terminal/shellper-process.d.ts.map +1 -0
  288. package/dist/terminal/shellper-process.js +279 -0
  289. package/dist/terminal/shellper-process.js.map +1 -0
  290. package/dist/terminal/shellper-protocol.d.ts +115 -0
  291. package/dist/terminal/shellper-protocol.d.ts.map +1 -0
  292. package/dist/terminal/shellper-protocol.js +214 -0
  293. package/dist/terminal/shellper-protocol.js.map +1 -0
  294. package/dist/terminal/shellper-replay-buffer.d.ts +38 -0
  295. package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -0
  296. package/dist/terminal/shellper-replay-buffer.js +94 -0
  297. package/dist/terminal/shellper-replay-buffer.js.map +1 -0
  298. package/dist/terminal/ws-protocol.d.ts +27 -0
  299. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  300. package/dist/terminal/ws-protocol.js +44 -0
  301. package/dist/terminal/ws-protocol.js.map +1 -0
  302. package/package.json +19 -5
  303. package/skeleton/.claude/skills/af/SKILL.md +89 -0
  304. package/skeleton/.claude/skills/codev/SKILL.md +41 -0
  305. package/skeleton/.claude/skills/consult/SKILL.md +81 -0
  306. package/skeleton/.claude/skills/generate-image/SKILL.md +56 -0
  307. package/skeleton/DEPENDENCIES.md +4 -62
  308. package/skeleton/builders.md +1 -1
  309. package/skeleton/consult-types/impl-review.md +18 -9
  310. package/skeleton/consult-types/integration-review.md +1 -1
  311. package/skeleton/consult-types/plan-review.md +1 -1
  312. package/skeleton/consult-types/pr-ready.md +1 -1
  313. package/skeleton/consult-types/spec-review.md +1 -1
  314. package/skeleton/porch/prompts/defend.md +1 -1
  315. package/skeleton/porch/prompts/evaluate.md +2 -2
  316. package/skeleton/porch/prompts/implement.md +1 -1
  317. package/skeleton/porch/prompts/plan.md +1 -1
  318. package/skeleton/porch/prompts/review.md +4 -4
  319. package/skeleton/porch/prompts/specify.md +1 -1
  320. package/skeleton/porch/prompts/understand.md +2 -2
  321. package/skeleton/protocol-schema.json +282 -0
  322. package/skeleton/protocols/bugfix/builder-prompt.md +60 -0
  323. package/skeleton/protocols/bugfix/prompts/fix.md +77 -0
  324. package/skeleton/protocols/bugfix/prompts/investigate.md +77 -0
  325. package/skeleton/protocols/bugfix/prompts/pr.md +84 -0
  326. package/skeleton/protocols/bugfix/protocol.json +20 -33
  327. package/skeleton/protocols/experiment/builder-prompt.md +52 -0
  328. package/skeleton/protocols/experiment/protocol.json +101 -0
  329. package/skeleton/protocols/experiment/protocol.md +3 -3
  330. package/skeleton/protocols/experiment/templates/notes.md +1 -1
  331. package/skeleton/protocols/maintain/builder-prompt.md +46 -0
  332. package/skeleton/protocols/maintain/prompts/audit.md +111 -0
  333. package/skeleton/protocols/maintain/prompts/clean.md +91 -0
  334. package/skeleton/protocols/maintain/prompts/sync.md +113 -0
  335. package/skeleton/protocols/maintain/prompts/verify.md +110 -0
  336. package/skeleton/protocols/maintain/protocol.json +141 -0
  337. package/skeleton/protocols/maintain/protocol.md +17 -11
  338. package/skeleton/protocols/protocol-schema.json +54 -1
  339. package/skeleton/protocols/spir/builder-prompt.md +66 -0
  340. package/skeleton/protocols/spir/prompts/implement.md +208 -0
  341. package/skeleton/protocols/{spider → spir}/prompts/plan.md +6 -70
  342. package/skeleton/protocols/{spider → spir}/prompts/review.md +20 -39
  343. package/skeleton/protocols/{spider → spir}/prompts/specify.md +33 -61
  344. package/skeleton/protocols/spir/protocol.json +156 -0
  345. package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
  346. package/skeleton/protocols/{spider → spir}/templates/plan.md +14 -0
  347. package/skeleton/protocols/spir/templates/review.md +89 -0
  348. package/skeleton/protocols/tick/builder-prompt.md +56 -0
  349. package/skeleton/protocols/tick/protocol.json +7 -2
  350. package/skeleton/protocols/tick/protocol.md +18 -18
  351. package/skeleton/protocols/tick/templates/review.md +1 -1
  352. package/skeleton/resources/commands/agent-farm.md +63 -46
  353. package/skeleton/resources/commands/codev.md +0 -2
  354. package/skeleton/resources/commands/overview.md +7 -17
  355. package/skeleton/resources/workflow-reference.md +4 -4
  356. package/skeleton/roles/architect.md +152 -315
  357. package/skeleton/roles/builder.md +120 -214
  358. package/skeleton/roles/consultant.md +6 -6
  359. package/skeleton/templates/AGENTS.md +2 -2
  360. package/skeleton/templates/CLAUDE.md +2 -2
  361. package/skeleton/templates/cheatsheet.md +7 -5
  362. package/skeleton/templates/projectlist.md +1 -1
  363. package/templates/dashboard/index.html +17 -43
  364. package/templates/dashboard/js/dialogs.js +7 -7
  365. package/templates/dashboard/js/files.js +2 -2
  366. package/templates/dashboard/js/main.js +4 -4
  367. package/templates/dashboard/js/projects.js +3 -3
  368. package/templates/dashboard/js/tabs.js +1 -1
  369. package/templates/dashboard/js/utils.js +22 -87
  370. package/templates/open.html +26 -0
  371. package/templates/tower.html +731 -91
  372. package/dist/agent-farm/commands/kickoff.d.ts +0 -20
  373. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  374. package/dist/agent-farm/commands/kickoff.js +0 -337
  375. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  376. package/dist/agent-farm/commands/rename.d.ts +0 -13
  377. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  378. package/dist/agent-farm/commands/rename.js +0 -33
  379. package/dist/agent-farm/commands/rename.js.map +0 -1
  380. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  381. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  382. package/dist/agent-farm/commands/tutorial.js +0 -49
  383. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  384. package/dist/agent-farm/commands/util.d.ts +0 -15
  385. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  386. package/dist/agent-farm/commands/util.js +0 -108
  387. package/dist/agent-farm/commands/util.js.map +0 -1
  388. package/dist/agent-farm/servers/dashboard-server.d.ts +0 -7
  389. package/dist/agent-farm/servers/dashboard-server.d.ts.map +0 -1
  390. package/dist/agent-farm/servers/dashboard-server.js +0 -1872
  391. package/dist/agent-farm/servers/dashboard-server.js.map +0 -1
  392. package/dist/agent-farm/servers/open-server.d.ts +0 -7
  393. package/dist/agent-farm/servers/open-server.d.ts.map +0 -1
  394. package/dist/agent-farm/servers/open-server.js +0 -315
  395. package/dist/agent-farm/servers/open-server.js.map +0 -1
  396. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  397. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  398. package/dist/agent-farm/tutorial/index.js +0 -8
  399. package/dist/agent-farm/tutorial/index.js.map +0 -1
  400. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  401. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  402. package/dist/agent-farm/tutorial/prompts.js +0 -147
  403. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  404. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  405. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  406. package/dist/agent-farm/tutorial/runner.js +0 -204
  407. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  408. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  409. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  410. package/dist/agent-farm/tutorial/state.js +0 -89
  411. package/dist/agent-farm/tutorial/state.js.map +0 -1
  412. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  413. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  414. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  415. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  416. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  417. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  418. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  419. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  420. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  421. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  422. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  423. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  424. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  425. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  426. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  427. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  428. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  429. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  430. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  431. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  432. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  433. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  434. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  435. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  436. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  437. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  438. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  439. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  440. package/dist/agent-farm/utils/orphan-handler.d.ts +0 -27
  441. package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
  442. package/dist/agent-farm/utils/orphan-handler.js +0 -149
  443. package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
  444. package/dist/agent-farm/utils/port-registry.d.ts +0 -58
  445. package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
  446. package/dist/agent-farm/utils/port-registry.js +0 -166
  447. package/dist/agent-farm/utils/port-registry.js.map +0 -1
  448. package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
  449. package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
  450. package/dist/agent-farm/utils/terminal-ports.js +0 -35
  451. package/dist/agent-farm/utils/terminal-ports.js.map +0 -1
  452. package/dist/commands/pcheck/cache.d.ts +0 -48
  453. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  454. package/dist/commands/pcheck/cache.js +0 -170
  455. package/dist/commands/pcheck/cache.js.map +0 -1
  456. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  457. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  458. package/dist/commands/pcheck/evaluator.js +0 -246
  459. package/dist/commands/pcheck/evaluator.js.map +0 -1
  460. package/dist/commands/pcheck/index.d.ts +0 -12
  461. package/dist/commands/pcheck/index.d.ts.map +0 -1
  462. package/dist/commands/pcheck/index.js +0 -249
  463. package/dist/commands/pcheck/index.js.map +0 -1
  464. package/dist/commands/pcheck/parser.d.ts +0 -39
  465. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  466. package/dist/commands/pcheck/parser.js +0 -155
  467. package/dist/commands/pcheck/parser.js.map +0 -1
  468. package/dist/commands/pcheck/types.d.ts +0 -82
  469. package/dist/commands/pcheck/types.d.ts.map +0 -1
  470. package/dist/commands/pcheck/types.js +0 -5
  471. package/dist/commands/pcheck/types.js.map +0 -1
  472. package/dist/commands/porch/consultation.d.ts +0 -56
  473. package/dist/commands/porch/consultation.d.ts.map +0 -1
  474. package/dist/commands/porch/consultation.js +0 -330
  475. package/dist/commands/porch/consultation.js.map +0 -1
  476. package/dist/commands/porch/notifications.d.ts +0 -99
  477. package/dist/commands/porch/notifications.d.ts.map +0 -1
  478. package/dist/commands/porch/notifications.js +0 -223
  479. package/dist/commands/porch/notifications.js.map +0 -1
  480. package/dist/commands/porch/plan-parser.d.ts +0 -38
  481. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  482. package/dist/commands/porch/plan-parser.js +0 -166
  483. package/dist/commands/porch/plan-parser.js.map +0 -1
  484. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  485. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  486. package/dist/commands/porch/protocol-loader.js +0 -253
  487. package/dist/commands/porch/protocol-loader.js.map +0 -1
  488. package/dist/commands/porch/signal-parser.d.ts +0 -88
  489. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  490. package/dist/commands/porch/signal-parser.js +0 -148
  491. package/dist/commands/porch/signal-parser.js.map +0 -1
  492. package/dist/commands/tower.d.ts +0 -16
  493. package/dist/commands/tower.d.ts.map +0 -1
  494. package/dist/commands/tower.js +0 -21
  495. package/dist/commands/tower.js.map +0 -1
  496. package/skeleton/config.json +0 -7
  497. package/skeleton/porch/protocols/bugfix.json +0 -85
  498. package/skeleton/porch/protocols/spider.json +0 -135
  499. package/skeleton/porch/protocols/tick.json +0 -76
  500. package/skeleton/protocols/spider/prompts/defend.md +0 -215
  501. package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
  502. package/skeleton/protocols/spider/prompts/implement.md +0 -149
  503. package/skeleton/protocols/spider/protocol.json +0 -210
  504. package/skeleton/protocols/spider/templates/review.md +0 -207
  505. package/templates/dashboard/css/activity.css +0 -151
  506. package/templates/dashboard/js/activity.js +0 -112
  507. /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
@@ -1,1332 +1,560 @@
1
1
  /**
2
2
  * Porch - Protocol Orchestrator
3
3
  *
4
- * Generic loop orchestrator that reads protocol definitions from JSON
5
- * and executes them with Claude. Implements the Ralph pattern: fresh
6
- * context per iteration with state persisted to files.
4
+ * Claude calls porch as a tool; porch returns prescriptive instructions.
5
+ * All commands produce clear, actionable output.
7
6
  */
8
7
  import * as fs from 'node:fs';
9
8
  import * as path from 'node:path';
10
- import * as readline from 'node:readline';
11
- import { spawn } from 'node:child_process';
12
9
  import chalk from 'chalk';
13
- import { findProjectRoot, getSkeletonDir } from '../../lib/skeleton.js';
14
- import { getProjectDir, readState, writeState, createInitialState, updateState, approveGate, requestGateApproval, updatePhaseStatus, setPlanPhases, findProjects, findExecutions, findStatusFile, findPendingGates, getConsultationAttempts, incrementConsultationAttempts, resetConsultationAttempts, } from './state.js';
15
- import { extractPhasesFromPlanFile, findPlanFile, } from './plan-parser.js';
16
- import { extractSignal } from './signal-parser.js';
17
- import { runPhaseChecks, formatCheckResults } from './checks.js';
18
- import { runConsultationLoop, formatConsultationResults, hasConsultation, } from './consultation.js';
19
- import { loadProtocol as loadProtocolFromLoader, listProtocols as listProtocolsFromLoader, } from './protocol-loader.js';
20
- import { createNotifier, } from './notifications.js';
10
+ import { globSync } from 'glob';
11
+ import { readState, writeState, createInitialState, findStatusPath, getProjectDir, getStatusPath, resolveProjectId, } from './state.js';
12
+ import { loadProtocol, getPhaseConfig, getNextPhase, getPhaseChecks, getPhaseGate, isPhased, isBuildVerify, getVerifyConfig, } from './protocol.js';
13
+ import { findPlanFile, extractPhasesFromFile, getCurrentPlanPhase, getPhaseContent, allPlanPhasesComplete, } from './plan.js';
14
+ import { runPhaseChecks, formatCheckResults, allChecksPassed, } from './checks.js';
21
15
  // ============================================================================
22
- // Protocol Loading (delegates to protocol-loader.ts)
16
+ // Output Helpers
23
17
  // ============================================================================
24
- /**
25
- * List available protocols
26
- * Delegates to protocol-loader.ts for proper conversion
27
- */
28
- export function listProtocols(projectRoot) {
29
- const root = projectRoot || findProjectRoot();
30
- return listProtocolsFromLoader(root);
18
+ function header(text) {
19
+ const line = '═'.repeat(50);
20
+ return `${line}\n ${text}\n${line}`;
31
21
  }
32
- /**
33
- * Load a protocol definition
34
- * Delegates to protocol-loader.ts which properly converts steps→substates
35
- */
36
- export function loadProtocol(name, projectRoot) {
37
- const root = projectRoot || findProjectRoot();
38
- const protocol = loadProtocolFromLoader(root, name);
39
- if (!protocol) {
40
- throw new Error(`Protocol not found: ${name}\nAvailable protocols: ${listProtocols(root).join(', ')}`);
41
- }
42
- return protocol;
43
- }
44
- /**
45
- * Load a prompt file for a phase
46
- */
47
- function loadPrompt(protocol, phaseId, projectRoot) {
48
- const phase = protocol.phases.find(p => p.id === phaseId);
49
- if (!phase?.prompt) {
50
- return null;
51
- }
52
- // New structure: protocols/<protocol>/prompts/<prompt>.md
53
- const promptPaths = [
54
- path.join(projectRoot, 'codev', 'protocols', protocol.name, 'prompts', phase.prompt),
55
- path.join(getSkeletonDir(), 'protocols', protocol.name, 'prompts', phase.prompt),
56
- // Legacy paths
57
- path.join(projectRoot, 'codev', 'porch', 'prompts', phase.prompt),
58
- path.join(getSkeletonDir(), 'porch', 'prompts', phase.prompt),
59
- ];
60
- for (const promptPath of promptPaths) {
61
- if (fs.existsSync(promptPath)) {
62
- return fs.readFileSync(promptPath, 'utf-8');
63
- }
64
- // Try with .md extension
65
- if (fs.existsSync(`${promptPath}.md`)) {
66
- return fs.readFileSync(`${promptPath}.md`, 'utf-8');
67
- }
68
- }
69
- return null;
22
+ function section(title, content) {
23
+ return `\n${chalk.bold(title)}:\n${content}`;
70
24
  }
71
25
  // ============================================================================
72
- // Protocol Helpers
26
+ // Commands
73
27
  // ============================================================================
74
28
  /**
75
- * Check if a phase is terminal
29
+ * porch status <id>
30
+ * Shows current state and prescriptive next steps.
76
31
  */
77
- function isTerminalPhase(protocol, phaseId) {
78
- const phase = protocol.phases.find(p => p.id === phaseId);
79
- return phase?.terminal === true;
80
- }
81
- /**
82
- * Find the phase that has a gate blocking after the given state
83
- */
84
- function getGateForState(protocol, state) {
85
- const [phaseId, substate] = state.split(':');
86
- const phase = protocol.phases.find(p => p.id === phaseId);
87
- if (phase?.gate && phase.gate.after === substate) {
88
- const gateId = `${phaseId}_approval`;
89
- return { gateId, phase };
32
+ export async function status(projectRoot, projectId) {
33
+ const statusPath = findStatusPath(projectRoot, projectId);
34
+ if (!statusPath) {
35
+ throw new Error(`Project ${projectId} not found.\nRun 'porch init' to create a new project.`);
90
36
  }
91
- return null;
92
- }
93
- /**
94
- * Get next state after gate passes
95
- */
96
- function getGateNextState(protocol, phaseId) {
97
- const phase = protocol.phases.find(p => p.id === phaseId);
98
- return phase?.gate?.next || null;
99
- }
100
- /**
101
- * Get signal-based next state
102
- */
103
- function getSignalNextState(protocol, phaseId, signal) {
104
- const phase = protocol.phases.find(p => p.id === phaseId);
105
- return phase?.signals?.[signal] || null;
106
- }
107
- /**
108
- * Get the default next state for a phase (first substate or next phase)
109
- */
110
- function getDefaultNextState(protocol, state) {
111
- const [phaseId, substate] = state.split(':');
112
- const phase = protocol.phases.find(p => p.id === phaseId);
113
- if (!phase)
114
- return null;
115
- // If phase has substates, move to next substate
116
- if (phase.substates && substate) {
117
- const currentIdx = phase.substates.indexOf(substate);
118
- if (currentIdx >= 0 && currentIdx < phase.substates.length - 1) {
119
- return `${phaseId}:${phase.substates[currentIdx + 1]}`;
120
- }
121
- }
122
- // Move to next phase
123
- const phaseIdx = protocol.phases.findIndex(p => p.id === phaseId);
124
- if (phaseIdx >= 0 && phaseIdx < protocol.phases.length - 1) {
125
- const nextPhase = protocol.phases[phaseIdx + 1];
126
- if (nextPhase.substates && nextPhase.substates.length > 0) {
127
- return `${nextPhase.id}:${nextPhase.substates[0]}`;
128
- }
129
- return nextPhase.id;
130
- }
131
- return null;
132
- }
133
- let currentRunner = null;
134
- let currentProc = null;
135
- /**
136
- * Get the logs directory for a project
137
- */
138
- function getLogsDir(projectRoot, projectId, projectTitle) {
139
- return path.join(projectRoot, 'codev', 'projects', `${projectId}-${projectTitle}`, 'logs');
140
- }
141
- /**
142
- * Get current log file path
143
- */
144
- function getCurrentLogFile() {
145
- return currentRunner?.logFile || null;
146
- }
147
- /**
148
- * Get runner status for display
149
- */
150
- function getRunnerStatus() {
151
- if (!currentRunner)
152
- return null;
153
- const elapsed = Math.floor((Date.now() - currentRunner.startTime) / 1000);
154
- const mins = Math.floor(elapsed / 60);
155
- const secs = elapsed % 60;
156
- const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
157
- return {
158
- status: currentRunner.status,
159
- details: `Phase: ${currentRunner.phaseId} | ${currentRunner.lineCount} lines | ${timeStr}`,
160
- };
161
- }
162
- /**
163
- * Cancel the current Claude execution
164
- */
165
- function cancelClaude() {
166
- if (!currentProc || !currentRunner || currentRunner.status !== 'running') {
167
- return false;
168
- }
169
- currentRunner.status = 'cancelled';
170
- currentProc.kill('SIGTERM');
171
- setTimeout(() => {
172
- if (currentProc && !currentProc.killed) {
173
- currentProc.kill('SIGKILL');
174
- }
175
- }, 2000);
176
- return true;
177
- }
178
- /**
179
- * Read last N lines from log file
180
- */
181
- function tailLog(n = 20) {
182
- if (!currentRunner?.logFile || !fs.existsSync(currentRunner.logFile)) {
183
- return [];
184
- }
185
- try {
186
- const content = fs.readFileSync(currentRunner.logFile, 'utf-8');
187
- const lines = content.split('\n');
188
- return lines.slice(-n);
189
- }
190
- catch {
191
- return [];
192
- }
193
- }
194
- /**
195
- * Invoke Claude for a phase - runs in background, writes to log file
196
- */
197
- async function invokeClaude(protocol, phaseId, state, statusFilePath, projectRoot, options) {
198
- const promptContent = loadPrompt(protocol, phaseId, projectRoot);
199
- if (!promptContent) {
200
- console.log(chalk.yellow(`[porch] No prompt file for phase: ${phaseId}`));
201
- return '';
202
- }
203
- if (options.dryRun) {
204
- console.log(chalk.yellow(`[porch] [DRY RUN] Would invoke Claude for phase: ${phaseId}`));
205
- return '';
206
- }
207
- if (options.noClaude) {
208
- console.log(chalk.blue(`[porch] [NO_CLAUDE] Simulating phase: ${phaseId}`));
209
- await new Promise(r => setTimeout(r, 1000));
210
- console.log(chalk.green(`[porch] Simulated completion of phase: ${phaseId}`));
211
- return '';
212
- }
213
- // Set up log file
214
- const logsDir = getLogsDir(projectRoot, state.id, state.title);
215
- fs.mkdirSync(logsDir, { recursive: true });
216
- const logFile = path.join(logsDir, `${phaseId}_${state.iteration}_${Date.now()}.log`);
217
- console.log(chalk.cyan(`[phase] Starting Claude for phase: ${phaseId}`));
218
- console.log(chalk.gray(`[porch] Log: ${logFile}`));
219
- console.log(chalk.gray(`[porch] Commands: tail, status, cancel`));
37
+ const state = readState(statusPath);
38
+ const protocol = loadProtocol(projectRoot, state.protocol);
39
+ const phaseConfig = getPhaseConfig(protocol, state.phase);
40
+ // Header
220
41
  console.log('');
221
- const timeout = protocol.config?.claude_timeout || 600000; // 10 minutes default
222
- const startTime = Date.now();
223
- const maxRetries = 3;
224
- const fullPrompt = `## Protocol: ${protocol.name}
225
- ## Phase: ${phaseId}
226
- ## Project ID: ${state.id}
227
-
228
- ## Current Status
229
- \`\`\`yaml
230
- state: "${state.current_state}"
231
- iteration: ${state.iteration}
232
- started_at: "${state.started_at}"
233
- \`\`\`
234
-
235
- ## Task
236
- Execute the ${phaseId} phase for project ${state.id} - ${state.title}
237
-
238
- ## Phase Instructions
239
- ${promptContent}
240
-
241
- ## Important
242
- - Project ID: ${state.id}
243
- - Protocol: ${protocol.name}
244
- - Follow the instructions above precisely
245
- - Output <signal>...</signal> tags when you reach completion points
246
- `;
247
- // Retry loop for crashes
248
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
249
- try {
250
- const result = await runClaudeWithLogging(fullPrompt, logFile, phaseId, timeout);
251
- return result;
252
- }
253
- catch (error) {
254
- const errorMsg = error instanceof Error ? error.message : String(error);
255
- // Check for known crash patterns that warrant retry
256
- if (errorMsg.includes('No messages returned') ||
257
- errorMsg.includes('SIGTERM') ||
258
- errorMsg.includes('ECONNRESET')) {
259
- if (attempt < maxRetries) {
260
- console.log(chalk.yellow(`[porch] Claude crashed (attempt ${attempt}/${maxRetries}): ${errorMsg}`));
261
- console.log(chalk.yellow(`[porch] Retrying in 5 seconds...`));
262
- await new Promise(r => setTimeout(r, 5000));
263
- continue;
264
- }
42
+ console.log(header(`PROJECT: ${state.id} - ${state.title}`));
43
+ console.log(` PROTOCOL: ${state.protocol}`);
44
+ console.log(` PHASE: ${state.phase} (${phaseConfig?.name || 'unknown'})`);
45
+ // For phased protocols, show plan phase status
46
+ if (isPhased(protocol, state.phase) && state.plan_phases.length > 0) {
47
+ console.log('');
48
+ console.log(chalk.bold('PLAN PHASES:'));
49
+ console.log('');
50
+ // Status icons
51
+ const icon = (status) => {
52
+ switch (status) {
53
+ case 'complete': return chalk.green('✓');
54
+ case 'in_progress': return chalk.yellow('►');
55
+ default: return chalk.gray('○');
265
56
  }
266
- // Non-retryable error or max retries reached
267
- throw error;
268
- }
269
- }
270
- return ''; // Should not reach here
271
- }
272
- /**
273
- * Run Claude with output logging
274
- */
275
- async function runClaudeWithLogging(prompt, logFile, phaseId, timeout) {
276
- const startTime = Date.now();
277
- return new Promise((resolve, reject) => {
278
- const args = ['--print', '-p', prompt, '--dangerously-skip-permissions'];
279
- const proc = spawn('claude', args, {
280
- stdio: ['ignore', 'pipe', 'pipe'],
281
- timeout,
282
- });
283
- currentProc = proc;
284
- currentRunner = {
285
- status: 'running',
286
- phaseId,
287
- logFile,
288
- startTime,
289
- lineCount: 0,
290
- output: '',
291
- pid: proc.pid,
292
57
  };
293
- let output = '';
294
- let stderr = '';
295
- // Open log file for writing
296
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
297
- logStream.write(`=== Claude execution started at ${new Date().toISOString()} ===\n`);
298
- logStream.write(`=== Phase: ${phaseId} ===\n\n`);
299
- proc.stdout.on('data', (data) => {
300
- const text = data.toString();
301
- output += text;
302
- if (currentRunner) {
303
- currentRunner.output += text;
304
- currentRunner.lineCount += (text.match(/\n/g) || []).length;
305
- }
306
- logStream.write(text);
307
- });
308
- proc.stderr.on('data', (data) => {
309
- const text = data.toString();
310
- stderr += text;
311
- logStream.write(`[stderr] ${text}`);
312
- });
313
- proc.on('close', (code) => {
314
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
315
- const mins = Math.floor(elapsed / 60);
316
- const secs = elapsed % 60;
317
- const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
318
- logStream.write(`\n=== Completed in ${timeStr} with code ${code} ===\n`);
319
- logStream.end();
320
- currentProc = null;
321
- if (currentRunner?.status === 'cancelled') {
322
- console.log(chalk.yellow(`[porch] Cancelled after ${timeStr}`));
323
- currentRunner.status = 'cancelled';
324
- resolve('');
58
+ // Show phases
59
+ for (const phase of state.plan_phases) {
60
+ const isCurrent = phase.status === 'in_progress';
61
+ const prefix = isCurrent ? chalk.cyan('→ ') : ' ';
62
+ const title = isCurrent ? chalk.bold(phase.title) : phase.title;
63
+ console.log(`${prefix}${icon(phase.status)} ${phase.id}: ${title}`);
64
+ }
65
+ const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
66
+ if (currentPlanPhase) {
67
+ console.log('');
68
+ console.log(chalk.bold(`CURRENT: ${currentPlanPhase.id} - ${currentPlanPhase.title}`));
69
+ // Show phase content from plan
70
+ const planPath = findPlanFile(projectRoot, state.id, state.title);
71
+ if (planPath) {
72
+ const content = fs.readFileSync(planPath, 'utf-8');
73
+ const phaseContent = getPhaseContent(content, currentPlanPhase.id);
74
+ if (phaseContent) {
75
+ console.log(section('FROM THE PLAN', phaseContent.slice(0, 500)));
76
+ }
325
77
  }
326
- else if (code !== 0) {
327
- console.log(chalk.red(`[porch] Failed after ${timeStr} (exit code ${code})`));
328
- if (currentRunner)
329
- currentRunner.status = 'failed';
330
- reject(new Error(`Claude exited with code ${code}\n${stderr}`));
78
+ // Find the next phase name for the warning
79
+ const currentIdx = state.plan_phases.findIndex(p => p.id === currentPlanPhase.id);
80
+ const nextPlanPhase = state.plan_phases[currentIdx + 1];
81
+ console.log('');
82
+ console.log(chalk.red.bold('╔══════════════════════════════════════════════════════════════╗'));
83
+ console.log(chalk.red.bold('║ 🛑 CRITICAL RULES ║'));
84
+ if (nextPlanPhase) {
85
+ console.log(chalk.red.bold(`║ 1. DO NOT start ${nextPlanPhase.id} until you run porch again!`.padEnd(63) + '║'));
331
86
  }
332
87
  else {
333
- console.log('');
334
- const lines = currentRunner?.lineCount || 0;
335
- console.log(chalk.green(`[porch] Phase complete: ${lines} lines in ${timeStr}`));
336
- if (currentRunner)
337
- currentRunner.status = 'completed';
338
- resolve(output);
88
+ console.log(chalk.red.bold('║ 1. DO NOT start the next phase until you run porch again! ║'));
339
89
  }
340
- });
341
- proc.on('error', (err) => {
342
- logStream.write(`\n=== Error: ${err.message} ===\n`);
343
- logStream.end();
344
- currentProc = null;
345
- if (currentRunner)
346
- currentRunner.status = 'failed';
347
- reject(err);
348
- });
349
- });
350
- }
351
- // ============================================================================
352
- // Commands
353
- // ============================================================================
354
- /**
355
- * Initialize a new project with a protocol
356
- */
357
- export async function init(protocolName, projectId, projectName, options = {}) {
358
- const projectRoot = findProjectRoot();
359
- const protocol = loadProtocol(protocolName, projectRoot);
360
- // Create project directory
361
- const projectDir = getProjectDir(projectRoot, projectId, projectName);
362
- fs.mkdirSync(projectDir, { recursive: true });
363
- // Create initial state
364
- const state = createInitialState(protocol, projectId, projectName, options.worktree);
365
- const statusPath = path.join(projectDir, 'status.yaml');
366
- await writeState(statusPath, state);
367
- console.log(chalk.green(`[porch] Initialized project ${projectId} with protocol ${protocolName}`));
368
- console.log(chalk.blue(`[porch] Project directory: ${projectDir}`));
369
- console.log(chalk.blue(`[porch] Initial state: ${state.current_state}`));
370
- }
371
- /**
372
- * Check if a protocol phase is a "phased" phase (runs per plan-phase)
373
- */
374
- function isPhasedPhase(protocol, phaseId) {
375
- const phase = protocol.phases.find(p => p.id === phaseId);
376
- return phase?.phased === true;
377
- }
378
- /**
379
- * Get the IDE phases (implement, defend, evaluate) that run per plan-phase
380
- */
381
- function getIDEPhases(protocol) {
382
- return protocol.phases
383
- .filter(p => p.phased === true)
384
- .map(p => p.id);
385
- }
386
- /**
387
- * Parse the current plan-phase from state like "implement:phase_1"
388
- */
389
- function parsePlanPhaseFromState(state) {
390
- const parts = state.split(':');
391
- const phaseId = parts[0];
392
- // Check if second part is a plan phase (phase_N) or a substate
393
- if (parts.length > 1) {
394
- if (parts[1].startsWith('phase_')) {
395
- return { phaseId, planPhaseId: parts[1], substate: parts[2] || null };
90
+ console.log(chalk.red.bold('║ 2. Run /compact before starting each new phase ║'));
91
+ console.log(chalk.red.bold('║ 3. After completing this phase, run: porch done ' + state.id.padEnd(12) + '║'));
92
+ console.log(chalk.red.bold('╚══════════════════════════════════════════════════════════════╝'));
396
93
  }
397
- return { phaseId, planPhaseId: null, substate: parts[1] };
398
94
  }
399
- return { phaseId, planPhaseId: null, substate: null };
400
- }
401
- /**
402
- * Get the next IDE state for a phased phase
403
- * implement:phase_1 → defend:phase_1 → evaluate:phase_1 → implement:phase_2 → ...
404
- */
405
- function getNextIDEState(protocol, currentState, planPhases, signal) {
406
- const { phaseId, planPhaseId } = parsePlanPhaseFromState(currentState);
407
- if (!planPhaseId)
408
- return null;
409
- const idePhases = getIDEPhases(protocol);
410
- const currentIdeIndex = idePhases.indexOf(phaseId);
411
- if (currentIdeIndex < 0)
412
- return null;
413
- // If not at the end of IDE phases, move to next IDE phase for same plan-phase
414
- if (currentIdeIndex < idePhases.length - 1) {
415
- return `${idePhases[currentIdeIndex + 1]}:${planPhaseId}`;
95
+ // Show checks status
96
+ const checks = getPhaseChecks(protocol, state.phase);
97
+ if (Object.keys(checks).length > 0) {
98
+ const checkLines = Object.keys(checks).map(name => ` ○ ${name} (not yet run)`);
99
+ console.log(section('CRITERIA', checkLines.join('\n')));
416
100
  }
417
- // At end of IDE phases (evaluate), move to next plan-phase
418
- const currentPlanIndex = planPhases.findIndex(p => p.id === planPhaseId);
419
- if (currentPlanIndex < 0)
420
- return null;
421
- // Check if there's a next plan phase
422
- if (currentPlanIndex < planPhases.length - 1) {
423
- const nextPlanPhase = planPhases[currentPlanIndex + 1];
424
- return `${idePhases[0]}:${nextPlanPhase.id}`; // Start implement for next phase
101
+ // Instructions
102
+ const gate = getPhaseGate(protocol, state.phase);
103
+ if (gate && state.gates[gate]?.status === 'pending' && state.gates[gate]?.requested_at) {
104
+ console.log(section('STATUS', chalk.yellow('WAITING FOR HUMAN APPROVAL')));
105
+ console.log(`\n Gate: ${gate}`);
106
+ console.log(' Do not proceed until gate is approved.');
107
+ console.log(`\n To approve: porch approve ${state.id} ${gate}`);
425
108
  }
426
- // All plan phases complete, move to review
427
- const reviewPhase = protocol.phases.find(p => p.id === 'review');
428
- if (reviewPhase) {
429
- return 'review';
109
+ else {
110
+ console.log(section('INSTRUCTIONS', getInstructions(state, protocol)));
430
111
  }
431
- return 'complete';
432
- }
433
- // ============================================================================
434
- // Interactive REPL
435
- // ============================================================================
436
- /**
437
- * Create a readline interface for interactive input
438
- */
439
- function createRepl() {
440
- return readline.createInterface({
441
- input: process.stdin,
442
- output: process.stdout,
443
- terminal: true,
444
- });
445
- }
446
- /**
447
- * Prompt user for input with a given message
448
- */
449
- async function prompt(rl, message) {
450
- return new Promise((resolve) => {
451
- rl.question(message, (answer) => {
452
- resolve(answer.trim().toLowerCase());
453
- });
454
- });
112
+ console.log(section('NEXT ACTION', getNextAction(state, protocol)));
113
+ console.log('');
455
114
  }
456
115
  /**
457
- * Show REPL help
116
+ * porch check <id>
117
+ * Runs the phase checks and reports results.
458
118
  */
459
- function showReplHelp() {
460
- console.log('');
461
- console.log(chalk.blue('Porch REPL Commands:'));
462
- console.log(chalk.gray(' While Claude is running:'));
463
- console.log(' ' + chalk.green('tail') + ' [n] - Show last n lines of Claude output (default: 20)');
464
- console.log(' ' + chalk.green('logs') + ' - Show path to current log file');
465
- console.log(' ' + chalk.green('cancel') + ' - Cancel current Claude execution');
466
- console.log(' ' + chalk.green('status') + ' - Show current project/runner status');
467
- console.log('');
468
- console.log(chalk.gray(' At gate prompts:'));
469
- console.log(' ' + chalk.green('approve') + ' [gate] - Approve pending gate (or current if omitted)');
470
- console.log(' ' + chalk.green('view') + ' - View the full artifact for current gate');
119
+ export async function check(projectRoot, projectId) {
120
+ const statusPath = findStatusPath(projectRoot, projectId);
121
+ if (!statusPath) {
122
+ throw new Error(`Project ${projectId} not found.`);
123
+ }
124
+ const state = readState(statusPath);
125
+ const protocol = loadProtocol(projectRoot, state.protocol);
126
+ const checks = getPhaseChecks(protocol, state.phase);
127
+ if (Object.keys(checks).length === 0) {
128
+ console.log(chalk.dim('No checks defined for this phase.'));
129
+ return;
130
+ }
131
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
471
132
  console.log('');
472
- console.log(chalk.gray(' General:'));
473
- console.log(' ' + chalk.green('continue') + ' - Continue to next iteration');
474
- console.log(' ' + chalk.green('skip') + ' - Skip current phase (use with caution)');
475
- console.log(' ' + chalk.green('help') + ' - Show this help');
476
- console.log(' ' + chalk.green('quit') + ' - Exit porch');
133
+ console.log(chalk.bold('RUNNING CHECKS...'));
477
134
  console.log('');
478
- }
479
- /**
480
- * Display current status summary
481
- */
482
- function displayStatus(state, protocol) {
135
+ const results = await runPhaseChecks(checks, projectRoot, checkEnv);
136
+ console.log(formatCheckResults(results));
483
137
  console.log('');
484
- console.log(chalk.blue('─'.repeat(50)));
485
- console.log(chalk.blue(`Project: ${state.id} - ${state.title}`));
486
- console.log(chalk.blue(`Protocol: ${state.protocol}`));
487
- console.log(chalk.blue(`State: ${state.current_state}`));
488
- console.log(chalk.blue(`Iteration: ${state.iteration}`));
489
- // Show pending gates
490
- const pendingGates = Object.entries(state.gates)
491
- .filter(([, g]) => g.status === 'pending' && g.requested_at)
492
- .map(([id]) => id);
493
- if (pendingGates.length > 0) {
494
- console.log(chalk.yellow(`Pending gates: ${pendingGates.join(', ')}`));
138
+ if (allChecksPassed(results)) {
139
+ console.log(chalk.green('RESULT: ALL CHECKS PASSED'));
140
+ console.log(`\n Run: porch done ${state.id} (to advance)`);
495
141
  }
496
- // Show Claude runner status
497
- const runnerStatus = getRunnerStatus();
498
- if (runnerStatus) {
499
- const statusColor = runnerStatus.status === 'running' ? chalk.cyan :
500
- runnerStatus.status === 'completed' ? chalk.green :
501
- runnerStatus.status === 'failed' ? chalk.red :
502
- chalk.yellow;
503
- console.log(statusColor(`Claude: ${runnerStatus.status} | ${runnerStatus.details}`));
504
- if (currentRunner?.logFile) {
505
- console.log(chalk.gray(`Log: ${currentRunner.logFile}`));
506
- }
142
+ else {
143
+ console.log(chalk.red('RESULT: CHECKS FAILED'));
144
+ console.log(`\n Fix the failures and run: porch check ${state.id}`);
507
145
  }
508
- console.log(chalk.blue('─'.repeat(50)));
509
146
  console.log('');
510
147
  }
511
148
  /**
512
- * Get the artifact file path for a gate
149
+ * porch done <id>
150
+ * Advances to next phase if checks pass. Refuses if checks fail.
513
151
  */
514
- function getGateArtifact(gateId, state, projectRoot) {
515
- const projectDir = `${state.id}-${state.title}`;
516
- if (gateId === 'specify_approval') {
517
- // Look for spec file
518
- const specPath = path.join(projectRoot, 'codev', 'specs', `${projectDir}.md`);
519
- if (fs.existsSync(specPath))
520
- return specPath;
521
- // Try alternate naming
522
- const altPath = path.join(projectRoot, 'codev', 'specs', `${state.id}-${state.title}.md`);
523
- if (fs.existsSync(altPath))
524
- return altPath;
152
+ export async function done(projectRoot, projectId) {
153
+ const statusPath = findStatusPath(projectRoot, projectId);
154
+ if (!statusPath) {
155
+ throw new Error(`Project ${projectId} not found.`);
525
156
  }
526
- else if (gateId === 'plan_approval') {
527
- // Look for plan file
528
- const planPath = path.join(projectRoot, 'codev', 'plans', `${projectDir}.md`);
529
- if (fs.existsSync(planPath))
530
- return planPath;
531
- const altPath = path.join(projectRoot, 'codev', 'plans', `${state.id}-${state.title}.md`);
532
- if (fs.existsSync(altPath))
533
- return altPath;
534
- }
535
- else if (gateId === 'review_approval') {
536
- // Look for review file
537
- const reviewPath = path.join(projectRoot, 'codev', 'reviews', `${projectDir}.md`);
538
- if (fs.existsSync(reviewPath))
539
- return reviewPath;
157
+ let state = readState(statusPath);
158
+ const protocol = loadProtocol(projectRoot, state.protocol);
159
+ const checks = getPhaseChecks(protocol, state.phase);
160
+ // Run checks first
161
+ if (Object.keys(checks).length > 0) {
162
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
163
+ console.log('');
164
+ console.log(chalk.bold('RUNNING CHECKS...'));
165
+ const results = await runPhaseChecks(checks, projectRoot, checkEnv);
166
+ console.log(formatCheckResults(results));
167
+ if (!allChecksPassed(results)) {
168
+ console.log('');
169
+ console.log(chalk.red('CHECKS FAILED. Cannot advance.'));
170
+ console.log(`\n Fix the failures and try again.`);
171
+ process.exit(1);
172
+ }
540
173
  }
541
- return null;
542
- }
543
- /**
544
- * Display gate context - show what artifact was created and key information
545
- */
546
- function displayGateContext(gateId, state, projectRoot) {
547
- const artifactPath = getGateArtifact(gateId, state, projectRoot);
548
- console.log('');
549
- console.log(chalk.cyan('─'.repeat(50)));
550
- console.log(chalk.cyan(' CONTEXT FOR REVIEW'));
551
- console.log(chalk.cyan('─'.repeat(50)));
552
- if (artifactPath) {
553
- console.log(chalk.white(` Artifact: ${artifactPath}`));
174
+ // For build_verify phases: mark build as complete for verification
175
+ if (isBuildVerify(protocol, state.phase) && !state.build_complete) {
176
+ state.build_complete = true;
177
+ writeState(statusPath, state);
554
178
  console.log('');
555
- // Read and show first ~30 lines of the artifact
556
- try {
557
- const content = fs.readFileSync(artifactPath, 'utf-8');
558
- const lines = content.split('\n');
559
- const preview = lines.slice(0, 30).join('\n');
560
- console.log(chalk.gray(' ┌' + '─'.repeat(46) + '┐'));
561
- for (const line of preview.split('\n')) {
562
- console.log(chalk.gray(' │ ') + line.slice(0, 44));
563
- }
564
- if (lines.length > 30) {
565
- console.log(chalk.gray(` │ ... (${lines.length - 30} more lines)`));
179
+ console.log(chalk.green('BUILD COMPLETE. Ready for verification.'));
180
+ console.log(`\n Run: porch next ${state.id} (to get verification tasks)`);
181
+ return;
182
+ }
183
+ // Enforce 3-way verification for build_verify phases
184
+ const verifyConfig = getVerifyConfig(protocol, state.phase);
185
+ if (verifyConfig) {
186
+ const projectDir = getProjectDir(projectRoot, state.id, state.title);
187
+ const phase = state.current_plan_phase || state.phase;
188
+ const missingModels = [];
189
+ for (const model of verifyConfig.models) {
190
+ // Look for any review file for this model+phase (any iteration)
191
+ const pattern = path.join(projectDir, `${state.id}-${phase}-iter*-${model}.txt`);
192
+ const matches = globSync(pattern);
193
+ if (matches.length === 0) {
194
+ missingModels.push(model);
566
195
  }
567
- console.log(chalk.gray(' └' + '─'.repeat(46) + '┘'));
568
- console.log('');
569
- console.log(chalk.white(` To view full document: cat ${artifactPath}`));
570
196
  }
571
- catch (e) {
572
- console.log(chalk.yellow(` Could not read artifact: ${e}`));
197
+ if (missingModels.length > 0) {
198
+ console.log('');
199
+ console.log(chalk.red('VERIFICATION REQUIRED'));
200
+ console.log(`\n 3-way review not completed. Missing: ${missingModels.join(', ')}`);
201
+ console.log(`\n Run: porch next ${state.id} (to trigger verification)`);
202
+ process.exit(1);
573
203
  }
574
204
  }
575
- else {
576
- console.log(chalk.yellow(' No artifact file found for this gate.'));
577
- console.log(chalk.yellow(' This may indicate Claude did not produce the expected output.'));
578
- // Show what we expected
579
- if (gateId === 'specify_approval') {
580
- console.log(chalk.gray(` Expected: codev/specs/${state.id}-${state.title}.md`));
581
- }
582
- else if (gateId === 'plan_approval') {
583
- console.log(chalk.gray(` Expected: codev/plans/${state.id}-${state.title}.md`));
205
+ // Check for gate
206
+ const gate = getPhaseGate(protocol, state.phase);
207
+ if (gate && state.gates[gate]?.status !== 'approved') {
208
+ console.log('');
209
+ console.log(chalk.yellow(`GATE REQUIRED: ${gate}`));
210
+ console.log(`\n Run: porch gate ${state.id}`);
211
+ console.log(' Wait for human approval before advancing.');
212
+ return;
213
+ }
214
+ // For phased protocols: plan phase advancement requires 3-way review.
215
+ // The isBuildVerify block above already marked build_complete=true.
216
+ // Redirect to porch next for verification (3-way review + unanimous verdict).
217
+ if (isPhased(protocol, state.phase) && state.plan_phases.length > 0) {
218
+ const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
219
+ if (currentPlanPhase && !allPlanPhasesComplete(state.plan_phases)) {
220
+ console.log('');
221
+ console.log(chalk.green('BUILD COMPLETE. Ready for 3-way review.'));
222
+ console.log(`\n Run: porch next ${state.id} (to trigger verification)`);
223
+ return;
584
224
  }
585
225
  }
586
- console.log('');
226
+ // Advance to next protocol phase
227
+ advanceProtocolPhase(state, protocol, statusPath);
587
228
  }
588
- /**
589
- * Run the protocol loop for a project (Interactive REPL mode)
590
- */
591
- export async function run(projectId, options = {}) {
592
- const projectRoot = findProjectRoot();
593
- // Find status file
594
- const statusFilePath = findStatusFile(projectRoot, projectId);
595
- if (!statusFilePath) {
596
- throw new Error(`Status file not found for project: ${projectId}\n` +
597
- `Run: porch init <protocol> ${projectId} <project-name>`);
598
- }
599
- // Read state and load protocol
600
- const state = readState(statusFilePath);
601
- if (!state) {
602
- throw new Error(`Could not read state from: ${statusFilePath}`);
229
+ function advanceProtocolPhase(state, protocol, statusPath) {
230
+ const nextPhase = getNextPhase(protocol, state.phase);
231
+ if (!nextPhase) {
232
+ state.phase = 'complete';
233
+ writeState(statusPath, state);
234
+ console.log('');
235
+ console.log(chalk.green.bold('🎉 PROTOCOL COMPLETE'));
236
+ console.log(`\n Project ${state.id} has completed the ${state.protocol} protocol.`);
237
+ return;
603
238
  }
604
- const protocol = loadProtocol(state.protocol, projectRoot);
605
- const maxIterations = protocol.config?.max_iterations || 100;
606
- // Create notifier for this project (desktop notifications for important events)
607
- const notifier = createNotifier(projectId, { desktop: true });
608
- // Create REPL interface
609
- const rl = createRepl();
610
- console.log('');
611
- console.log(chalk.green('═'.repeat(50)));
612
- console.log(chalk.green(` PORCH - Protocol Orchestrator`));
613
- console.log(chalk.green('═'.repeat(50)));
614
- console.log(chalk.blue(` Project: ${state.id} - ${state.title}`));
615
- console.log(chalk.blue(` Protocol: ${state.protocol}`));
616
- console.log(chalk.blue(` State: ${state.current_state}`));
617
- console.log(chalk.green('═'.repeat(50)));
618
- console.log('');
619
- console.log(chalk.gray('Type "help" for commands, "quit" to exit'));
620
- console.log('');
621
- let currentState = state;
622
- // Extract plan phases if not already done and we're past planning
623
- if (!currentState.plan_phases || currentState.plan_phases.length === 0) {
624
- const planFile = findPlanFile(projectRoot, projectId, currentState.title);
625
- if (planFile) {
626
- try {
627
- const planPhases = extractPhasesFromPlanFile(planFile);
628
- currentState = setPlanPhases(currentState, planPhases);
629
- await writeState(statusFilePath, currentState);
630
- console.log(chalk.blue(`[porch] Extracted ${planPhases.length} phases from plan`));
631
- for (const phase of planPhases) {
632
- console.log(chalk.blue(` - ${phase.id}: ${phase.title}`));
633
- }
634
- }
635
- catch (e) {
636
- console.log(chalk.yellow(`[porch] Could not extract plan phases: ${e}`));
239
+ state.phase = nextPhase.id;
240
+ state.build_complete = false;
241
+ state.iteration = 1;
242
+ // If entering a phased phase (implement), extract plan phases
243
+ if (isPhased(protocol, nextPhase.id)) {
244
+ const planPath = findPlanFile(process.cwd(), state.id, state.title);
245
+ if (planPath) {
246
+ state.plan_phases = extractPhasesFromFile(planPath);
247
+ // extractPhasesFromFile already marks first phase as in_progress
248
+ if (state.plan_phases.length > 0) {
249
+ state.current_plan_phase = state.plan_phases[0].id;
637
250
  }
638
251
  }
639
252
  }
640
- for (let iteration = currentState.iteration; iteration <= maxIterations; iteration++) {
641
- console.log(chalk.blue(''.repeat(40)));
642
- console.log(chalk.blue(`[porch] Iteration ${iteration}`));
643
- console.log(chalk.blue('━'.repeat(40)));
644
- // Fresh read of state each iteration (Ralph pattern)
645
- currentState = readState(statusFilePath) || currentState;
646
- console.log(chalk.blue(`[porch] Current state: ${currentState.current_state}`));
647
- // Parse state into phase and substate
648
- const { phaseId, planPhaseId, substate } = parsePlanPhaseFromState(currentState.current_state);
649
- // Re-attempt plan phase extraction if entering a phased phase without plan phases
650
- // This handles the case where porch started during Specify phase (no plan file yet)
651
- if (isPhasedPhase(protocol, phaseId) && (!currentState.plan_phases || currentState.plan_phases.length === 0)) {
652
- const planFile = findPlanFile(projectRoot, projectId, currentState.title);
653
- if (planFile) {
654
- try {
655
- const planPhases = extractPhasesFromPlanFile(planFile);
656
- currentState = setPlanPhases(currentState, planPhases);
657
- await writeState(statusFilePath, currentState);
658
- console.log(chalk.blue(`[porch] Late discovery: Extracted ${planPhases.length} phases from plan`));
659
- for (const phase of planPhases) {
660
- console.log(chalk.blue(` - ${phase.id}: ${phase.title}`));
661
- }
662
- }
663
- catch (e) {
664
- console.log(chalk.yellow(`[porch] Could not extract plan phases: ${e}`));
665
- }
666
- }
667
- else {
668
- console.log(chalk.yellow(`[porch] Warning: Entering phased phase '${phaseId}' but no plan file found`));
669
- }
670
- }
671
- // Check if terminal phase
672
- if (isTerminalPhase(protocol, phaseId)) {
673
- console.log(chalk.green('━'.repeat(40)));
674
- console.log(chalk.green(`[porch] ${state.protocol} loop COMPLETE`));
675
- console.log(chalk.green(`[porch] Project ${projectId} finished all phases`));
676
- console.log(chalk.green('━'.repeat(40)));
677
- rl.close();
678
- return;
679
- }
680
- // Check if there's a pending gate from a previous iteration (step already executed)
681
- const pendingGateInfo = getGateForState(protocol, currentState.current_state);
682
- if (pendingGateInfo) {
683
- const { gateId } = pendingGateInfo;
684
- // Check if gate is already approved
685
- if (currentState.gates[gateId]?.status === 'passed') {
686
- // Gate approved - proceed to next state
687
- const nextState = getGateNextState(protocol, phaseId);
688
- if (nextState) {
689
- console.log(chalk.green(`[porch] Gate ${gateId} passed! Proceeding to ${nextState}`));
690
- await notifier.gateApproved(gateId);
691
- // Reset any consultation attempts for the gated state
692
- currentState = resetConsultationAttempts(currentState, currentState.current_state);
693
- // If entering a phased phase, start with first plan phase
694
- if (isPhasedPhase(protocol, nextState.split(':')[0]) && currentState.plan_phases?.length) {
695
- const firstPlanPhase = currentState.plan_phases[0];
696
- currentState = updateState(currentState, `${nextState.split(':')[0]}:${firstPlanPhase.id}`);
697
- currentState = updatePhaseStatus(currentState, firstPlanPhase.id, 'in_progress');
698
- }
699
- else {
700
- currentState = updateState(currentState, nextState);
701
- }
702
- await writeState(statusFilePath, currentState);
703
- continue; // Start next iteration with new state
704
- }
705
- }
706
- else if (currentState.gates[gateId]?.requested_at) {
707
- // Gate requested but not approved - prompt user interactively
708
- console.log('');
709
- console.log(chalk.yellow('═'.repeat(50)));
710
- console.log(chalk.yellow(` GATE PENDING: ${gateId}`));
711
- console.log(chalk.yellow('═'.repeat(50)));
712
- console.log(chalk.cyan(` Phase: ${phaseId}`));
713
- console.log(chalk.cyan(` State: ${currentState.current_state}`));
714
- // Show context for the gate - what artifact was created
715
- displayGateContext(gateId, currentState, projectRoot);
716
- // Interactive gate approval loop
717
- let gateHandled = false;
718
- while (!gateHandled) {
719
- const answer = await prompt(rl, chalk.yellow(`Approve ${gateId}? [y/n/help]: `));
720
- switch (answer) {
721
- case 'y':
722
- case 'yes':
723
- case 'approve':
724
- currentState = approveGate(currentState, gateId);
725
- await writeState(statusFilePath, currentState);
726
- console.log(chalk.green(`✓ Gate ${gateId} approved`));
727
- gateHandled = true;
728
- break;
729
- case 'n':
730
- case 'no':
731
- console.log(chalk.yellow('Gate not approved. Staying in current state.'));
732
- console.log(chalk.gray('Type "quit" to exit or wait for changes.'));
733
- break;
734
- case 'status':
735
- displayStatus(currentState, protocol);
736
- break;
737
- case 'tail': {
738
- const lines = tailLog(20);
739
- if (lines.length > 0) {
740
- console.log(chalk.gray('─'.repeat(50)));
741
- for (const line of lines) {
742
- console.log(line);
743
- }
744
- console.log(chalk.gray('─'.repeat(50)));
745
- }
746
- else {
747
- console.log(chalk.yellow('No log output available'));
748
- }
749
- break;
750
- }
751
- case 'logs':
752
- case 'log': {
753
- const logFile = getCurrentLogFile();
754
- if (logFile) {
755
- console.log(chalk.cyan(`Log file: ${logFile}`));
756
- }
757
- else {
758
- console.log(chalk.yellow('No active log file'));
759
- }
760
- break;
761
- }
762
- case 'cancel':
763
- if (cancelClaude()) {
764
- console.log(chalk.yellow('Claude execution cancelled'));
765
- }
766
- else {
767
- console.log(chalk.gray('No Claude process running'));
768
- }
769
- break;
770
- case 'view':
771
- case 'cat':
772
- case 'show':
773
- // Show full artifact
774
- const artifactPath = getGateArtifact(gateId, currentState, projectRoot);
775
- if (artifactPath) {
776
- console.log(chalk.cyan(`\n─── ${artifactPath} ───\n`));
777
- try {
778
- const content = fs.readFileSync(artifactPath, 'utf-8');
779
- console.log(content);
780
- console.log(chalk.cyan(`\n─── End of ${artifactPath} ───\n`));
781
- }
782
- catch (e) {
783
- console.log(chalk.red(`Error reading file: ${e}`));
784
- }
785
- }
786
- else {
787
- console.log(chalk.yellow('No artifact file found for this gate.'));
788
- }
789
- break;
790
- case 'help':
791
- case '?':
792
- showReplHelp();
793
- break;
794
- case 'quit':
795
- case 'exit':
796
- console.log(chalk.blue('Exiting porch...'));
797
- rl.close();
798
- return;
799
- default:
800
- console.log(chalk.gray('Type "y" to approve, "n" to decline, or "help" for commands'));
801
- }
802
- }
803
- continue;
804
- }
805
- // If gate not yet requested, fall through to execute phase first
806
- }
807
- // Get the current phase definition
808
- const phase = protocol.phases.find(p => p.id === phaseId);
809
- // Show plan phase context if in a phased phase
810
- if (planPhaseId && currentState.plan_phases) {
811
- const planPhase = currentState.plan_phases.find(p => p.id === planPhaseId);
812
- if (planPhase) {
813
- console.log(chalk.cyan(`[phase] IDE Phase: ${phaseId} | Plan Phase: ${planPhase.title}`));
814
- }
815
- else {
816
- console.log(chalk.cyan(`[phase] Phase: ${phaseId}`));
817
- }
818
- }
819
- else {
820
- console.log(chalk.cyan(`[phase] Phase: ${phaseId}`));
821
- }
822
- // Notify phase start
823
- await notifier.phaseStart(phaseId);
824
- // Execute phase
825
- const output = await invokeClaude(protocol, phaseId, currentState, statusFilePath, projectRoot, options);
826
- const signal = extractSignal(output);
827
- // Run phase checks (build/test) if defined
828
- if (phase?.checks && !options.dryRun) {
829
- console.log(chalk.blue(`[porch] Running checks for phase ${phaseId}...`));
830
- const checkResult = await runPhaseChecks(phase, {
831
- cwd: projectRoot,
832
- dryRun: options.dryRun,
833
- });
834
- console.log(formatCheckResults(checkResult));
835
- if (!checkResult.success) {
836
- // Notify about check failure
837
- const failedCheck = checkResult.checks.find(c => !c.success);
838
- await notifier.checkFailed(phaseId, failedCheck?.name || 'build/test', failedCheck?.error || 'Check failed');
839
- // If checks fail, handle based on check configuration
840
- if (checkResult.returnTo) {
841
- console.log(chalk.yellow(`[porch] Checks failed, returning to ${checkResult.returnTo}`));
842
- if (planPhaseId) {
843
- currentState = updateState(currentState, `${checkResult.returnTo}:${planPhaseId}`);
844
- }
845
- else {
846
- currentState = updateState(currentState, checkResult.returnTo);
847
- }
848
- await writeState(statusFilePath, currentState);
849
- continue;
850
- }
851
- // No returnTo means we should retry (already handled in checks)
852
- }
853
- }
854
- // Run consultation if configured for this phase/substate
855
- if (phase?.consultation && hasConsultation(phase) && !options.dryRun && !options.noClaude) {
856
- const consultConfig = phase.consultation;
857
- const currentSubstate = substate || parsePlanPhaseFromState(currentState.current_state).substate;
858
- // Check if consultation is triggered by current substate
859
- if (consultConfig.on === currentSubstate || consultConfig.on === phaseId) {
860
- const maxRounds = consultConfig.max_rounds || 3;
861
- const stateKey = currentState.current_state;
862
- // Get attempt count from state (persisted across porch iterations)
863
- const attemptCount = getConsultationAttempts(currentState, stateKey) + 1;
864
- console.log(chalk.blue(`[porch] Consultation triggered for phase ${phaseId} (attempt ${attemptCount}/${maxRounds})`));
865
- await notifier.consultationStart(phaseId, consultConfig.models || ['gemini', 'codex', 'claude']);
866
- const consultResult = await runConsultationLoop(consultConfig, {
867
- subcommand: consultConfig.type.includes('pr') ? 'pr' : consultConfig.type.includes('spec') ? 'spec' : 'plan',
868
- identifier: projectId,
869
- cwd: projectRoot,
870
- timeout: protocol.config?.consultation_timeout,
871
- dryRun: options.dryRun,
872
- });
873
- console.log(formatConsultationResults(consultResult));
874
- await notifier.consultationComplete(phaseId, consultResult.feedback, consultResult.allApproved);
875
- // If not all approved, track attempt and check for escalation
876
- if (!consultResult.allApproved) {
877
- // Increment attempt count in state (persists across iterations)
878
- currentState = incrementConsultationAttempts(currentState, stateKey);
879
- await writeState(statusFilePath, currentState);
880
- // Check if we've reached max attempts
881
- if (attemptCount >= maxRounds) {
882
- // Create escalation gate - requires human intervention
883
- const escalationGateId = `${phaseId}_consultation_escalation`;
884
- // Check if escalation gate was already approved (human override)
885
- if (currentState.gates[escalationGateId]?.status === 'passed') {
886
- console.log(chalk.green(`[porch] Consultation escalation gate already approved, continuing`));
887
- // Reset attempts and fall through to next state handling
888
- currentState = resetConsultationAttempts(currentState, stateKey);
889
- await writeState(statusFilePath, currentState);
890
- }
891
- else {
892
- console.log(chalk.red(`[porch] Consultation failed after ${attemptCount} attempts - escalating to human`));
893
- console.log(chalk.yellow(`[porch] To override and continue: porch approve ${projectId} ${escalationGateId}`));
894
- // Request human gate if not already requested
895
- if (!currentState.gates[escalationGateId]?.requested_at) {
896
- currentState = requestGateApproval(currentState, escalationGateId);
897
- await writeState(statusFilePath, currentState);
898
- await notifier.gatePending(phaseId, escalationGateId);
899
- }
900
- // Prompt user to approve escalation gate
901
- console.log('');
902
- console.log(chalk.red('═'.repeat(50)));
903
- console.log(chalk.red(` ESCALATION: ${escalationGateId}`));
904
- console.log(chalk.red('═'.repeat(50)));
905
- console.log(chalk.yellow(` Consultation failed after ${attemptCount} attempts`));
906
- console.log('');
907
- let escalationHandled = false;
908
- while (!escalationHandled) {
909
- const answer = await prompt(rl, chalk.red(`Override and continue? [y/n/help]: `));
910
- switch (answer) {
911
- case 'y':
912
- case 'yes':
913
- case 'approve':
914
- case 'override':
915
- currentState = approveGate(currentState, escalationGateId);
916
- currentState = resetConsultationAttempts(currentState, stateKey);
917
- await writeState(statusFilePath, currentState);
918
- console.log(chalk.green(`✓ Escalation gate ${escalationGateId} approved`));
919
- escalationHandled = true;
920
- break;
921
- case 'n':
922
- case 'no':
923
- console.log(chalk.yellow('Escalation not approved. Consultation loop will continue.'));
924
- break;
925
- case 'status':
926
- displayStatus(currentState, protocol);
927
- break;
928
- case 'help':
929
- case '?':
930
- showReplHelp();
931
- break;
932
- case 'quit':
933
- case 'exit':
934
- console.log(chalk.blue('Exiting porch...'));
935
- rl.close();
936
- return;
937
- default:
938
- console.log(chalk.gray('Type "y" to override and continue, "n" to decline, or "help" for commands'));
939
- }
940
- }
941
- continue;
942
- }
943
- }
944
- else {
945
- console.log(chalk.yellow(`[porch] Consultation requested changes (attempt ${attemptCount}/${maxRounds}), continuing for revision`));
946
- // Stay in same state for Claude to revise on next iteration
947
- continue;
948
- }
949
- }
950
- else {
951
- // All approved - reset attempt counter
952
- currentState = resetConsultationAttempts(currentState, stateKey);
953
- await writeState(statusFilePath, currentState);
954
- }
955
- // All approved (or escalation gate passed) - use consultation's next state if defined
956
- if (consultConfig.next) {
957
- if (planPhaseId) {
958
- currentState = updateState(currentState, `${consultConfig.next}:${planPhaseId}`);
959
- }
960
- else {
961
- currentState = updateState(currentState, consultConfig.next);
962
- }
963
- await writeState(statusFilePath, currentState);
964
- continue;
965
- }
966
- }
967
- }
968
- // Check if current state has a gate that should trigger AFTER this step
969
- // This is the gate trigger point - step has executed, now block before transition
970
- const gateInfo = getGateForState(protocol, currentState.current_state);
971
- if (gateInfo) {
972
- const { gateId } = gateInfo;
973
- // Request gate approval if not already requested
974
- if (!currentState.gates[gateId]?.requested_at) {
975
- currentState = requestGateApproval(currentState, gateId);
976
- await writeState(statusFilePath, currentState);
977
- console.log(chalk.yellow(`[porch] Step complete. Gate approval requested: ${gateId}`));
978
- await notifier.gatePending(phaseId, gateId);
979
- }
980
- // Gate not yet approved - prompt user interactively
981
- if (currentState.gates[gateId]?.status !== 'passed') {
982
- console.log('');
983
- console.log(chalk.yellow('═'.repeat(50)));
984
- console.log(chalk.yellow(` GATE PENDING: ${gateId}`));
985
- console.log(chalk.yellow('═'.repeat(50)));
986
- let gateHandled = false;
987
- while (!gateHandled) {
988
- const answer = await prompt(rl, chalk.yellow(`Approve ${gateId}? [y/n/help]: `));
989
- switch (answer) {
990
- case 'y':
991
- case 'yes':
992
- case 'approve':
993
- currentState = approveGate(currentState, gateId);
994
- await writeState(statusFilePath, currentState);
995
- console.log(chalk.green(`✓ Gate ${gateId} approved`));
996
- gateHandled = true;
997
- break;
998
- case 'n':
999
- case 'no':
1000
- console.log(chalk.yellow('Gate not approved. Staying in current state.'));
1001
- break;
1002
- case 'status':
1003
- displayStatus(currentState, protocol);
1004
- break;
1005
- case 'tail': {
1006
- const tailLines = tailLog(20);
1007
- if (tailLines.length > 0) {
1008
- console.log(chalk.gray('─'.repeat(50)));
1009
- for (const line of tailLines) {
1010
- console.log(line);
1011
- }
1012
- console.log(chalk.gray('─'.repeat(50)));
1013
- }
1014
- else {
1015
- console.log(chalk.yellow('No log output available'));
1016
- }
1017
- break;
1018
- }
1019
- case 'logs':
1020
- case 'log': {
1021
- const logPath = getCurrentLogFile();
1022
- if (logPath) {
1023
- console.log(chalk.cyan(`Log file: ${logPath}`));
1024
- }
1025
- else {
1026
- console.log(chalk.yellow('No active log file'));
1027
- }
1028
- break;
1029
- }
1030
- case 'cancel':
1031
- if (cancelClaude()) {
1032
- console.log(chalk.yellow('Claude execution cancelled'));
1033
- }
1034
- else {
1035
- console.log(chalk.gray('No Claude process running'));
1036
- }
1037
- break;
1038
- case 'help':
1039
- case '?':
1040
- showReplHelp();
1041
- break;
1042
- case 'quit':
1043
- case 'exit':
1044
- console.log(chalk.blue('Exiting porch...'));
1045
- rl.close();
1046
- return;
1047
- default:
1048
- console.log(chalk.gray('Type "y" to approve, "n" to decline, or "help" for commands'));
1049
- }
1050
- }
1051
- continue;
1052
- }
1053
- }
1054
- // Determine next state
1055
- let nextState = null;
1056
- if (signal) {
1057
- console.log(chalk.green(`[porch] Signal received: ${signal}`));
1058
- nextState = getSignalNextState(protocol, phaseId, signal);
1059
- }
1060
- if (!nextState) {
1061
- // Check if this is a phased phase (IDE loop)
1062
- if (isPhasedPhase(protocol, phaseId) && currentState.plan_phases?.length) {
1063
- nextState = getNextIDEState(protocol, currentState.current_state, currentState.plan_phases, signal || undefined);
1064
- // Mark current plan phase as complete if moving to next
1065
- if (nextState && planPhaseId) {
1066
- const { planPhaseId: nextPlanPhaseId } = parsePlanPhaseFromState(nextState);
1067
- if (nextPlanPhaseId !== planPhaseId) {
1068
- currentState = updatePhaseStatus(currentState, planPhaseId, 'complete');
1069
- if (nextPlanPhaseId) {
1070
- currentState = updatePhaseStatus(currentState, nextPlanPhaseId, 'in_progress');
1071
- }
1072
- }
1073
- }
1074
- }
1075
- else {
1076
- // Use default transition
1077
- nextState = getDefaultNextState(protocol, currentState.current_state);
253
+ writeState(statusPath, state);
254
+ console.log('');
255
+ console.log(chalk.green(`ADVANCING TO: ${nextPhase.id} - ${nextPhase.name}`));
256
+ // If we just entered implement phase, show phase 1 info and the critical warning
257
+ if (isPhased(protocol, nextPhase.id) && state.plan_phases.length > 0) {
258
+ const firstPhase = state.plan_phases[0];
259
+ const nextPlanPhase = state.plan_phases[1];
260
+ console.log('');
261
+ console.log(chalk.bold(`YOUR TASK: ${firstPhase.id} - "${firstPhase.title}"`));
262
+ // Show phase content from plan
263
+ const planPath = findPlanFile(process.cwd(), state.id, state.title);
264
+ if (planPath) {
265
+ const content = fs.readFileSync(planPath, 'utf-8');
266
+ const phaseContent = getPhaseContent(content, firstPhase.id);
267
+ if (phaseContent) {
268
+ console.log(section('FROM THE PLAN', phaseContent.slice(0, 800)));
1078
269
  }
1079
270
  }
1080
- if (nextState) {
1081
- currentState = updateState(currentState, nextState, signal ? { signal } : undefined);
1082
- await writeState(statusFilePath, currentState);
271
+ console.log('');
272
+ console.log(chalk.red.bold('╔══════════════════════════════════════════════════════════════╗'));
273
+ console.log(chalk.red.bold('║ 🛑 CRITICAL RULES ║'));
274
+ if (nextPlanPhase) {
275
+ console.log(chalk.red.bold(`║ 1. DO NOT start ${nextPlanPhase.id} until you run porch again!`.padEnd(63) + '║'));
1083
276
  }
1084
277
  else {
1085
- console.log(chalk.yellow(`[porch] No transition defined, staying in current state`));
278
+ console.log(chalk.red.bold('║ 1. DO NOT start the next phase until you run porch again! ║'));
1086
279
  }
280
+ console.log(chalk.red.bold('║ 2. Run /compact before starting each new phase ║'));
281
+ console.log(chalk.red.bold('║ 3. When phase complete, run: porch done ' + state.id.padEnd(20) + '║'));
282
+ console.log(chalk.red.bold('╚══════════════════════════════════════════════════════════════╝'));
1087
283
  }
1088
- rl.close();
1089
- throw new Error(`Max iterations (${maxIterations}) reached!`);
284
+ console.log(`\n Run: porch status ${state.id}`);
1090
285
  }
1091
286
  /**
1092
- * Approve a gate
287
+ * porch gate <id>
288
+ * Requests human approval for current gate.
1093
289
  */
1094
- export async function approve(projectId, gateId) {
1095
- const projectRoot = findProjectRoot();
1096
- const statusFilePath = findStatusFile(projectRoot, projectId);
1097
- if (!statusFilePath) {
1098
- throw new Error(`Status file not found for project: ${projectId}`);
290
+ export async function gate(projectRoot, projectId) {
291
+ const statusPath = findStatusPath(projectRoot, projectId);
292
+ if (!statusPath) {
293
+ throw new Error(`Project ${projectId} not found.`);
1099
294
  }
1100
- const state = readState(statusFilePath);
1101
- if (!state) {
1102
- throw new Error(`Could not read state from: ${statusFilePath}`);
295
+ const state = readState(statusPath);
296
+ const protocol = loadProtocol(projectRoot, state.protocol);
297
+ const gateName = getPhaseGate(protocol, state.phase);
298
+ if (!gateName) {
299
+ console.log(chalk.dim('No gate required for this phase.'));
300
+ console.log(`\n Run: porch done ${state.id}`);
301
+ return;
1103
302
  }
1104
- const updatedState = approveGate(state, gateId);
1105
- await writeState(statusFilePath, updatedState);
1106
- console.log(chalk.green(`[porch] Approved: ${gateId}`));
1107
- }
1108
- /**
1109
- * Show project status
1110
- */
1111
- export async function status(projectId) {
1112
- const projectRoot = findProjectRoot();
1113
- if (projectId) {
1114
- // Show specific project
1115
- const statusFilePath = findStatusFile(projectRoot, projectId);
1116
- if (!statusFilePath) {
1117
- throw new Error(`Status file not found for project: ${projectId}`);
1118
- }
1119
- const state = readState(statusFilePath);
1120
- if (!state) {
1121
- throw new Error(`Could not read state from: ${statusFilePath}`);
1122
- }
1123
- console.log(chalk.blue(`[porch] Status for project ${projectId}:`));
1124
- console.log('');
1125
- console.log(` ID: ${state.id}`);
1126
- console.log(` Title: ${state.title}`);
1127
- console.log(` Protocol: ${state.protocol}`);
1128
- console.log(` State: ${state.current_state}`);
1129
- console.log(` Iteration: ${state.iteration}`);
1130
- console.log(` Started: ${state.started_at}`);
1131
- console.log(` Updated: ${state.last_updated}`);
1132
- console.log('');
1133
- if (Object.keys(state.gates).length > 0) {
1134
- console.log(' Gates:');
1135
- for (const [gateId, gateStatus] of Object.entries(state.gates)) {
1136
- const icon = gateStatus.status === 'passed' ? '✓' : gateStatus.status === 'failed' ? '✗' : '⏳';
1137
- console.log(` ${icon} ${gateId}: ${gateStatus.status}`);
1138
- }
1139
- console.log('');
1140
- }
1141
- if (state.plan_phases && state.plan_phases.length > 0) {
1142
- console.log(' Plan Phases:');
1143
- for (const phase of state.plan_phases) {
1144
- const phaseStatus = state.phases[phase.id]?.status || 'pending';
1145
- const icon = phaseStatus === 'complete' ? '✓' : phaseStatus === 'in_progress' ? '🔄' : '○';
1146
- console.log(` ${icon} ${phase.id}: ${phase.title}`);
1147
- }
1148
- }
303
+ // Mark gate as requested
304
+ if (!state.gates[gateName]) {
305
+ state.gates[gateName] = { status: 'pending' };
1149
306
  }
1150
- else {
1151
- // Show all projects
1152
- const projects = findProjects(projectRoot);
1153
- const executions = findExecutions(projectRoot);
1154
- if (projects.length === 0 && executions.length === 0) {
1155
- console.log(chalk.yellow('[porch] No projects found'));
1156
- return;
1157
- }
1158
- console.log(chalk.blue('[porch] Projects:'));
1159
- for (const { id, path: statusPath } of projects) {
1160
- const state = readState(statusPath);
1161
- if (state) {
1162
- const pendingGates = Object.entries(state.gates)
1163
- .filter(([, g]) => g.status === 'pending' && g.requested_at)
1164
- .map(([id]) => id);
1165
- const gateStr = pendingGates.length > 0 ? chalk.yellow(` [${pendingGates.join(', ')}]`) : '';
1166
- console.log(` ${id} ${state.title} - ${state.current_state}${gateStr}`);
1167
- }
1168
- }
1169
- if (executions.length > 0) {
307
+ if (!state.gates[gateName].requested_at) {
308
+ state.gates[gateName].requested_at = new Date().toISOString();
309
+ writeState(statusPath, state);
310
+ }
311
+ console.log('');
312
+ console.log(chalk.bold(`GATE: ${gateName}`));
313
+ console.log('');
314
+ // Show relevant artifact and open it for review
315
+ const artifact = getArtifactForPhase(projectRoot, state);
316
+ if (artifact) {
317
+ const fullPath = path.join(projectRoot, artifact);
318
+ if (fs.existsSync(fullPath)) {
319
+ console.log(` Artifact: ${artifact}`);
1170
320
  console.log('');
1171
- console.log(chalk.blue('[porch] Executions:'));
1172
- for (const { protocol, id, path: statusPath } of executions) {
1173
- const state = readState(statusPath);
1174
- if (state) {
1175
- console.log(` ${protocol}/${id} - ${state.current_state}`);
1176
- }
1177
- }
321
+ console.log(chalk.cyan(' Opening artifact for human review...'));
322
+ // Use af open to display in annotation viewer
323
+ const { spawn } = await import('node:child_process');
324
+ spawn('af', ['open', fullPath], {
325
+ stdio: 'inherit',
326
+ detached: true
327
+ }).unref();
1178
328
  }
1179
329
  }
330
+ console.log('');
331
+ console.log(chalk.yellow(' Human approval required. STOP and wait.'));
332
+ console.log(' Do not proceed until gate is approved.');
333
+ console.log('');
334
+ console.log(chalk.bold('STATUS: WAITING FOR HUMAN APPROVAL'));
335
+ console.log('');
336
+ console.log(chalk.dim(` To approve: porch approve ${state.id} ${gateName}`));
337
+ console.log('');
1180
338
  }
1181
339
  /**
1182
- * List available protocols
340
+ * porch approve <id> <gate> --a-human-explicitly-approved-this
341
+ * Human approves a gate. Requires explicit flag to prevent automated approvals.
1183
342
  */
1184
- export async function list() {
1185
- const projectRoot = findProjectRoot();
1186
- const protocols = listProtocols(projectRoot);
1187
- if (protocols.length === 0) {
1188
- console.log(chalk.yellow('[porch] No protocols found'));
343
+ export async function approve(projectRoot, projectId, gateName, hasHumanFlag) {
344
+ const statusPath = findStatusPath(projectRoot, projectId);
345
+ if (!statusPath) {
346
+ throw new Error(`Project ${projectId} not found.`);
347
+ }
348
+ const state = readState(statusPath);
349
+ if (!state.gates[gateName]) {
350
+ const knownGates = Object.keys(state.gates).join(', ');
351
+ throw new Error(`Unknown gate: ${gateName}\nKnown gates: ${knownGates || 'none'}`);
352
+ }
353
+ if (state.gates[gateName].status === 'approved') {
354
+ console.log(chalk.yellow(`Gate ${gateName} is already approved.`));
1189
355
  return;
1190
356
  }
1191
- console.log(chalk.blue('[porch] Available protocols:'));
1192
- for (const name of protocols) {
1193
- try {
1194
- const protocol = loadProtocol(name, projectRoot);
1195
- console.log(` - ${name}: ${protocol.description}`);
1196
- }
1197
- catch {
1198
- console.log(` - ${name}: (error loading)`);
357
+ // Require explicit human flag
358
+ if (!hasHumanFlag) {
359
+ console.log('');
360
+ console.log(chalk.red('ERROR: Human approval required.'));
361
+ console.log('');
362
+ console.log(' To approve, please run:');
363
+ console.log('');
364
+ console.log(chalk.cyan(` porch approve ${projectId} ${gateName} --a-human-explicitly-approved-this`));
365
+ console.log('');
366
+ process.exit(1);
367
+ }
368
+ // Run phase checks before approving
369
+ const protocol = loadProtocol(projectRoot, state.protocol);
370
+ const checks = getPhaseChecks(protocol, state.phase);
371
+ if (Object.keys(checks).length > 0) {
372
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
373
+ console.log('');
374
+ console.log(chalk.bold('RUNNING CHECKS...'));
375
+ const results = await runPhaseChecks(checks, projectRoot, checkEnv);
376
+ console.log(formatCheckResults(results));
377
+ if (!allChecksPassed(results)) {
378
+ console.log('');
379
+ console.log(chalk.red('CHECKS FAILED. Cannot approve gate.'));
380
+ console.log(`\n Fix the failures and try again.`);
381
+ process.exit(1);
1199
382
  }
1200
383
  }
1201
- }
1202
- /**
1203
- * Show protocol definition
1204
- */
1205
- export async function show(protocolName) {
1206
- const projectRoot = findProjectRoot();
1207
- const protocol = loadProtocol(protocolName, projectRoot);
1208
- console.log(chalk.blue(`[porch] Protocol: ${protocolName}`));
384
+ state.gates[gateName].status = 'approved';
385
+ state.gates[gateName].approved_at = new Date().toISOString();
386
+ writeState(statusPath, state);
387
+ console.log('');
388
+ console.log(chalk.green(`Gate ${gateName} approved.`));
389
+ console.log(`\n Run: porch done ${state.id} (to advance)`);
1209
390
  console.log('');
1210
- console.log(JSON.stringify(protocol, null, 2));
1211
391
  }
1212
392
  /**
1213
- * Show pending gates across all projects
1214
- */
1215
- export async function pending() {
1216
- const projectRoot = findProjectRoot();
1217
- const gates = findPendingGates(projectRoot);
1218
- if (gates.length === 0) {
1219
- console.log(chalk.green('[porch] No pending gates'));
393
+ * porch init <protocol> <id> <name>
394
+ * Initialize a new project.
395
+ *
396
+ * Idempotent: if status.yaml already exists, preserves it and reports
397
+ * current state. This supports `af spawn --resume` where the builder
398
+ * may re-run `porch init` after a session restart.
399
+ */
400
+ export async function init(projectRoot, protocolName, projectId, projectName) {
401
+ const protocol = loadProtocol(projectRoot, protocolName);
402
+ const statusPath = getStatusPath(projectRoot, projectId, projectName);
403
+ // If status.yaml already exists, preserve it (idempotent for resume)
404
+ if (fs.existsSync(statusPath)) {
405
+ const existingState = readState(statusPath);
406
+ console.log('');
407
+ console.log(chalk.yellow(`Project ${projectId}-${projectName} already exists. Preserving existing state.`));
408
+ console.log(` Protocol: ${existingState.protocol}`);
409
+ console.log(` Current phase: ${existingState.phase}`);
410
+ if (existingState.current_plan_phase) {
411
+ console.log(` Plan phase: ${existingState.current_plan_phase}`);
412
+ }
413
+ console.log(`\n Run: porch next ${projectId}`);
414
+ console.log('');
1220
415
  return;
1221
416
  }
1222
- console.log(chalk.yellow('[porch] Pending gates:'));
1223
- for (const gate of gates) {
1224
- const requestedAt = gate.requestedAt ? ` (requested ${gate.requestedAt})` : '';
1225
- console.log(` ${gate.projectId}: ${gate.gateId}${requestedAt}`);
1226
- console.log(` → porch approve ${gate.projectId} ${gate.gateId}`);
417
+ // Also check if a project with this ID exists under a different name
418
+ const existingPath = findStatusPath(projectRoot, projectId);
419
+ if (existingPath) {
420
+ const existingState = readState(existingPath);
421
+ console.log('');
422
+ console.log(chalk.yellow(`Project ${projectId} already exists (as ${existingState.id}-${existingState.title}). Preserving existing state.`));
423
+ console.log(` Protocol: ${existingState.protocol}`);
424
+ console.log(` Current phase: ${existingState.phase}`);
425
+ if (existingState.current_plan_phase) {
426
+ console.log(` Plan phase: ${existingState.current_plan_phase}`);
427
+ }
428
+ console.log(`\n Run: porch next ${projectId}`);
429
+ console.log('');
430
+ return;
1227
431
  }
432
+ const state = createInitialState(protocol, projectId, projectName, projectRoot);
433
+ writeState(statusPath, state);
434
+ console.log('');
435
+ console.log(chalk.green(`Project initialized: ${projectId}-${projectName}`));
436
+ console.log(` Protocol: ${protocolName}`);
437
+ console.log(` Starting phase: ${state.phase}`);
438
+ console.log(`\n Run: porch status ${projectId}`);
439
+ console.log('');
1228
440
  }
1229
441
  // ============================================================================
1230
- // Auto-Detection
442
+ // Helpers
1231
443
  // ============================================================================
1232
- /**
1233
- * Auto-detect project ID from current directory
1234
- *
1235
- * Detection methods:
1236
- * 1. Check if cwd is a worktree matching pattern: .builders/<id> or worktrees/<protocol>_<id>_*
1237
- * 2. Check for a single project in codev/projects/
1238
- * 3. Check for .porch-project marker file
1239
- */
1240
- function autoDetectProject() {
1241
- const cwd = process.cwd();
1242
- // Method 1: Check path pattern for builder worktree
1243
- // Pattern: .builders/<id> or .builders/<id>-<name>
1244
- const buildersMatch = cwd.match(/[/\\]\.builders[/\\](\d+)(?:-[^/\\]*)?(?:[/\\]|$)/);
1245
- if (buildersMatch) {
1246
- return buildersMatch[1];
1247
- }
1248
- // Pattern: worktrees/<protocol>_<id>_<name>
1249
- const worktreeMatch = cwd.match(/[/\\]worktrees[/\\]\w+_(\d+)_[^/\\]*(?:[/\\]|$)/);
1250
- if (worktreeMatch) {
1251
- return worktreeMatch[1];
1252
- }
1253
- // Method 2: Check for .porch-project marker file
1254
- const markerPath = path.join(cwd, '.porch-project');
1255
- if (fs.existsSync(markerPath)) {
1256
- const content = fs.readFileSync(markerPath, 'utf-8').trim();
1257
- if (content) {
1258
- return content;
1259
- }
1260
- }
1261
- // Method 3: Check if there's exactly one project in codev/projects/
1262
- try {
1263
- const projectRoot = findProjectRoot();
1264
- const projects = findProjects(projectRoot);
1265
- if (projects.length === 1) {
1266
- return projects[0].id;
1267
- }
1268
- }
1269
- catch {
1270
- // Not in a codev project
444
+ function getInstructions(state, protocol) {
445
+ const phase = state.phase;
446
+ if (isPhased(protocol, phase) && state.plan_phases.length > 0) {
447
+ const current = getCurrentPlanPhase(state.plan_phases);
448
+ if (current) {
449
+ return ` You are implementing ${current.id}: "${current.title}".\n\n Complete the work, then run: porch check ${state.id}`;
450
+ }
451
+ }
452
+ const phaseConfig = getPhaseConfig(protocol, phase);
453
+ return ` You are in the ${phaseConfig?.name || phase} phase.\n\n When complete, run: porch done ${state.id}`;
454
+ }
455
+ function getNextAction(state, protocol) {
456
+ const checks = getPhaseChecks(protocol, state.phase);
457
+ const gate = getPhaseGate(protocol, state.phase);
458
+ if (gate && state.gates[gate]?.status === 'pending' && state.gates[gate]?.requested_at) {
459
+ return chalk.yellow('Wait for human to approve the gate.');
460
+ }
461
+ if (isPhased(protocol, state.phase)) {
462
+ const current = getCurrentPlanPhase(state.plan_phases);
463
+ if (current) {
464
+ return `Implement ${current.title} as specified in the plan.`;
465
+ }
466
+ }
467
+ if (Object.keys(checks).length > 0) {
468
+ return `Complete the phase work, then run: porch check ${state.id}`;
469
+ }
470
+ return `Complete the phase work, then run: porch done ${state.id}`;
471
+ }
472
+ function getArtifactForPhase(projectRoot, state) {
473
+ switch (state.phase) {
474
+ case 'specify':
475
+ return `codev/specs/${state.id}-${state.title}.md`;
476
+ case 'plan':
477
+ return `codev/plans/${state.id}-${state.title}.md`;
478
+ case 'review':
479
+ return `codev/reviews/${state.id}-${state.title}.md`;
480
+ default:
481
+ return null;
1271
482
  }
1272
- return null;
1273
483
  }
1274
- export async function porch(options) {
1275
- const { subcommand, args, dryRun, noClaude, pollInterval, description, worktree } = options;
1276
- switch (subcommand.toLowerCase()) {
1277
- case 'run': {
1278
- let projectId = args[0];
1279
- // Auto-detect project if not provided
1280
- if (!projectId) {
1281
- const detected = autoDetectProject();
1282
- if (!detected) {
1283
- throw new Error('Usage: porch run <project-id>\n' +
1284
- 'Or run from a project worktree to auto-detect.');
1285
- }
1286
- projectId = detected;
1287
- console.log(chalk.blue(`[porch] Auto-detected project: ${projectId}`));
1288
- }
1289
- await run(projectId, { dryRun, noClaude, pollInterval });
1290
- break;
1291
- }
1292
- case 'init': {
1293
- if (args.length < 3) {
1294
- throw new Error('Usage: porch init <protocol> <project-id> <project-name>');
1295
- }
1296
- await init(args[0], args[1], args[2], { description, worktree });
1297
- break;
1298
- }
1299
- case 'approve': {
1300
- if (args.length < 2) {
1301
- throw new Error('Usage: porch approve <project-id> <gate-id>');
1302
- }
1303
- await approve(args[0], args[1]);
1304
- break;
1305
- }
1306
- case 'status': {
1307
- await status(args[0]);
1308
- break;
1309
- }
1310
- case 'pending': {
1311
- await pending();
1312
- break;
484
+ // ============================================================================
485
+ // CLI
486
+ // ============================================================================
487
+ export async function cli(args) {
488
+ const [command, ...rest] = args;
489
+ const projectRoot = process.cwd();
490
+ // Auto-detect project ID for commands that need it
491
+ function getProjectId(provided) {
492
+ const { id, source } = resolveProjectId(provided, process.cwd(), projectRoot);
493
+ if (source === 'cwd') {
494
+ console.log(chalk.dim(`[auto-detected project from worktree: ${id}]`));
1313
495
  }
1314
- case 'list':
1315
- case 'list-protocols': {
1316
- await list();
1317
- break;
496
+ else if (source === 'filesystem') {
497
+ console.log(chalk.dim(`[auto-detected project: ${id}]`));
1318
498
  }
1319
- case 'show':
1320
- case 'show-protocol': {
1321
- if (args.length < 1) {
1322
- throw new Error('Usage: porch show <protocol>');
499
+ return id;
500
+ }
501
+ try {
502
+ switch (command) {
503
+ case 'next': {
504
+ const { next: porchNext } = await import('./next.js');
505
+ const result = await porchNext(projectRoot, getProjectId(rest[0]));
506
+ console.log(JSON.stringify(result, null, 2));
507
+ break;
1323
508
  }
1324
- await show(args[0]);
1325
- break;
509
+ case 'run':
510
+ console.error("Error: 'porch run' has been removed. Use 'porch next <id>' instead.");
511
+ console.error("See: porch --help");
512
+ process.exit(1);
513
+ break;
514
+ case 'status':
515
+ await status(projectRoot, getProjectId(rest[0]));
516
+ break;
517
+ case 'check':
518
+ await check(projectRoot, getProjectId(rest[0]));
519
+ break;
520
+ case 'done':
521
+ await done(projectRoot, getProjectId(rest[0]));
522
+ break;
523
+ case 'gate':
524
+ await gate(projectRoot, getProjectId(rest[0]));
525
+ break;
526
+ case 'approve':
527
+ if (!rest[0] || !rest[1])
528
+ throw new Error('Usage: porch approve <id> <gate> --a-human-explicitly-approved-this');
529
+ const hasHumanFlag = rest.includes('--a-human-explicitly-approved-this');
530
+ await approve(projectRoot, rest[0], rest[1], hasHumanFlag);
531
+ break;
532
+ case 'init':
533
+ if (!rest[0] || !rest[1] || !rest[2]) {
534
+ throw new Error('Usage: porch init <protocol> <id> <name>');
535
+ }
536
+ await init(projectRoot, rest[0], rest[1], rest[2]);
537
+ break;
538
+ default:
539
+ console.log('porch - Protocol Orchestrator');
540
+ console.log('');
541
+ console.log('Commands:');
542
+ console.log(' next [id] Emit next tasks as JSON (planner mode)');
543
+ console.log(' status [id] Show current state and instructions');
544
+ console.log(' check [id] Run checks for current phase');
545
+ console.log(' done [id] Signal build complete (validates checks, advances)');
546
+ console.log(' gate [id] Request human approval');
547
+ console.log(' approve <id> <gate> --a-human-explicitly-approved-this');
548
+ console.log(' init <protocol> <id> <name> Initialize a new project');
549
+ console.log('');
550
+ console.log('Project ID is auto-detected from worktree path or when exactly one project exists.');
551
+ console.log('');
552
+ process.exit(command && command !== '--help' && command !== '-h' ? 1 : 0);
1326
553
  }
1327
- default:
1328
- throw new Error(`Unknown subcommand: ${subcommand}\n` +
1329
- 'Valid subcommands: run, init, approve, status, pending, list, show');
554
+ }
555
+ catch (err) {
556
+ console.error(chalk.red(`Error: ${err.message}`));
557
+ process.exit(1);
1330
558
  }
1331
559
  }
1332
560
  //# sourceMappingURL=index.js.map