@cluesmith/codev 2.0.0-rc.4 → 2.0.0-rc.40

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 (316) hide show
  1. package/bin/porch.js +6 -35
  2. package/dashboard/dist/assets/index-BLqoFC1H.js +120 -0
  3. package/dashboard/dist/assets/index-BLqoFC1H.js.map +1 -0
  4. package/dashboard/dist/assets/index-CXwnJkPh.css +32 -0
  5. package/dashboard/dist/index.html +13 -0
  6. package/dist/agent-farm/cli.d.ts.map +1 -1
  7. package/dist/agent-farm/cli.js +93 -64
  8. package/dist/agent-farm/cli.js.map +1 -1
  9. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/architect.js +13 -6
  11. package/dist/agent-farm/commands/architect.js.map +1 -1
  12. package/dist/agent-farm/commands/attach.d.ts +13 -0
  13. package/dist/agent-farm/commands/attach.d.ts.map +1 -0
  14. package/dist/agent-farm/commands/attach.js +179 -0
  15. package/dist/agent-farm/commands/attach.js.map +1 -0
  16. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/cleanup.js +30 -3
  18. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  19. package/dist/agent-farm/commands/consult.js +1 -1
  20. package/dist/agent-farm/commands/consult.js.map +1 -1
  21. package/dist/agent-farm/commands/index.d.ts +2 -2
  22. package/dist/agent-farm/commands/index.d.ts.map +1 -1
  23. package/dist/agent-farm/commands/index.js +2 -2
  24. package/dist/agent-farm/commands/index.js.map +1 -1
  25. package/dist/agent-farm/commands/{util.d.ts → shell.d.ts} +5 -5
  26. package/dist/agent-farm/commands/shell.d.ts.map +1 -0
  27. package/dist/agent-farm/commands/{util.js → shell.js} +23 -36
  28. package/dist/agent-farm/commands/shell.js.map +1 -0
  29. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  30. package/dist/agent-farm/commands/spawn.js +455 -217
  31. package/dist/agent-farm/commands/spawn.js.map +1 -1
  32. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  33. package/dist/agent-farm/commands/start.js +27 -79
  34. package/dist/agent-farm/commands/start.js.map +1 -1
  35. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  36. package/dist/agent-farm/commands/stop.js +79 -10
  37. package/dist/agent-farm/commands/stop.js.map +1 -1
  38. package/dist/agent-farm/commands/tower.d.ts +9 -0
  39. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  40. package/dist/agent-farm/commands/tower.js +54 -18
  41. package/dist/agent-farm/commands/tower.js.map +1 -1
  42. package/dist/agent-farm/db/index.d.ts.map +1 -1
  43. package/dist/agent-farm/db/index.js +15 -0
  44. package/dist/agent-farm/db/index.js.map +1 -1
  45. package/dist/agent-farm/db/schema.d.ts +1 -1
  46. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  47. package/dist/agent-farm/db/schema.js +6 -3
  48. package/dist/agent-farm/db/schema.js.map +1 -1
  49. package/dist/agent-farm/db/types.d.ts +3 -0
  50. package/dist/agent-farm/db/types.d.ts.map +1 -1
  51. package/dist/agent-farm/db/types.js +3 -0
  52. package/dist/agent-farm/db/types.js.map +1 -1
  53. package/dist/agent-farm/hq-connector.d.ts +2 -2
  54. package/dist/agent-farm/hq-connector.js +2 -2
  55. package/dist/agent-farm/servers/dashboard-server.js +435 -131
  56. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  57. package/dist/agent-farm/servers/tower-server.js +430 -17
  58. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  59. package/dist/agent-farm/state.d.ts +4 -10
  60. package/dist/agent-farm/state.d.ts.map +1 -1
  61. package/dist/agent-farm/state.js +30 -31
  62. package/dist/agent-farm/state.js.map +1 -1
  63. package/dist/agent-farm/types.d.ts +48 -0
  64. package/dist/agent-farm/types.d.ts.map +1 -1
  65. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  66. package/dist/agent-farm/utils/config.js +12 -11
  67. package/dist/agent-farm/utils/config.js.map +1 -1
  68. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  69. package/dist/agent-farm/utils/deps.js +0 -16
  70. package/dist/agent-farm/utils/deps.js.map +1 -1
  71. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  72. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  73. package/dist/agent-farm/utils/notifications.js +121 -0
  74. package/dist/agent-farm/utils/notifications.js.map +1 -0
  75. package/dist/agent-farm/utils/server-utils.d.ts +2 -1
  76. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  77. package/dist/agent-farm/utils/server-utils.js +11 -1
  78. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  79. package/dist/agent-farm/utils/shell.d.ts +9 -22
  80. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  81. package/dist/agent-farm/utils/shell.js +34 -34
  82. package/dist/agent-farm/utils/shell.js.map +1 -1
  83. package/dist/agent-farm/utils/terminal-ports.d.ts +1 -1
  84. package/dist/agent-farm/utils/terminal-ports.js +1 -1
  85. package/dist/cli.d.ts.map +1 -1
  86. package/dist/cli.js +5 -54
  87. package/dist/cli.js.map +1 -1
  88. package/dist/commands/adopt.d.ts.map +1 -1
  89. package/dist/commands/adopt.js +39 -4
  90. package/dist/commands/adopt.js.map +1 -1
  91. package/dist/commands/consult/index.d.ts.map +1 -1
  92. package/dist/commands/consult/index.js +63 -3
  93. package/dist/commands/consult/index.js.map +1 -1
  94. package/dist/commands/doctor.d.ts.map +1 -1
  95. package/dist/commands/doctor.js +0 -15
  96. package/dist/commands/doctor.js.map +1 -1
  97. package/dist/commands/init.d.ts.map +1 -1
  98. package/dist/commands/init.js +31 -2
  99. package/dist/commands/init.js.map +1 -1
  100. package/dist/commands/porch/build-counter.d.ts +5 -0
  101. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  102. package/dist/commands/porch/build-counter.js +5 -0
  103. package/dist/commands/porch/build-counter.js.map +1 -0
  104. package/dist/commands/porch/checks.d.ts +16 -29
  105. package/dist/commands/porch/checks.d.ts.map +1 -1
  106. package/dist/commands/porch/checks.js +90 -144
  107. package/dist/commands/porch/checks.js.map +1 -1
  108. package/dist/commands/porch/claude.d.ts +27 -0
  109. package/dist/commands/porch/claude.d.ts.map +1 -0
  110. package/dist/commands/porch/claude.js +107 -0
  111. package/dist/commands/porch/claude.js.map +1 -0
  112. package/dist/commands/porch/index.d.ts +21 -43
  113. package/dist/commands/porch/index.d.ts.map +1 -1
  114. package/dist/commands/porch/index.js +466 -926
  115. package/dist/commands/porch/index.js.map +1 -1
  116. package/dist/commands/porch/plan.d.ts +70 -0
  117. package/dist/commands/porch/plan.d.ts.map +1 -0
  118. package/dist/commands/porch/plan.js +190 -0
  119. package/dist/commands/porch/plan.js.map +1 -0
  120. package/dist/commands/porch/prompts.d.ts +19 -0
  121. package/dist/commands/porch/prompts.d.ts.map +1 -0
  122. package/dist/commands/porch/prompts.js +250 -0
  123. package/dist/commands/porch/prompts.js.map +1 -0
  124. package/dist/commands/porch/protocol.d.ts +59 -0
  125. package/dist/commands/porch/protocol.d.ts.map +1 -0
  126. package/dist/commands/porch/protocol.js +260 -0
  127. package/dist/commands/porch/protocol.js.map +1 -0
  128. package/dist/commands/porch/run.d.ts +40 -0
  129. package/dist/commands/porch/run.d.ts.map +1 -0
  130. package/dist/commands/porch/run.js +893 -0
  131. package/dist/commands/porch/run.js.map +1 -0
  132. package/dist/commands/porch/state.d.ts +23 -112
  133. package/dist/commands/porch/state.d.ts.map +1 -1
  134. package/dist/commands/porch/state.js +81 -699
  135. package/dist/commands/porch/state.js.map +1 -1
  136. package/dist/commands/porch/types.d.ts +72 -173
  137. package/dist/commands/porch/types.d.ts.map +1 -1
  138. package/dist/commands/porch/types.js +2 -1
  139. package/dist/commands/porch/types.js.map +1 -1
  140. package/dist/commands/update.d.ts.map +1 -1
  141. package/dist/commands/update.js +22 -0
  142. package/dist/commands/update.js.map +1 -1
  143. package/dist/lib/scaffold.d.ts +24 -0
  144. package/dist/lib/scaffold.d.ts.map +1 -1
  145. package/dist/lib/scaffold.js +78 -0
  146. package/dist/lib/scaffold.js.map +1 -1
  147. package/dist/terminal/index.d.ts +8 -0
  148. package/dist/terminal/index.d.ts.map +1 -0
  149. package/dist/terminal/index.js +5 -0
  150. package/dist/terminal/index.js.map +1 -0
  151. package/dist/terminal/pty-manager.d.ts +60 -0
  152. package/dist/terminal/pty-manager.d.ts.map +1 -0
  153. package/dist/terminal/pty-manager.js +334 -0
  154. package/dist/terminal/pty-manager.js.map +1 -0
  155. package/dist/terminal/pty-session.d.ts +79 -0
  156. package/dist/terminal/pty-session.d.ts.map +1 -0
  157. package/dist/terminal/pty-session.js +215 -0
  158. package/dist/terminal/pty-session.js.map +1 -0
  159. package/dist/terminal/ring-buffer.d.ts +27 -0
  160. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  161. package/dist/terminal/ring-buffer.js +74 -0
  162. package/dist/terminal/ring-buffer.js.map +1 -0
  163. package/dist/terminal/ws-protocol.d.ts +27 -0
  164. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  165. package/dist/terminal/ws-protocol.js +44 -0
  166. package/dist/terminal/ws-protocol.js.map +1 -0
  167. package/package.json +18 -3
  168. package/skeleton/DEPENDENCIES.md +3 -29
  169. package/skeleton/builders.md +1 -1
  170. package/skeleton/protocol-schema.json +282 -0
  171. package/skeleton/protocols/bugfix/builder-prompt.md +49 -0
  172. package/skeleton/protocols/bugfix/protocol.json +14 -2
  173. package/skeleton/protocols/experiment/builder-prompt.md +47 -0
  174. package/skeleton/protocols/experiment/protocol.json +101 -0
  175. package/skeleton/protocols/maintain/builder-prompt.md +41 -0
  176. package/skeleton/protocols/maintain/prompts/audit.md +111 -0
  177. package/skeleton/protocols/maintain/prompts/clean.md +91 -0
  178. package/skeleton/protocols/maintain/prompts/sync.md +113 -0
  179. package/skeleton/protocols/maintain/prompts/verify.md +110 -0
  180. package/skeleton/protocols/maintain/protocol.json +141 -0
  181. package/skeleton/protocols/maintain/protocol.md +13 -7
  182. package/skeleton/protocols/protocol-schema.json +53 -0
  183. package/skeleton/protocols/spider/builder-prompt.md +53 -0
  184. package/skeleton/protocols/spider/prompts/implement.md +109 -50
  185. package/skeleton/protocols/spider/prompts/specify.md +29 -4
  186. package/skeleton/protocols/spider/protocol.json +96 -154
  187. package/skeleton/protocols/spider/protocol.md +26 -16
  188. package/skeleton/protocols/spider/templates/plan.md +14 -0
  189. package/skeleton/protocols/tick/builder-prompt.md +51 -0
  190. package/skeleton/protocols/tick/protocol.json +7 -2
  191. package/skeleton/resources/commands/agent-farm.md +25 -43
  192. package/skeleton/resources/commands/overview.md +6 -16
  193. package/skeleton/resources/workflow-reference.md +2 -2
  194. package/skeleton/roles/architect.md +152 -315
  195. package/skeleton/roles/builder.md +109 -218
  196. package/skeleton/templates/AGENTS.md +1 -1
  197. package/skeleton/templates/CLAUDE.md +1 -1
  198. package/skeleton/templates/cheatsheet.md +4 -2
  199. package/templates/dashboard/index.html +17 -43
  200. package/templates/dashboard/js/dialogs.js +7 -7
  201. package/templates/dashboard/js/files.js +2 -2
  202. package/templates/dashboard/js/main.js +3 -3
  203. package/templates/dashboard/js/projects.js +3 -3
  204. package/templates/dashboard/js/tabs.js +1 -1
  205. package/templates/dashboard/js/utils.js +22 -87
  206. package/templates/tower.html +497 -26
  207. package/dist/agent-farm/commands/kickoff.d.ts +0 -19
  208. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  209. package/dist/agent-farm/commands/kickoff.js +0 -331
  210. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  211. package/dist/agent-farm/commands/rename.d.ts +0 -13
  212. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  213. package/dist/agent-farm/commands/rename.js +0 -33
  214. package/dist/agent-farm/commands/rename.js.map +0 -1
  215. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  216. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  217. package/dist/agent-farm/commands/tutorial.js +0 -49
  218. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  219. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  220. package/dist/agent-farm/commands/util.js.map +0 -1
  221. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  222. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  223. package/dist/agent-farm/tutorial/index.js +0 -8
  224. package/dist/agent-farm/tutorial/index.js.map +0 -1
  225. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  226. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  227. package/dist/agent-farm/tutorial/prompts.js +0 -147
  228. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  229. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  230. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  231. package/dist/agent-farm/tutorial/runner.js +0 -204
  232. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  233. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  234. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  235. package/dist/agent-farm/tutorial/state.js +0 -89
  236. package/dist/agent-farm/tutorial/state.js.map +0 -1
  237. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  238. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  239. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  240. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  241. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  242. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  243. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  244. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  245. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  246. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  247. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  248. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  249. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  250. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  251. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  252. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  253. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  254. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  255. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  256. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  257. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  258. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  259. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  260. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  261. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  262. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  263. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  264. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  265. package/dist/commands/pcheck/cache.d.ts +0 -48
  266. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  267. package/dist/commands/pcheck/cache.js +0 -170
  268. package/dist/commands/pcheck/cache.js.map +0 -1
  269. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  270. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  271. package/dist/commands/pcheck/evaluator.js +0 -246
  272. package/dist/commands/pcheck/evaluator.js.map +0 -1
  273. package/dist/commands/pcheck/index.d.ts +0 -12
  274. package/dist/commands/pcheck/index.d.ts.map +0 -1
  275. package/dist/commands/pcheck/index.js +0 -249
  276. package/dist/commands/pcheck/index.js.map +0 -1
  277. package/dist/commands/pcheck/parser.d.ts +0 -39
  278. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  279. package/dist/commands/pcheck/parser.js +0 -155
  280. package/dist/commands/pcheck/parser.js.map +0 -1
  281. package/dist/commands/pcheck/types.d.ts +0 -82
  282. package/dist/commands/pcheck/types.d.ts.map +0 -1
  283. package/dist/commands/pcheck/types.js +0 -5
  284. package/dist/commands/pcheck/types.js.map +0 -1
  285. package/dist/commands/porch/consultation.d.ts +0 -56
  286. package/dist/commands/porch/consultation.d.ts.map +0 -1
  287. package/dist/commands/porch/consultation.js +0 -330
  288. package/dist/commands/porch/consultation.js.map +0 -1
  289. package/dist/commands/porch/notifications.d.ts +0 -99
  290. package/dist/commands/porch/notifications.d.ts.map +0 -1
  291. package/dist/commands/porch/notifications.js +0 -223
  292. package/dist/commands/porch/notifications.js.map +0 -1
  293. package/dist/commands/porch/plan-parser.d.ts +0 -38
  294. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  295. package/dist/commands/porch/plan-parser.js +0 -166
  296. package/dist/commands/porch/plan-parser.js.map +0 -1
  297. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  298. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  299. package/dist/commands/porch/protocol-loader.js +0 -249
  300. package/dist/commands/porch/protocol-loader.js.map +0 -1
  301. package/dist/commands/porch/signal-parser.d.ts +0 -88
  302. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  303. package/dist/commands/porch/signal-parser.js +0 -148
  304. package/dist/commands/porch/signal-parser.js.map +0 -1
  305. package/dist/commands/tower.d.ts +0 -16
  306. package/dist/commands/tower.d.ts.map +0 -1
  307. package/dist/commands/tower.js +0 -21
  308. package/dist/commands/tower.js.map +0 -1
  309. package/skeleton/config.json +0 -7
  310. package/skeleton/porch/protocols/bugfix.json +0 -85
  311. package/skeleton/porch/protocols/spider.json +0 -135
  312. package/skeleton/porch/protocols/tick.json +0 -76
  313. package/skeleton/protocols/spider/prompts/defend.md +0 -215
  314. package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
  315. package/templates/dashboard/css/activity.css +0 -151
  316. package/templates/dashboard/js/activity.js +0 -112
@@ -1,1004 +1,544 @@
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 { readState, writeState, createInitialState, findStatusPath, getStatusPath, detectProjectId, } from './state.js';
11
+ import { loadProtocol, getPhaseConfig, getNextPhase, getPhaseChecks, getPhaseGate, isPhased, getPhaseCompletionChecks, } from './protocol.js';
12
+ import { findPlanFile, extractPhasesFromFile, getCurrentPlanPhase, getPhaseContent, advancePlanPhase, allPlanPhasesComplete, } from './plan.js';
13
+ import { runPhaseChecks, formatCheckResults, allChecksPassed, } from './checks.js';
21
14
  // ============================================================================
22
- // Protocol Loading (delegates to protocol-loader.ts)
15
+ // Output Helpers
23
16
  // ============================================================================
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);
31
- }
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;
70
- }
71
- // ============================================================================
72
- // Protocol Helpers
73
- // ============================================================================
74
- /**
75
- * Check if a phase is terminal
76
- */
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 };
90
- }
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;
17
+ function header(text) {
18
+ const line = '═'.repeat(50);
19
+ return `${line}\n ${text}\n${line}`;
99
20
  }
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;
21
+ function section(title, content) {
22
+ return `\n${chalk.bold(title)}:\n${content}`;
132
23
  }
133
24
  // ============================================================================
134
- // Claude Invocation
25
+ // Commands
135
26
  // ============================================================================
136
- // Note: extractSignal is now imported from signal-parser.js
137
27
  /**
138
- * Invoke Claude for a phase
28
+ * porch status <id>
29
+ * Shows current state and prescriptive next steps.
139
30
  */
140
- async function invokeClaude(protocol, phaseId, state, statusFilePath, projectRoot, options) {
141
- const promptContent = loadPrompt(protocol, phaseId, projectRoot);
142
- if (!promptContent) {
143
- console.log(chalk.yellow(`[porch] No prompt file for phase: ${phaseId}`));
144
- return '';
145
- }
146
- if (options.dryRun) {
147
- console.log(chalk.yellow(`[porch] [DRY RUN] Would invoke Claude for phase: ${phaseId}`));
148
- return '';
149
- }
150
- if (options.noClaude) {
151
- console.log(chalk.blue(`[porch] [NO_CLAUDE] Simulating phase: ${phaseId}`));
152
- await new Promise(r => setTimeout(r, 1000));
153
- console.log(chalk.green(`[porch] Simulated completion of phase: ${phaseId}`));
154
- return '';
155
- }
156
- console.log(chalk.cyan(`[phase] Invoking Claude for phase: ${phaseId}`));
157
- const timeout = protocol.config?.claude_timeout || 600000; // 10 minutes default
158
- const fullPrompt = `## Protocol: ${protocol.name}
159
- ## Phase: ${phaseId}
160
- ## Project ID: ${state.id}
161
-
162
- ## Current Status
163
- \`\`\`yaml
164
- state: "${state.current_state}"
165
- iteration: ${state.iteration}
166
- started_at: "${state.started_at}"
167
- \`\`\`
168
-
169
- ## Task
170
- Execute the ${phaseId} phase for project ${state.id} - ${state.title}
171
-
172
- ## Phase Instructions
173
- ${promptContent}
174
-
175
- ## Important
176
- - Project ID: ${state.id}
177
- - Protocol: ${protocol.name}
178
- - Follow the instructions above precisely
179
- - Output <signal>...</signal> tags when you reach completion points
180
- `;
181
- return new Promise((resolve, reject) => {
182
- const args = ['--print', '-p', fullPrompt, '--dangerously-skip-permissions'];
183
- const proc = spawn('claude', args, {
184
- stdio: ['ignore', 'pipe', 'pipe'],
185
- timeout,
186
- });
187
- let output = '';
188
- let stderr = '';
189
- proc.stdout.on('data', (data) => {
190
- output += data.toString();
191
- process.stdout.write(data);
192
- });
193
- proc.stderr.on('data', (data) => {
194
- stderr += data.toString();
195
- process.stderr.write(data);
196
- });
197
- proc.on('close', (code) => {
198
- if (code !== 0) {
199
- reject(new Error(`Claude exited with code ${code}\n${stderr}`));
31
+ export async function status(projectRoot, projectId) {
32
+ const statusPath = findStatusPath(projectRoot, projectId);
33
+ if (!statusPath) {
34
+ throw new Error(`Project ${projectId} not found.\nRun 'porch init' to create a new project.`);
35
+ }
36
+ const state = readState(statusPath);
37
+ const protocol = loadProtocol(projectRoot, state.protocol);
38
+ const phaseConfig = getPhaseConfig(protocol, state.phase);
39
+ // Header
40
+ console.log('');
41
+ console.log(header(`PROJECT: ${state.id} - ${state.title}`));
42
+ console.log(` PROTOCOL: ${state.protocol}`);
43
+ console.log(` PHASE: ${state.phase} (${phaseConfig?.name || 'unknown'})`);
44
+ // For phased protocols, show plan phase status
45
+ if (isPhased(protocol, state.phase) && state.plan_phases.length > 0) {
46
+ console.log('');
47
+ console.log(chalk.bold('PLAN PHASES:'));
48
+ console.log('');
49
+ // Status icons
50
+ const icon = (status) => {
51
+ switch (status) {
52
+ case 'complete': return chalk.green('✓');
53
+ case 'in_progress': return chalk.yellow('►');
54
+ default: return chalk.gray('○');
55
+ }
56
+ };
57
+ // Show phases
58
+ for (const phase of state.plan_phases) {
59
+ const isCurrent = phase.status === 'in_progress';
60
+ const prefix = isCurrent ? chalk.cyan('→ ') : ' ';
61
+ const title = isCurrent ? chalk.bold(phase.title) : phase.title;
62
+ console.log(`${prefix}${icon(phase.status)} ${phase.id}: ${title}`);
63
+ }
64
+ const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
65
+ if (currentPlanPhase) {
66
+ console.log('');
67
+ console.log(chalk.bold(`CURRENT: ${currentPlanPhase.id} - ${currentPlanPhase.title}`));
68
+ // Show phase content from plan
69
+ const planPath = findPlanFile(projectRoot, state.id, state.title);
70
+ if (planPath) {
71
+ const content = fs.readFileSync(planPath, 'utf-8');
72
+ const phaseContent = getPhaseContent(content, currentPlanPhase.id);
73
+ if (phaseContent) {
74
+ console.log(section('FROM THE PLAN', phaseContent.slice(0, 500)));
75
+ }
76
+ }
77
+ // Find the next phase name for the warning
78
+ const currentIdx = state.plan_phases.findIndex(p => p.id === currentPlanPhase.id);
79
+ const nextPlanPhase = state.plan_phases[currentIdx + 1];
80
+ console.log('');
81
+ console.log(chalk.red.bold('╔══════════════════════════════════════════════════════════════╗'));
82
+ console.log(chalk.red.bold('║ 🛑 CRITICAL RULES ║'));
83
+ if (nextPlanPhase) {
84
+ console.log(chalk.red.bold(`║ 1. DO NOT start ${nextPlanPhase.id} until you run porch again!`.padEnd(63) + '║'));
200
85
  }
201
86
  else {
202
- resolve(output);
87
+ console.log(chalk.red.bold('║ 1. DO NOT start the next phase until you run porch again! ║'));
203
88
  }
204
- });
205
- proc.on('error', reject);
206
- });
207
- }
208
- // ============================================================================
209
- // Commands
210
- // ============================================================================
211
- /**
212
- * Initialize a new project with a protocol
213
- */
214
- export async function init(protocolName, projectId, projectName, options = {}) {
215
- const projectRoot = findProjectRoot();
216
- const protocol = loadProtocol(protocolName, projectRoot);
217
- // Create project directory
218
- const projectDir = getProjectDir(projectRoot, projectId, projectName);
219
- fs.mkdirSync(projectDir, { recursive: true });
220
- // Create initial state
221
- const state = createInitialState(protocol, projectId, projectName, options.worktree);
222
- const statusPath = path.join(projectDir, 'status.yaml');
223
- await writeState(statusPath, state);
224
- console.log(chalk.green(`[porch] Initialized project ${projectId} with protocol ${protocolName}`));
225
- console.log(chalk.blue(`[porch] Project directory: ${projectDir}`));
226
- console.log(chalk.blue(`[porch] Initial state: ${state.current_state}`));
227
- }
228
- /**
229
- * Check if a protocol phase is a "phased" phase (runs per plan-phase)
230
- */
231
- function isPhasedPhase(protocol, phaseId) {
232
- const phase = protocol.phases.find(p => p.id === phaseId);
233
- return phase?.phased === true;
234
- }
235
- /**
236
- * Get the IDE phases (implement, defend, evaluate) that run per plan-phase
237
- */
238
- function getIDEPhases(protocol) {
239
- return protocol.phases
240
- .filter(p => p.phased === true)
241
- .map(p => p.id);
242
- }
243
- /**
244
- * Parse the current plan-phase from state like "implement:phase_1"
245
- */
246
- function parsePlanPhaseFromState(state) {
247
- const parts = state.split(':');
248
- const phaseId = parts[0];
249
- // Check if second part is a plan phase (phase_N) or a substate
250
- if (parts.length > 1) {
251
- if (parts[1].startsWith('phase_')) {
252
- return { phaseId, planPhaseId: parts[1], substate: parts[2] || null };
89
+ console.log(chalk.red.bold('║ 2. Run /compact before starting each new phase ║'));
90
+ console.log(chalk.red.bold('║ 3. After completing this phase, run: porch done ' + state.id.padEnd(12) + '║'));
91
+ console.log(chalk.red.bold('╚══════════════════════════════════════════════════════════════╝'));
253
92
  }
254
- return { phaseId, planPhaseId: null, substate: parts[1] };
255
93
  }
256
- return { phaseId, planPhaseId: null, substate: null };
257
- }
258
- /**
259
- * Get the next IDE state for a phased phase
260
- * implement:phase_1 → defend:phase_1 → evaluate:phase_1 → implement:phase_2 → ...
261
- */
262
- function getNextIDEState(protocol, currentState, planPhases, signal) {
263
- const { phaseId, planPhaseId } = parsePlanPhaseFromState(currentState);
264
- if (!planPhaseId)
265
- return null;
266
- const idePhases = getIDEPhases(protocol);
267
- const currentIdeIndex = idePhases.indexOf(phaseId);
268
- if (currentIdeIndex < 0)
269
- return null;
270
- // If not at the end of IDE phases, move to next IDE phase for same plan-phase
271
- if (currentIdeIndex < idePhases.length - 1) {
272
- return `${idePhases[currentIdeIndex + 1]}:${planPhaseId}`;
273
- }
274
- // At end of IDE phases (evaluate), move to next plan-phase
275
- const currentPlanIndex = planPhases.findIndex(p => p.id === planPhaseId);
276
- if (currentPlanIndex < 0)
277
- return null;
278
- // Check if there's a next plan phase
279
- if (currentPlanIndex < planPhases.length - 1) {
280
- const nextPlanPhase = planPhases[currentPlanIndex + 1];
281
- return `${idePhases[0]}:${nextPlanPhase.id}`; // Start implement for next phase
282
- }
283
- // All plan phases complete, move to review
284
- const reviewPhase = protocol.phases.find(p => p.id === 'review');
285
- if (reviewPhase) {
286
- return 'review';
287
- }
288
- return 'complete';
289
- }
290
- // ============================================================================
291
- // Interactive REPL
292
- // ============================================================================
293
- /**
294
- * Create a readline interface for interactive input
295
- */
296
- function createRepl() {
297
- return readline.createInterface({
298
- input: process.stdin,
299
- output: process.stdout,
300
- terminal: true,
301
- });
302
- }
303
- /**
304
- * Prompt user for input with a given message
305
- */
306
- async function prompt(rl, message) {
307
- return new Promise((resolve) => {
308
- rl.question(message, (answer) => {
309
- resolve(answer.trim().toLowerCase());
310
- });
311
- });
94
+ // Show checks status
95
+ const checks = getPhaseChecks(protocol, state.phase);
96
+ if (Object.keys(checks).length > 0) {
97
+ const checkLines = Object.keys(checks).map(name => ` ○ ${name} (not yet run)`);
98
+ console.log(section('CRITERIA', checkLines.join('\n')));
99
+ }
100
+ // Instructions
101
+ const gate = getPhaseGate(protocol, state.phase);
102
+ if (gate && state.gates[gate]?.status === 'pending' && state.gates[gate]?.requested_at) {
103
+ console.log(section('STATUS', chalk.yellow('WAITING FOR HUMAN APPROVAL')));
104
+ console.log(`\n Gate: ${gate}`);
105
+ console.log(' Do not proceed until gate is approved.');
106
+ console.log(`\n To approve: porch approve ${state.id} ${gate}`);
107
+ }
108
+ else {
109
+ console.log(section('INSTRUCTIONS', getInstructions(state, protocol)));
110
+ }
111
+ console.log(section('NEXT ACTION', getNextAction(state, protocol)));
112
+ console.log('');
312
113
  }
313
114
  /**
314
- * Show REPL help
115
+ * porch check <id>
116
+ * Runs the phase checks and reports results.
315
117
  */
316
- function showReplHelp() {
118
+ export async function check(projectRoot, projectId) {
119
+ const statusPath = findStatusPath(projectRoot, projectId);
120
+ if (!statusPath) {
121
+ throw new Error(`Project ${projectId} not found.`);
122
+ }
123
+ const state = readState(statusPath);
124
+ const protocol = loadProtocol(projectRoot, state.protocol);
125
+ const checks = getPhaseChecks(protocol, state.phase);
126
+ if (Object.keys(checks).length === 0) {
127
+ console.log(chalk.dim('No checks defined for this phase.'));
128
+ return;
129
+ }
130
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
317
131
  console.log('');
318
- console.log(chalk.blue('Porch REPL Commands:'));
319
- console.log(' ' + chalk.green('approve') + ' [gate] - Approve pending gate (or current if omitted)');
320
- console.log(' ' + chalk.green('status') + ' - Show current project status');
321
- console.log(' ' + chalk.green('continue') + ' - Continue to next iteration');
322
- console.log(' ' + chalk.green('skip') + ' - Skip current phase (use with caution)');
323
- console.log(' ' + chalk.green('help') + ' - Show this help');
324
- console.log(' ' + chalk.green('quit') + ' - Exit porch');
132
+ console.log(chalk.bold('RUNNING CHECKS...'));
325
133
  console.log('');
326
- }
327
- /**
328
- * Display current status summary
329
- */
330
- function displayStatus(state, protocol) {
134
+ const results = await runPhaseChecks(checks, projectRoot, checkEnv);
135
+ console.log(formatCheckResults(results));
331
136
  console.log('');
332
- console.log(chalk.blue('─'.repeat(50)));
333
- console.log(chalk.blue(`Project: ${state.id} - ${state.title}`));
334
- console.log(chalk.blue(`Protocol: ${state.protocol}`));
335
- console.log(chalk.blue(`State: ${state.current_state}`));
336
- console.log(chalk.blue(`Iteration: ${state.iteration}`));
337
- // Show pending gates
338
- const pendingGates = Object.entries(state.gates)
339
- .filter(([, g]) => g.status === 'pending' && g.requested_at)
340
- .map(([id]) => id);
341
- if (pendingGates.length > 0) {
342
- console.log(chalk.yellow(`Pending gates: ${pendingGates.join(', ')}`));
343
- }
344
- console.log(chalk.blue('─'.repeat(50)));
137
+ if (allChecksPassed(results)) {
138
+ console.log(chalk.green('RESULT: ALL CHECKS PASSED'));
139
+ console.log(`\n Run: porch done ${state.id} (to advance)`);
140
+ }
141
+ else {
142
+ console.log(chalk.red('RESULT: CHECKS FAILED'));
143
+ console.log(`\n Fix the failures and run: porch check ${state.id}`);
144
+ }
345
145
  console.log('');
346
146
  }
347
147
  /**
348
- * Run the protocol loop for a project (Interactive REPL mode)
148
+ * porch done <id>
149
+ * Advances to next phase if checks pass. Refuses if checks fail.
349
150
  */
350
- export async function run(projectId, options = {}) {
351
- const projectRoot = findProjectRoot();
352
- // Find status file
353
- const statusFilePath = findStatusFile(projectRoot, projectId);
354
- if (!statusFilePath) {
355
- throw new Error(`Status file not found for project: ${projectId}\n` +
356
- `Run: porch init <protocol> ${projectId} <project-name>`);
357
- }
358
- // Read state and load protocol
359
- const state = readState(statusFilePath);
360
- if (!state) {
361
- throw new Error(`Could not read state from: ${statusFilePath}`);
362
- }
363
- const protocol = loadProtocol(state.protocol, projectRoot);
364
- const maxIterations = protocol.config?.max_iterations || 100;
365
- // Create notifier for this project (desktop notifications for important events)
366
- const notifier = createNotifier(projectId, { desktop: true });
367
- // Create REPL interface
368
- const rl = createRepl();
369
- console.log('');
370
- console.log(chalk.green('═'.repeat(50)));
371
- console.log(chalk.green(` PORCH - Protocol Orchestrator`));
372
- console.log(chalk.green('═'.repeat(50)));
373
- console.log(chalk.blue(` Project: ${state.id} - ${state.title}`));
374
- console.log(chalk.blue(` Protocol: ${state.protocol}`));
375
- console.log(chalk.blue(` State: ${state.current_state}`));
376
- console.log(chalk.green('═'.repeat(50)));
377
- console.log('');
378
- console.log(chalk.gray('Type "help" for commands, "quit" to exit'));
379
- console.log('');
380
- let currentState = state;
381
- // Extract plan phases if not already done and we're past planning
382
- if (!currentState.plan_phases || currentState.plan_phases.length === 0) {
383
- const planFile = findPlanFile(projectRoot, projectId, currentState.title);
384
- if (planFile) {
385
- try {
386
- const planPhases = extractPhasesFromPlanFile(planFile);
387
- currentState = setPlanPhases(currentState, planPhases);
388
- await writeState(statusFilePath, currentState);
389
- console.log(chalk.blue(`[porch] Extracted ${planPhases.length} phases from plan`));
390
- for (const phase of planPhases) {
391
- console.log(chalk.blue(` - ${phase.id}: ${phase.title}`));
392
- }
393
- }
394
- catch (e) {
395
- console.log(chalk.yellow(`[porch] Could not extract plan phases: ${e}`));
396
- }
151
+ export async function done(projectRoot, projectId) {
152
+ const statusPath = findStatusPath(projectRoot, projectId);
153
+ if (!statusPath) {
154
+ throw new Error(`Project ${projectId} not found.`);
155
+ }
156
+ let state = readState(statusPath);
157
+ const protocol = loadProtocol(projectRoot, state.protocol);
158
+ const checks = getPhaseChecks(protocol, state.phase);
159
+ // Run checks first
160
+ if (Object.keys(checks).length > 0) {
161
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
162
+ console.log('');
163
+ console.log(chalk.bold('RUNNING CHECKS...'));
164
+ const results = await runPhaseChecks(checks, projectRoot, checkEnv);
165
+ console.log(formatCheckResults(results));
166
+ if (!allChecksPassed(results)) {
167
+ console.log('');
168
+ console.log(chalk.red('CHECKS FAILED. Cannot advance.'));
169
+ console.log(`\n Fix the failures and try again.`);
170
+ process.exit(1);
397
171
  }
398
172
  }
399
- for (let iteration = currentState.iteration; iteration <= maxIterations; iteration++) {
400
- console.log(chalk.blue('━'.repeat(40)));
401
- console.log(chalk.blue(`[porch] Iteration ${iteration}`));
402
- console.log(chalk.blue(''.repeat(40)));
403
- // Fresh read of state each iteration (Ralph pattern)
404
- currentState = readState(statusFilePath) || currentState;
405
- console.log(chalk.blue(`[porch] Current state: ${currentState.current_state}`));
406
- // Parse state into phase and substate
407
- const { phaseId, planPhaseId, substate } = parsePlanPhaseFromState(currentState.current_state);
408
- // Re-attempt plan phase extraction if entering a phased phase without plan phases
409
- // This handles the case where porch started during Specify phase (no plan file yet)
410
- if (isPhasedPhase(protocol, phaseId) && (!currentState.plan_phases || currentState.plan_phases.length === 0)) {
411
- const planFile = findPlanFile(projectRoot, projectId, currentState.title);
412
- if (planFile) {
413
- try {
414
- const planPhases = extractPhasesFromPlanFile(planFile);
415
- currentState = setPlanPhases(currentState, planPhases);
416
- await writeState(statusFilePath, currentState);
417
- console.log(chalk.blue(`[porch] Late discovery: Extracted ${planPhases.length} phases from plan`));
418
- for (const phase of planPhases) {
419
- console.log(chalk.blue(` - ${phase.id}: ${phase.title}`));
420
- }
421
- }
422
- catch (e) {
423
- console.log(chalk.yellow(`[porch] Could not extract plan phases: ${e}`));
424
- }
425
- }
426
- else {
427
- console.log(chalk.yellow(`[porch] Warning: Entering phased phase '${phaseId}' but no plan file found`));
428
- }
429
- }
430
- // Check if terminal phase
431
- if (isTerminalPhase(protocol, phaseId)) {
432
- console.log(chalk.green('━'.repeat(40)));
433
- console.log(chalk.green(`[porch] ${state.protocol} loop COMPLETE`));
434
- console.log(chalk.green(`[porch] Project ${projectId} finished all phases`));
435
- console.log(chalk.green('━'.repeat(40)));
436
- rl.close();
437
- return;
438
- }
439
- // Check if there's a pending gate from a previous iteration (step already executed)
440
- const pendingGateInfo = getGateForState(protocol, currentState.current_state);
441
- if (pendingGateInfo) {
442
- const { gateId } = pendingGateInfo;
443
- // Check if gate is already approved
444
- if (currentState.gates[gateId]?.status === 'passed') {
445
- // Gate approved - proceed to next state
446
- const nextState = getGateNextState(protocol, phaseId);
447
- if (nextState) {
448
- console.log(chalk.green(`[porch] Gate ${gateId} passed! Proceeding to ${nextState}`));
449
- await notifier.gateApproved(gateId);
450
- // Reset any consultation attempts for the gated state
451
- currentState = resetConsultationAttempts(currentState, currentState.current_state);
452
- // If entering a phased phase, start with first plan phase
453
- if (isPhasedPhase(protocol, nextState.split(':')[0]) && currentState.plan_phases?.length) {
454
- const firstPlanPhase = currentState.plan_phases[0];
455
- currentState = updateState(currentState, `${nextState.split(':')[0]}:${firstPlanPhase.id}`);
456
- currentState = updatePhaseStatus(currentState, firstPlanPhase.id, 'in_progress');
457
- }
458
- else {
459
- currentState = updateState(currentState, nextState);
460
- }
461
- await writeState(statusFilePath, currentState);
462
- continue; // Start next iteration with new state
463
- }
464
- }
465
- else if (currentState.gates[gateId]?.requested_at) {
466
- // Gate requested but not approved - prompt user interactively
467
- console.log('');
468
- console.log(chalk.yellow('═'.repeat(50)));
469
- console.log(chalk.yellow(` GATE PENDING: ${gateId}`));
470
- console.log(chalk.yellow('═'.repeat(50)));
471
- console.log(chalk.cyan(` Phase: ${phaseId}`));
472
- console.log(chalk.cyan(` State: ${currentState.current_state}`));
173
+ // Check for gate
174
+ const gate = getPhaseGate(protocol, state.phase);
175
+ if (gate && state.gates[gate]?.status !== 'approved') {
176
+ console.log('');
177
+ console.log(chalk.yellow(`GATE REQUIRED: ${gate}`));
178
+ console.log(`\n Run: porch gate ${state.id}`);
179
+ console.log(' Wait for human approval before advancing.');
180
+ return;
181
+ }
182
+ // Handle phased protocols (plan phases with checks at completion)
183
+ if (isPhased(protocol, state.phase) && state.plan_phases.length > 0) {
184
+ const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
185
+ if (currentPlanPhase && !allPlanPhasesComplete(state.plan_phases)) {
186
+ // Run phase completion checks (implement + defend + evaluate all at once)
187
+ const completionChecks = getPhaseCompletionChecks(protocol);
188
+ if (Object.keys(completionChecks).length > 0) {
189
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
473
190
  console.log('');
474
- // Interactive gate approval loop
475
- let gateHandled = false;
476
- while (!gateHandled) {
477
- const answer = await prompt(rl, chalk.yellow(`Approve ${gateId}? [y/n/help]: `));
478
- switch (answer) {
479
- case 'y':
480
- case 'yes':
481
- case 'approve':
482
- currentState = approveGate(currentState, gateId);
483
- await writeState(statusFilePath, currentState);
484
- console.log(chalk.green(`✓ Gate ${gateId} approved`));
485
- gateHandled = true;
486
- break;
487
- case 'n':
488
- case 'no':
489
- console.log(chalk.yellow('Gate not approved. Staying in current state.'));
490
- console.log(chalk.gray('Type "quit" to exit or wait for changes.'));
491
- break;
492
- case 'status':
493
- displayStatus(currentState, protocol);
494
- break;
495
- case 'help':
496
- case '?':
497
- showReplHelp();
498
- break;
499
- case 'quit':
500
- case 'exit':
501
- console.log(chalk.blue('Exiting porch...'));
502
- rl.close();
503
- return;
504
- default:
505
- console.log(chalk.gray('Type "y" to approve, "n" to decline, or "help" for commands'));
506
- }
191
+ console.log(chalk.bold(`RUNNING PHASE COMPLETION CHECKS (${currentPlanPhase.id})...`));
192
+ const results = await runPhaseChecks(completionChecks, projectRoot, checkEnv);
193
+ console.log(formatCheckResults(results));
194
+ if (!allChecksPassed(results)) {
195
+ console.log('');
196
+ console.log(chalk.red('PHASE COMPLETION CHECKS FAILED. Cannot advance.'));
197
+ console.log(`\n Ensure your commit includes:`);
198
+ console.log(` - Implementation code`);
199
+ console.log(` - Tests`);
200
+ console.log(` - 3-way review results in commit message`);
201
+ console.log(`\n Then try again.`);
202
+ process.exit(1);
507
203
  }
508
- continue;
509
- }
510
- // If gate not yet requested, fall through to execute phase first
511
- }
512
- // Get the current phase definition
513
- const phase = protocol.phases.find(p => p.id === phaseId);
514
- // Show plan phase context if in a phased phase
515
- if (planPhaseId && currentState.plan_phases) {
516
- const planPhase = currentState.plan_phases.find(p => p.id === planPhaseId);
517
- if (planPhase) {
518
- console.log(chalk.cyan(`[phase] IDE Phase: ${phaseId} | Plan Phase: ${planPhase.title}`));
519
204
  }
520
- else {
521
- console.log(chalk.cyan(`[phase] Phase: ${phaseId}`));
522
- }
523
- }
524
- else {
525
- console.log(chalk.cyan(`[phase] Phase: ${phaseId}`));
526
- }
527
- // Notify phase start
528
- await notifier.phaseStart(phaseId);
529
- // Execute phase
530
- const output = await invokeClaude(protocol, phaseId, currentState, statusFilePath, projectRoot, options);
531
- const signal = extractSignal(output);
532
- // Run phase checks (build/test) if defined
533
- if (phase?.checks && !options.dryRun) {
534
- console.log(chalk.blue(`[porch] Running checks for phase ${phaseId}...`));
535
- const checkResult = await runPhaseChecks(phase, {
536
- cwd: projectRoot,
537
- dryRun: options.dryRun,
538
- });
539
- console.log(formatCheckResults(checkResult));
540
- if (!checkResult.success) {
541
- // Notify about check failure
542
- const failedCheck = checkResult.checks.find(c => !c.success);
543
- await notifier.checkFailed(phaseId, failedCheck?.name || 'build/test', failedCheck?.error || 'Check failed');
544
- // If checks fail, handle based on check configuration
545
- if (checkResult.returnTo) {
546
- console.log(chalk.yellow(`[porch] Checks failed, returning to ${checkResult.returnTo}`));
547
- if (planPhaseId) {
548
- currentState = updateState(currentState, `${checkResult.returnTo}:${planPhaseId}`);
549
- }
550
- else {
551
- currentState = updateState(currentState, checkResult.returnTo);
552
- }
553
- await writeState(statusFilePath, currentState);
554
- continue;
555
- }
556
- // No returnTo means we should retry (already handled in checks)
205
+ // Advance to next plan phase
206
+ const { phases: updatedPhases, moveToReview } = advancePlanPhase(state.plan_phases, currentPlanPhase.id);
207
+ state.plan_phases = updatedPhases;
208
+ console.log('');
209
+ console.log(chalk.green(`PLAN PHASE COMPLETE: ${currentPlanPhase.id} - ${currentPlanPhase.title}`));
210
+ // Check if moving to review (all plan phases done)
211
+ if (moveToReview) {
212
+ state.phase = 'review';
213
+ state.current_plan_phase = null;
214
+ writeState(statusPath, state);
215
+ console.log(chalk.cyan('All plan phases complete. Moving to REVIEW phase.'));
216
+ console.log(`\n Run: porch status ${state.id}`);
217
+ return;
557
218
  }
558
- }
559
- // Run consultation if configured for this phase/substate
560
- if (phase?.consultation && hasConsultation(phase) && !options.dryRun && !options.noClaude) {
561
- const consultConfig = phase.consultation;
562
- const currentSubstate = substate || parsePlanPhaseFromState(currentState.current_state).substate;
563
- // Check if consultation is triggered by current substate
564
- if (consultConfig.on === currentSubstate || consultConfig.on === phaseId) {
565
- const maxRounds = consultConfig.max_rounds || 3;
566
- const stateKey = currentState.current_state;
567
- // Get attempt count from state (persisted across porch iterations)
568
- const attemptCount = getConsultationAttempts(currentState, stateKey) + 1;
569
- console.log(chalk.blue(`[porch] Consultation triggered for phase ${phaseId} (attempt ${attemptCount}/${maxRounds})`));
570
- await notifier.consultationStart(phaseId, consultConfig.models || ['gemini', 'codex', 'claude']);
571
- const consultResult = await runConsultationLoop(consultConfig, {
572
- subcommand: consultConfig.type.includes('pr') ? 'pr' : consultConfig.type.includes('spec') ? 'spec' : 'plan',
573
- identifier: projectId,
574
- cwd: projectRoot,
575
- timeout: protocol.config?.consultation_timeout,
576
- dryRun: options.dryRun,
577
- });
578
- console.log(formatConsultationResults(consultResult));
579
- await notifier.consultationComplete(phaseId, consultResult.feedback, consultResult.allApproved);
580
- // If not all approved, track attempt and check for escalation
581
- if (!consultResult.allApproved) {
582
- // Increment attempt count in state (persists across iterations)
583
- currentState = incrementConsultationAttempts(currentState, stateKey);
584
- await writeState(statusFilePath, currentState);
585
- // Check if we've reached max attempts
586
- if (attemptCount >= maxRounds) {
587
- // Create escalation gate - requires human intervention
588
- const escalationGateId = `${phaseId}_consultation_escalation`;
589
- // Check if escalation gate was already approved (human override)
590
- if (currentState.gates[escalationGateId]?.status === 'passed') {
591
- console.log(chalk.green(`[porch] Consultation escalation gate already approved, continuing`));
592
- // Reset attempts and fall through to next state handling
593
- currentState = resetConsultationAttempts(currentState, stateKey);
594
- await writeState(statusFilePath, currentState);
595
- }
596
- else {
597
- console.log(chalk.red(`[porch] Consultation failed after ${attemptCount} attempts - escalating to human`));
598
- console.log(chalk.yellow(`[porch] To override and continue: porch approve ${projectId} ${escalationGateId}`));
599
- // Request human gate if not already requested
600
- if (!currentState.gates[escalationGateId]?.requested_at) {
601
- currentState = requestGateApproval(currentState, escalationGateId);
602
- await writeState(statusFilePath, currentState);
603
- await notifier.gatePending(phaseId, escalationGateId);
604
- }
605
- // Prompt user to approve escalation gate
606
- console.log('');
607
- console.log(chalk.red('═'.repeat(50)));
608
- console.log(chalk.red(` ESCALATION: ${escalationGateId}`));
609
- console.log(chalk.red('═'.repeat(50)));
610
- console.log(chalk.yellow(` Consultation failed after ${attemptCount} attempts`));
611
- console.log('');
612
- let escalationHandled = false;
613
- while (!escalationHandled) {
614
- const answer = await prompt(rl, chalk.red(`Override and continue? [y/n/help]: `));
615
- switch (answer) {
616
- case 'y':
617
- case 'yes':
618
- case 'approve':
619
- case 'override':
620
- currentState = approveGate(currentState, escalationGateId);
621
- currentState = resetConsultationAttempts(currentState, stateKey);
622
- await writeState(statusFilePath, currentState);
623
- console.log(chalk.green(`✓ Escalation gate ${escalationGateId} approved`));
624
- escalationHandled = true;
625
- break;
626
- case 'n':
627
- case 'no':
628
- console.log(chalk.yellow('Escalation not approved. Consultation loop will continue.'));
629
- break;
630
- case 'status':
631
- displayStatus(currentState, protocol);
632
- break;
633
- case 'help':
634
- case '?':
635
- showReplHelp();
636
- break;
637
- case 'quit':
638
- case 'exit':
639
- console.log(chalk.blue('Exiting porch...'));
640
- rl.close();
641
- return;
642
- default:
643
- console.log(chalk.gray('Type "y" to override and continue, "n" to decline, or "help" for commands'));
644
- }
645
- }
646
- continue;
647
- }
648
- }
649
- else {
650
- console.log(chalk.yellow(`[porch] Consultation requested changes (attempt ${attemptCount}/${maxRounds}), continuing for revision`));
651
- // Stay in same state for Claude to revise on next iteration
652
- continue;
653
- }
654
- }
655
- else {
656
- // All approved - reset attempt counter
657
- currentState = resetConsultationAttempts(currentState, stateKey);
658
- await writeState(statusFilePath, currentState);
659
- }
660
- // All approved (or escalation gate passed) - use consultation's next state if defined
661
- if (consultConfig.next) {
662
- if (planPhaseId) {
663
- currentState = updateState(currentState, `${consultConfig.next}:${planPhaseId}`);
664
- }
665
- else {
666
- currentState = updateState(currentState, consultConfig.next);
667
- }
668
- await writeState(statusFilePath, currentState);
669
- continue;
670
- }
219
+ // Update current plan phase tracker
220
+ const newCurrentPhase = getCurrentPlanPhase(state.plan_phases);
221
+ state.current_plan_phase = newCurrentPhase?.id || null;
222
+ writeState(statusPath, state);
223
+ if (newCurrentPhase) {
224
+ console.log(chalk.cyan(`NEXT: ${newCurrentPhase.id} - ${newCurrentPhase.title}`));
671
225
  }
226
+ console.log(`\n Run: porch status ${state.id}`);
227
+ return;
672
228
  }
673
- // Check if current state has a gate that should trigger AFTER this step
674
- // This is the gate trigger point - step has executed, now block before transition
675
- const gateInfo = getGateForState(protocol, currentState.current_state);
676
- if (gateInfo) {
677
- const { gateId } = gateInfo;
678
- // Request gate approval if not already requested
679
- if (!currentState.gates[gateId]?.requested_at) {
680
- currentState = requestGateApproval(currentState, gateId);
681
- await writeState(statusFilePath, currentState);
682
- console.log(chalk.yellow(`[porch] Step complete. Gate approval requested: ${gateId}`));
683
- await notifier.gatePending(phaseId, gateId);
684
- }
685
- // Gate not yet approved - prompt user interactively
686
- if (currentState.gates[gateId]?.status !== 'passed') {
687
- console.log('');
688
- console.log(chalk.yellow('═'.repeat(50)));
689
- console.log(chalk.yellow(` GATE PENDING: ${gateId}`));
690
- console.log(chalk.yellow('═'.repeat(50)));
691
- let gateHandled = false;
692
- while (!gateHandled) {
693
- const answer = await prompt(rl, chalk.yellow(`Approve ${gateId}? [y/n/help]: `));
694
- switch (answer) {
695
- case 'y':
696
- case 'yes':
697
- case 'approve':
698
- currentState = approveGate(currentState, gateId);
699
- await writeState(statusFilePath, currentState);
700
- console.log(chalk.green(`✓ Gate ${gateId} approved`));
701
- gateHandled = true;
702
- break;
703
- case 'n':
704
- case 'no':
705
- console.log(chalk.yellow('Gate not approved. Staying in current state.'));
706
- break;
707
- case 'status':
708
- displayStatus(currentState, protocol);
709
- break;
710
- case 'help':
711
- case '?':
712
- showReplHelp();
713
- break;
714
- case 'quit':
715
- case 'exit':
716
- console.log(chalk.blue('Exiting porch...'));
717
- rl.close();
718
- return;
719
- default:
720
- console.log(chalk.gray('Type "y" to approve, "n" to decline, or "help" for commands'));
721
- }
722
- }
723
- continue;
229
+ }
230
+ // Advance to next protocol phase
231
+ advanceProtocolPhase(state, protocol, statusPath);
232
+ }
233
+ function advanceProtocolPhase(state, protocol, statusPath) {
234
+ const nextPhase = getNextPhase(protocol, state.phase);
235
+ if (!nextPhase) {
236
+ state.phase = 'complete';
237
+ writeState(statusPath, state);
238
+ console.log('');
239
+ console.log(chalk.green.bold('🎉 PROTOCOL COMPLETE'));
240
+ console.log(`\n Project ${state.id} has completed the ${state.protocol} protocol.`);
241
+ return;
242
+ }
243
+ state.phase = nextPhase.id;
244
+ // If entering a phased phase (implement), extract plan phases
245
+ if (isPhased(protocol, nextPhase.id)) {
246
+ const planPath = findPlanFile(process.cwd(), state.id, state.title);
247
+ if (planPath) {
248
+ state.plan_phases = extractPhasesFromFile(planPath);
249
+ // extractPhasesFromFile already marks first phase as in_progress
250
+ if (state.plan_phases.length > 0) {
251
+ state.current_plan_phase = state.plan_phases[0].id;
724
252
  }
725
253
  }
726
- // Determine next state
727
- let nextState = null;
728
- if (signal) {
729
- console.log(chalk.green(`[porch] Signal received: ${signal}`));
730
- nextState = getSignalNextState(protocol, phaseId, signal);
731
- }
732
- if (!nextState) {
733
- // Check if this is a phased phase (IDE loop)
734
- if (isPhasedPhase(protocol, phaseId) && currentState.plan_phases?.length) {
735
- nextState = getNextIDEState(protocol, currentState.current_state, currentState.plan_phases, signal || undefined);
736
- // Mark current plan phase as complete if moving to next
737
- if (nextState && planPhaseId) {
738
- const { planPhaseId: nextPlanPhaseId } = parsePlanPhaseFromState(nextState);
739
- if (nextPlanPhaseId !== planPhaseId) {
740
- currentState = updatePhaseStatus(currentState, planPhaseId, 'complete');
741
- if (nextPlanPhaseId) {
742
- currentState = updatePhaseStatus(currentState, nextPlanPhaseId, 'in_progress');
743
- }
744
- }
745
- }
746
- }
747
- else {
748
- // Use default transition
749
- nextState = getDefaultNextState(protocol, currentState.current_state);
254
+ }
255
+ writeState(statusPath, state);
256
+ console.log('');
257
+ console.log(chalk.green(`ADVANCING TO: ${nextPhase.id} - ${nextPhase.name}`));
258
+ // If we just entered implement phase, show phase 1 info and the critical warning
259
+ if (isPhased(protocol, nextPhase.id) && state.plan_phases.length > 0) {
260
+ const firstPhase = state.plan_phases[0];
261
+ const nextPlanPhase = state.plan_phases[1];
262
+ console.log('');
263
+ console.log(chalk.bold(`YOUR TASK: ${firstPhase.id} - "${firstPhase.title}"`));
264
+ // Show phase content from plan
265
+ const planPath = findPlanFile(process.cwd(), state.id, state.title);
266
+ if (planPath) {
267
+ const content = fs.readFileSync(planPath, 'utf-8');
268
+ const phaseContent = getPhaseContent(content, firstPhase.id);
269
+ if (phaseContent) {
270
+ console.log(section('FROM THE PLAN', phaseContent.slice(0, 800)));
750
271
  }
751
272
  }
752
- if (nextState) {
753
- currentState = updateState(currentState, nextState, signal ? { signal } : undefined);
754
- await writeState(statusFilePath, currentState);
273
+ console.log('');
274
+ console.log(chalk.red.bold('╔══════════════════════════════════════════════════════════════╗'));
275
+ console.log(chalk.red.bold('║ 🛑 CRITICAL RULES ║'));
276
+ if (nextPlanPhase) {
277
+ console.log(chalk.red.bold(`║ 1. DO NOT start ${nextPlanPhase.id} until you run porch again!`.padEnd(63) + '║'));
755
278
  }
756
279
  else {
757
- console.log(chalk.yellow(`[porch] No transition defined, staying in current state`));
280
+ console.log(chalk.red.bold('║ 1. DO NOT start the next phase until you run porch again! ║'));
758
281
  }
282
+ console.log(chalk.red.bold('║ 2. Run /compact before starting each new phase ║'));
283
+ console.log(chalk.red.bold('║ 3. When phase complete, run: porch done ' + state.id.padEnd(20) + '║'));
284
+ console.log(chalk.red.bold('╚══════════════════════════════════════════════════════════════╝'));
759
285
  }
760
- rl.close();
761
- throw new Error(`Max iterations (${maxIterations}) reached!`);
762
- }
763
- /**
764
- * Approve a gate
765
- */
766
- export async function approve(projectId, gateId) {
767
- const projectRoot = findProjectRoot();
768
- const statusFilePath = findStatusFile(projectRoot, projectId);
769
- if (!statusFilePath) {
770
- throw new Error(`Status file not found for project: ${projectId}`);
771
- }
772
- const state = readState(statusFilePath);
773
- if (!state) {
774
- throw new Error(`Could not read state from: ${statusFilePath}`);
775
- }
776
- const updatedState = approveGate(state, gateId);
777
- await writeState(statusFilePath, updatedState);
778
- console.log(chalk.green(`[porch] Approved: ${gateId}`));
286
+ console.log(`\n Run: porch status ${state.id}`);
779
287
  }
780
288
  /**
781
- * Show project status
289
+ * porch gate <id>
290
+ * Requests human approval for current gate.
782
291
  */
783
- export async function status(projectId) {
784
- const projectRoot = findProjectRoot();
785
- if (projectId) {
786
- // Show specific project
787
- const statusFilePath = findStatusFile(projectRoot, projectId);
788
- if (!statusFilePath) {
789
- throw new Error(`Status file not found for project: ${projectId}`);
790
- }
791
- const state = readState(statusFilePath);
792
- if (!state) {
793
- throw new Error(`Could not read state from: ${statusFilePath}`);
794
- }
795
- console.log(chalk.blue(`[porch] Status for project ${projectId}:`));
796
- console.log('');
797
- console.log(` ID: ${state.id}`);
798
- console.log(` Title: ${state.title}`);
799
- console.log(` Protocol: ${state.protocol}`);
800
- console.log(` State: ${state.current_state}`);
801
- console.log(` Iteration: ${state.iteration}`);
802
- console.log(` Started: ${state.started_at}`);
803
- console.log(` Updated: ${state.last_updated}`);
804
- console.log('');
805
- if (Object.keys(state.gates).length > 0) {
806
- console.log(' Gates:');
807
- for (const [gateId, gateStatus] of Object.entries(state.gates)) {
808
- const icon = gateStatus.status === 'passed' ? '✓' : gateStatus.status === 'failed' ? '✗' : '⏳';
809
- console.log(` ${icon} ${gateId}: ${gateStatus.status}`);
810
- }
811
- console.log('');
812
- }
813
- if (state.plan_phases && state.plan_phases.length > 0) {
814
- console.log(' Plan Phases:');
815
- for (const phase of state.plan_phases) {
816
- const phaseStatus = state.phases[phase.id]?.status || 'pending';
817
- const icon = phaseStatus === 'complete' ? '✓' : phaseStatus === 'in_progress' ? '🔄' : '○';
818
- console.log(` ${icon} ${phase.id}: ${phase.title}`);
819
- }
820
- }
292
+ export async function gate(projectRoot, projectId) {
293
+ const statusPath = findStatusPath(projectRoot, projectId);
294
+ if (!statusPath) {
295
+ throw new Error(`Project ${projectId} not found.`);
296
+ }
297
+ const state = readState(statusPath);
298
+ const protocol = loadProtocol(projectRoot, state.protocol);
299
+ const gateName = getPhaseGate(protocol, state.phase);
300
+ if (!gateName) {
301
+ console.log(chalk.dim('No gate required for this phase.'));
302
+ console.log(`\n Run: porch done ${state.id}`);
303
+ return;
821
304
  }
822
- else {
823
- // Show all projects
824
- const projects = findProjects(projectRoot);
825
- const executions = findExecutions(projectRoot);
826
- if (projects.length === 0 && executions.length === 0) {
827
- console.log(chalk.yellow('[porch] No projects found'));
828
- return;
829
- }
830
- console.log(chalk.blue('[porch] Projects:'));
831
- for (const { id, path: statusPath } of projects) {
832
- const state = readState(statusPath);
833
- if (state) {
834
- const pendingGates = Object.entries(state.gates)
835
- .filter(([, g]) => g.status === 'pending' && g.requested_at)
836
- .map(([id]) => id);
837
- const gateStr = pendingGates.length > 0 ? chalk.yellow(` [${pendingGates.join(', ')}]`) : '';
838
- console.log(` ${id} ${state.title} - ${state.current_state}${gateStr}`);
839
- }
840
- }
841
- if (executions.length > 0) {
305
+ // Mark gate as requested
306
+ if (!state.gates[gateName]) {
307
+ state.gates[gateName] = { status: 'pending' };
308
+ }
309
+ if (!state.gates[gateName].requested_at) {
310
+ state.gates[gateName].requested_at = new Date().toISOString();
311
+ writeState(statusPath, state);
312
+ }
313
+ console.log('');
314
+ console.log(chalk.bold(`GATE: ${gateName}`));
315
+ console.log('');
316
+ // Show relevant artifact and open it for review
317
+ const artifact = getArtifactForPhase(projectRoot, state);
318
+ if (artifact) {
319
+ const fullPath = path.join(projectRoot, artifact);
320
+ if (fs.existsSync(fullPath)) {
321
+ console.log(` Artifact: ${artifact}`);
842
322
  console.log('');
843
- console.log(chalk.blue('[porch] Executions:'));
844
- for (const { protocol, id, path: statusPath } of executions) {
845
- const state = readState(statusPath);
846
- if (state) {
847
- console.log(` ${protocol}/${id} - ${state.current_state}`);
848
- }
849
- }
323
+ console.log(chalk.cyan(' Opening artifact for human review...'));
324
+ // Use af open to display in annotation viewer
325
+ const { spawn } = await import('node:child_process');
326
+ spawn('af', ['open', fullPath], {
327
+ stdio: 'inherit',
328
+ detached: true
329
+ }).unref();
850
330
  }
851
331
  }
332
+ console.log('');
333
+ console.log(chalk.yellow(' Human approval required. STOP and wait.'));
334
+ console.log(' Do not proceed until gate is approved.');
335
+ console.log('');
336
+ console.log(chalk.bold('STATUS: WAITING FOR HUMAN APPROVAL'));
337
+ console.log('');
338
+ console.log(chalk.dim(` To approve: porch approve ${state.id} ${gateName}`));
339
+ console.log('');
852
340
  }
853
341
  /**
854
- * List available protocols
342
+ * porch approve <id> <gate> --a-human-explicitly-approved-this
343
+ * Human approves a gate. Requires explicit flag to prevent automated approvals.
855
344
  */
856
- export async function list() {
857
- const projectRoot = findProjectRoot();
858
- const protocols = listProtocols(projectRoot);
859
- if (protocols.length === 0) {
860
- console.log(chalk.yellow('[porch] No protocols found'));
345
+ export async function approve(projectRoot, projectId, gateName, hasHumanFlag) {
346
+ const statusPath = findStatusPath(projectRoot, projectId);
347
+ if (!statusPath) {
348
+ throw new Error(`Project ${projectId} not found.`);
349
+ }
350
+ const state = readState(statusPath);
351
+ if (!state.gates[gateName]) {
352
+ const knownGates = Object.keys(state.gates).join(', ');
353
+ throw new Error(`Unknown gate: ${gateName}\nKnown gates: ${knownGates || 'none'}`);
354
+ }
355
+ if (state.gates[gateName].status === 'approved') {
356
+ console.log(chalk.yellow(`Gate ${gateName} is already approved.`));
861
357
  return;
862
358
  }
863
- console.log(chalk.blue('[porch] Available protocols:'));
864
- for (const name of protocols) {
865
- try {
866
- const protocol = loadProtocol(name, projectRoot);
867
- console.log(` - ${name}: ${protocol.description}`);
868
- }
869
- catch {
870
- console.log(` - ${name}: (error loading)`);
359
+ // Require explicit human flag
360
+ if (!hasHumanFlag) {
361
+ console.log('');
362
+ console.log(chalk.red('ERROR: Human approval required.'));
363
+ console.log('');
364
+ console.log(' To approve, please run:');
365
+ console.log('');
366
+ console.log(chalk.cyan(` porch approve ${projectId} ${gateName} --a-human-explicitly-approved-this`));
367
+ console.log('');
368
+ process.exit(1);
369
+ }
370
+ // Run phase checks before approving
371
+ const protocol = loadProtocol(projectRoot, state.protocol);
372
+ const checks = getPhaseChecks(protocol, state.phase);
373
+ if (Object.keys(checks).length > 0) {
374
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: state.title };
375
+ console.log('');
376
+ console.log(chalk.bold('RUNNING CHECKS...'));
377
+ const results = await runPhaseChecks(checks, projectRoot, checkEnv);
378
+ console.log(formatCheckResults(results));
379
+ if (!allChecksPassed(results)) {
380
+ console.log('');
381
+ console.log(chalk.red('CHECKS FAILED. Cannot approve gate.'));
382
+ console.log(`\n Fix the failures and try again.`);
383
+ process.exit(1);
871
384
  }
872
385
  }
873
- }
874
- /**
875
- * Show protocol definition
876
- */
877
- export async function show(protocolName) {
878
- const projectRoot = findProjectRoot();
879
- const protocol = loadProtocol(protocolName, projectRoot);
880
- console.log(chalk.blue(`[porch] Protocol: ${protocolName}`));
386
+ state.gates[gateName].status = 'approved';
387
+ state.gates[gateName].approved_at = new Date().toISOString();
388
+ writeState(statusPath, state);
389
+ console.log('');
390
+ console.log(chalk.green(`Gate ${gateName} approved.`));
391
+ console.log(`\n Run: porch done ${state.id} (to advance)`);
881
392
  console.log('');
882
- console.log(JSON.stringify(protocol, null, 2));
883
393
  }
884
394
  /**
885
- * Show pending gates across all projects
395
+ * porch init <protocol> <id> <name>
396
+ * Initialize a new project.
886
397
  */
887
- export async function pending() {
888
- const projectRoot = findProjectRoot();
889
- const gates = findPendingGates(projectRoot);
890
- if (gates.length === 0) {
891
- console.log(chalk.green('[porch] No pending gates'));
892
- return;
893
- }
894
- console.log(chalk.yellow('[porch] Pending gates:'));
895
- for (const gate of gates) {
896
- const requestedAt = gate.requestedAt ? ` (requested ${gate.requestedAt})` : '';
897
- console.log(` ${gate.projectId}: ${gate.gateId}${requestedAt}`);
898
- console.log(` porch approve ${gate.projectId} ${gate.gateId}`);
899
- }
398
+ export async function init(projectRoot, protocolName, projectId, projectName) {
399
+ const protocol = loadProtocol(projectRoot, protocolName);
400
+ const statusPath = getStatusPath(projectRoot, projectId, projectName);
401
+ // Check if already exists
402
+ if (fs.existsSync(statusPath)) {
403
+ throw new Error(`Project ${projectId}-${projectName} already exists.`);
404
+ }
405
+ const state = createInitialState(protocol, projectId, projectName, projectRoot);
406
+ writeState(statusPath, state);
407
+ console.log('');
408
+ console.log(chalk.green(`Project initialized: ${projectId}-${projectName}`));
409
+ console.log(` Protocol: ${protocolName}`);
410
+ console.log(` Starting phase: ${state.phase}`);
411
+ console.log(`\n Run: porch status ${projectId}`);
412
+ console.log('');
900
413
  }
901
414
  // ============================================================================
902
- // Auto-Detection
415
+ // Helpers
903
416
  // ============================================================================
904
- /**
905
- * Auto-detect project ID from current directory
906
- *
907
- * Detection methods:
908
- * 1. Check if cwd is a worktree matching pattern: .builders/<id> or worktrees/<protocol>_<id>_*
909
- * 2. Check for a single project in codev/projects/
910
- * 3. Check for .porch-project marker file
911
- */
912
- function autoDetectProject() {
913
- const cwd = process.cwd();
914
- // Method 1: Check path pattern for builder worktree
915
- // Pattern: .builders/<id> or .builders/<id>-<name>
916
- const buildersMatch = cwd.match(/[/\\]\.builders[/\\](\d+)(?:-[^/\\]*)?(?:[/\\]|$)/);
917
- if (buildersMatch) {
918
- return buildersMatch[1];
919
- }
920
- // Pattern: worktrees/<protocol>_<id>_<name>
921
- const worktreeMatch = cwd.match(/[/\\]worktrees[/\\]\w+_(\d+)_[^/\\]*(?:[/\\]|$)/);
922
- if (worktreeMatch) {
923
- return worktreeMatch[1];
924
- }
925
- // Method 2: Check for .porch-project marker file
926
- const markerPath = path.join(cwd, '.porch-project');
927
- if (fs.existsSync(markerPath)) {
928
- const content = fs.readFileSync(markerPath, 'utf-8').trim();
929
- if (content) {
930
- return content;
417
+ function getInstructions(state, protocol) {
418
+ const phase = state.phase;
419
+ if (isPhased(protocol, phase) && state.plan_phases.length > 0) {
420
+ const current = getCurrentPlanPhase(state.plan_phases);
421
+ if (current) {
422
+ return ` You are implementing ${current.id}: "${current.title}".\n\n Complete the work, then run: porch check ${state.id}`;
931
423
  }
932
424
  }
933
- // Method 3: Check if there's exactly one project in codev/projects/
934
- try {
935
- const projectRoot = findProjectRoot();
936
- const projects = findProjects(projectRoot);
937
- if (projects.length === 1) {
938
- return projects[0].id;
425
+ const phaseConfig = getPhaseConfig(protocol, phase);
426
+ return ` You are in the ${phaseConfig?.name || phase} phase.\n\n When complete, run: porch done ${state.id}`;
427
+ }
428
+ function getNextAction(state, protocol) {
429
+ const checks = getPhaseChecks(protocol, state.phase);
430
+ const gate = getPhaseGate(protocol, state.phase);
431
+ if (gate && state.gates[gate]?.status === 'pending' && state.gates[gate]?.requested_at) {
432
+ return chalk.yellow('Wait for human to approve the gate.');
433
+ }
434
+ if (isPhased(protocol, state.phase)) {
435
+ const current = getCurrentPlanPhase(state.plan_phases);
436
+ if (current) {
437
+ return `Implement ${current.title} as specified in the plan.`;
939
438
  }
940
439
  }
941
- catch {
942
- // Not in a codev project
440
+ if (Object.keys(checks).length > 0) {
441
+ return `Complete the phase work, then run: porch check ${state.id}`;
943
442
  }
944
- return null;
443
+ return `Complete the phase work, then run: porch done ${state.id}`;
945
444
  }
946
- export async function porch(options) {
947
- const { subcommand, args, dryRun, noClaude, pollInterval, description, worktree } = options;
948
- switch (subcommand.toLowerCase()) {
949
- case 'run': {
950
- let projectId = args[0];
951
- // Auto-detect project if not provided
952
- if (!projectId) {
953
- const detected = autoDetectProject();
954
- if (!detected) {
955
- throw new Error('Usage: porch run <project-id>\n' +
956
- 'Or run from a project worktree to auto-detect.');
445
+ function getArtifactForPhase(projectRoot, state) {
446
+ switch (state.phase) {
447
+ case 'specify':
448
+ return `codev/specs/${state.id}-${state.title}.md`;
449
+ case 'plan':
450
+ return `codev/plans/${state.id}-${state.title}.md`;
451
+ case 'review':
452
+ return `codev/reviews/${state.id}-${state.title}.md`;
453
+ default:
454
+ return null;
455
+ }
456
+ }
457
+ // ============================================================================
458
+ // CLI
459
+ // ============================================================================
460
+ export async function cli(args) {
461
+ const [command, ...rest] = args;
462
+ const projectRoot = process.cwd();
463
+ // Auto-detect project ID for commands that need it
464
+ function getProjectId(provided) {
465
+ if (provided)
466
+ return provided;
467
+ const detected = detectProjectId(projectRoot);
468
+ if (detected) {
469
+ console.log(chalk.dim(`[auto-detected project: ${detected}]`));
470
+ return detected;
471
+ }
472
+ throw new Error('No project ID provided and could not auto-detect.\nProvide ID explicitly or ensure exactly one project exists in codev/projects/');
473
+ }
474
+ try {
475
+ switch (command) {
476
+ case 'run':
477
+ const { run } = await import('./run.js');
478
+ // Parse options for run command
479
+ const runOptions = {};
480
+ let runProjectId;
481
+ for (let i = 0; i < rest.length; i++) {
482
+ if (rest[i] === '--single-iteration' || rest[i] === '-1') {
483
+ runOptions.singleIteration = true;
484
+ }
485
+ else if (rest[i] === '--single-phase') {
486
+ runOptions.singlePhase = true;
487
+ }
488
+ else if (!rest[i].startsWith('--')) {
489
+ runProjectId = rest[i];
490
+ }
957
491
  }
958
- projectId = detected;
959
- console.log(chalk.blue(`[porch] Auto-detected project: ${projectId}`));
960
- }
961
- await run(projectId, { dryRun, noClaude, pollInterval });
962
- break;
963
- }
964
- case 'init': {
965
- if (args.length < 3) {
966
- throw new Error('Usage: porch init <protocol> <project-id> <project-name>');
967
- }
968
- await init(args[0], args[1], args[2], { description, worktree });
969
- break;
970
- }
971
- case 'approve': {
972
- if (args.length < 2) {
973
- throw new Error('Usage: porch approve <project-id> <gate-id>');
974
- }
975
- await approve(args[0], args[1]);
976
- break;
977
- }
978
- case 'status': {
979
- await status(args[0]);
980
- break;
981
- }
982
- case 'pending': {
983
- await pending();
984
- break;
985
- }
986
- case 'list':
987
- case 'list-protocols': {
988
- await list();
989
- break;
990
- }
991
- case 'show':
992
- case 'show-protocol': {
993
- if (args.length < 1) {
994
- throw new Error('Usage: porch show <protocol>');
995
- }
996
- await show(args[0]);
997
- break;
492
+ await run(projectRoot, getProjectId(runProjectId), runOptions);
493
+ break;
494
+ case 'status':
495
+ await status(projectRoot, getProjectId(rest[0]));
496
+ break;
497
+ case 'check':
498
+ await check(projectRoot, getProjectId(rest[0]));
499
+ break;
500
+ case 'done':
501
+ await done(projectRoot, getProjectId(rest[0]));
502
+ break;
503
+ case 'gate':
504
+ await gate(projectRoot, getProjectId(rest[0]));
505
+ break;
506
+ case 'approve':
507
+ if (!rest[0] || !rest[1])
508
+ throw new Error('Usage: porch approve <id> <gate> --a-human-explicitly-approved-this');
509
+ const hasHumanFlag = rest.includes('--a-human-explicitly-approved-this');
510
+ await approve(projectRoot, rest[0], rest[1], hasHumanFlag);
511
+ break;
512
+ case 'init':
513
+ if (!rest[0] || !rest[1] || !rest[2]) {
514
+ throw new Error('Usage: porch init <protocol> <id> <name>');
515
+ }
516
+ await init(projectRoot, rest[0], rest[1], rest[2]);
517
+ break;
518
+ default:
519
+ console.log('porch - Protocol Orchestrator');
520
+ console.log('');
521
+ console.log('Commands:');
522
+ console.log(' run [id] [options] Run the protocol (auto-detects if one project)');
523
+ console.log(' status [id] Show current state and instructions');
524
+ console.log(' check [id] Run checks for current phase');
525
+ console.log(' done [id] Advance to next phase (if checks pass)');
526
+ console.log(' gate [id] Request human approval');
527
+ console.log(' approve <id> <gate> --a-human-explicitly-approved-this');
528
+ console.log(' init <protocol> <id> <name> Initialize a new project');
529
+ console.log('');
530
+ console.log('Run options:');
531
+ console.log(' --single-iteration, -1 Run one build-verify cycle then exit');
532
+ console.log(' --single-phase Run one phase then exit (for Builder/Enforcer pattern)');
533
+ console.log('');
534
+ console.log('Project ID is auto-detected when exactly one project exists.');
535
+ console.log('');
536
+ process.exit(command ? 1 : 0);
998
537
  }
999
- default:
1000
- throw new Error(`Unknown subcommand: ${subcommand}\n` +
1001
- 'Valid subcommands: run, init, approve, status, pending, list, show');
538
+ }
539
+ catch (err) {
540
+ console.error(chalk.red(`Error: ${err.message}`));
541
+ process.exit(1);
1002
542
  }
1003
543
  }
1004
544
  //# sourceMappingURL=index.js.map