@cluesmith/codev 2.0.0-rc.2 → 2.0.0-rc.23

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 (146) hide show
  1. package/bin/porch.js +6 -35
  2. package/dist/agent-farm/cli.d.ts.map +1 -1
  3. package/dist/agent-farm/cli.js +2 -14
  4. package/dist/agent-farm/cli.js.map +1 -1
  5. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  6. package/dist/agent-farm/commands/cleanup.js +29 -2
  7. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  8. package/dist/agent-farm/commands/kickoff.d.ts +1 -0
  9. package/dist/agent-farm/commands/kickoff.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/kickoff.js +151 -77
  11. package/dist/agent-farm/commands/kickoff.js.map +1 -1
  12. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/spawn.js +30 -54
  14. package/dist/agent-farm/commands/spawn.js.map +1 -1
  15. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  16. package/dist/agent-farm/commands/start.js +8 -50
  17. package/dist/agent-farm/commands/start.js.map +1 -1
  18. package/dist/agent-farm/servers/dashboard-server.js +17 -16
  19. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  20. package/dist/agent-farm/state.d.ts +0 -10
  21. package/dist/agent-farm/state.d.ts.map +1 -1
  22. package/dist/agent-farm/state.js +0 -24
  23. package/dist/agent-farm/state.js.map +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +5 -17
  26. package/dist/cli.js.map +1 -1
  27. package/dist/commands/adopt.d.ts.map +1 -1
  28. package/dist/commands/adopt.js +17 -1
  29. package/dist/commands/adopt.js.map +1 -1
  30. package/dist/commands/consult/index.d.ts.map +1 -1
  31. package/dist/commands/consult/index.js +83 -2
  32. package/dist/commands/consult/index.js.map +1 -1
  33. package/dist/commands/init.d.ts.map +1 -1
  34. package/dist/commands/init.js +17 -1
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/porch/checks.d.ts +16 -29
  37. package/dist/commands/porch/checks.d.ts.map +1 -1
  38. package/dist/commands/porch/checks.js +90 -144
  39. package/dist/commands/porch/checks.js.map +1 -1
  40. package/dist/commands/porch/claude.d.ts +29 -0
  41. package/dist/commands/porch/claude.d.ts.map +1 -0
  42. package/dist/commands/porch/claude.js +80 -0
  43. package/dist/commands/porch/claude.js.map +1 -0
  44. package/dist/commands/porch/index.d.ts +21 -43
  45. package/dist/commands/porch/index.d.ts.map +1 -1
  46. package/dist/commands/porch/index.js +468 -753
  47. package/dist/commands/porch/index.js.map +1 -1
  48. package/dist/commands/porch/plan.d.ts +60 -0
  49. package/dist/commands/porch/plan.d.ts.map +1 -0
  50. package/dist/commands/porch/plan.js +162 -0
  51. package/dist/commands/porch/plan.js.map +1 -0
  52. package/dist/commands/porch/prompts.d.ts +19 -0
  53. package/dist/commands/porch/prompts.d.ts.map +1 -0
  54. package/dist/commands/porch/prompts.js +270 -0
  55. package/dist/commands/porch/prompts.js.map +1 -0
  56. package/dist/commands/porch/protocol.d.ts +59 -0
  57. package/dist/commands/porch/protocol.d.ts.map +1 -0
  58. package/dist/commands/porch/protocol.js +252 -0
  59. package/dist/commands/porch/protocol.js.map +1 -0
  60. package/dist/commands/porch/repl.d.ts +33 -0
  61. package/dist/commands/porch/repl.d.ts.map +1 -0
  62. package/dist/commands/porch/repl.js +206 -0
  63. package/dist/commands/porch/repl.js.map +1 -0
  64. package/dist/commands/porch/run.d.ts +23 -0
  65. package/dist/commands/porch/run.d.ts.map +1 -0
  66. package/dist/commands/porch/run.js +743 -0
  67. package/dist/commands/porch/run.js.map +1 -0
  68. package/dist/commands/porch/signals.d.ts +38 -0
  69. package/dist/commands/porch/signals.d.ts.map +1 -0
  70. package/dist/commands/porch/signals.js +81 -0
  71. package/dist/commands/porch/signals.js.map +1 -0
  72. package/dist/commands/porch/state.d.ts +23 -112
  73. package/dist/commands/porch/state.d.ts.map +1 -1
  74. package/dist/commands/porch/state.js +119 -680
  75. package/dist/commands/porch/state.js.map +1 -1
  76. package/dist/commands/porch/types.d.ts +69 -173
  77. package/dist/commands/porch/types.d.ts.map +1 -1
  78. package/dist/commands/porch/types.js +2 -1
  79. package/dist/commands/porch/types.js.map +1 -1
  80. package/dist/commands/update.d.ts.map +1 -1
  81. package/dist/commands/update.js +12 -0
  82. package/dist/commands/update.js.map +1 -1
  83. package/dist/lib/scaffold.d.ts +24 -0
  84. package/dist/lib/scaffold.d.ts.map +1 -1
  85. package/dist/lib/scaffold.js +78 -0
  86. package/dist/lib/scaffold.js.map +1 -1
  87. package/package.json +7 -2
  88. package/skeleton/protocols/spider/prompts/implement.md +201 -0
  89. package/skeleton/protocols/spider/prompts/plan.md +214 -0
  90. package/skeleton/protocols/spider/prompts/review.md +217 -0
  91. package/skeleton/protocols/spider/prompts/specify.md +192 -0
  92. package/skeleton/protocols/spider/protocol.json +79 -147
  93. package/skeleton/protocols/spider/templates/plan.md +14 -0
  94. package/skeleton/roles/architect.md +140 -319
  95. package/skeleton/roles/builder.md +135 -213
  96. package/templates/dashboard/index.html +0 -27
  97. package/templates/dashboard/js/utils.js +0 -86
  98. package/dist/agent-farm/commands/rename.d.ts +0 -13
  99. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  100. package/dist/agent-farm/commands/rename.js +0 -33
  101. package/dist/agent-farm/commands/rename.js.map +0 -1
  102. package/dist/commands/pcheck/cache.d.ts +0 -48
  103. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  104. package/dist/commands/pcheck/cache.js +0 -170
  105. package/dist/commands/pcheck/cache.js.map +0 -1
  106. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  107. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  108. package/dist/commands/pcheck/evaluator.js +0 -246
  109. package/dist/commands/pcheck/evaluator.js.map +0 -1
  110. package/dist/commands/pcheck/index.d.ts +0 -12
  111. package/dist/commands/pcheck/index.d.ts.map +0 -1
  112. package/dist/commands/pcheck/index.js +0 -249
  113. package/dist/commands/pcheck/index.js.map +0 -1
  114. package/dist/commands/pcheck/parser.d.ts +0 -39
  115. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  116. package/dist/commands/pcheck/parser.js +0 -155
  117. package/dist/commands/pcheck/parser.js.map +0 -1
  118. package/dist/commands/pcheck/types.d.ts +0 -82
  119. package/dist/commands/pcheck/types.d.ts.map +0 -1
  120. package/dist/commands/pcheck/types.js +0 -5
  121. package/dist/commands/pcheck/types.js.map +0 -1
  122. package/dist/commands/porch/consultation.d.ts +0 -56
  123. package/dist/commands/porch/consultation.d.ts.map +0 -1
  124. package/dist/commands/porch/consultation.js +0 -330
  125. package/dist/commands/porch/consultation.js.map +0 -1
  126. package/dist/commands/porch/notifications.d.ts +0 -99
  127. package/dist/commands/porch/notifications.d.ts.map +0 -1
  128. package/dist/commands/porch/notifications.js +0 -223
  129. package/dist/commands/porch/notifications.js.map +0 -1
  130. package/dist/commands/porch/plan-parser.d.ts +0 -38
  131. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  132. package/dist/commands/porch/plan-parser.js +0 -166
  133. package/dist/commands/porch/plan-parser.js.map +0 -1
  134. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  135. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  136. package/dist/commands/porch/protocol-loader.js +0 -249
  137. package/dist/commands/porch/protocol-loader.js.map +0 -1
  138. package/dist/commands/porch/signal-parser.d.ts +0 -88
  139. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  140. package/dist/commands/porch/signal-parser.js +0 -148
  141. package/dist/commands/porch/signal-parser.js.map +0 -1
  142. package/skeleton/porch/protocols/bugfix.json +0 -85
  143. package/skeleton/porch/protocols/spider.json +0 -135
  144. package/skeleton/porch/protocols/tick.json +0 -76
  145. package/templates/dashboard/css/activity.css +0 -151
  146. package/templates/dashboard/js/activity.js +0 -112
@@ -0,0 +1,743 @@
1
+ /**
2
+ * porch run - Main run loop (Build-Verify design)
3
+ *
4
+ * Porch orchestrates build-verify cycles:
5
+ * 1. BUILD: Spawn Claude to create artifact
6
+ * 2. VERIFY: Run 3-way consultation (Gemini, Codex, Claude)
7
+ * 3. ITERATE: If any REQUEST_CHANGES, feed back to Claude
8
+ * 4. COMPLETE: When all APPROVE (or max iterations), commit + push + gate
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import chalk from 'chalk';
13
+ import { readState, writeState, findStatusPath } from './state.js';
14
+ import { loadProtocol, getPhaseConfig, isPhased, getPhaseGate, isBuildVerify, getVerifyConfig, getMaxIterations, getOnCompleteConfig, getBuildConfig } from './protocol.js';
15
+ import { getCurrentPlanPhase } from './plan.js';
16
+ import { spawnClaude } from './claude.js';
17
+ import { runRepl } from './repl.js';
18
+ import { buildPhasePrompt } from './prompts.js';
19
+ // Runtime artifacts go in project directory, not a hidden folder
20
+ function getPorchDir(projectRoot, state) {
21
+ return path.join(projectRoot, 'codev', 'projects', `${state.id}-${state.title}`);
22
+ }
23
+ /**
24
+ * Generate output file name with phase and iteration info.
25
+ * e.g., "0074-specify-iter-1.txt" or "0074-phase_1-iter-2.txt"
26
+ *
27
+ * Uses state.iteration which is persisted and survives porch restarts.
28
+ */
29
+ function getOutputFileName(state) {
30
+ const planPhase = getCurrentPlanPhase(state.plan_phases);
31
+ // Build filename using persisted iteration from state
32
+ const parts = [state.id];
33
+ if (planPhase) {
34
+ parts.push(planPhase.id);
35
+ }
36
+ else {
37
+ parts.push(state.phase);
38
+ }
39
+ parts.push(`iter-${state.iteration}`);
40
+ return `${parts.join('-')}.txt`;
41
+ }
42
+ /** Exit code when AWAITING_INPUT is detected in non-interactive mode */
43
+ export const EXIT_AWAITING_INPUT = 10;
44
+ /**
45
+ * Main run loop for porch.
46
+ * Spawns Claude for each phase and monitors until protocol complete.
47
+ */
48
+ export async function run(projectRoot, projectId, options = {}) {
49
+ const statusPath = findStatusPath(projectRoot, projectId);
50
+ if (!statusPath) {
51
+ throw new Error(`Project ${projectId} not found.\nRun 'porch init' to create a new project.`);
52
+ }
53
+ // Read initial state to get project directory
54
+ let state = readState(statusPath);
55
+ const singleIteration = options.singleIteration || false;
56
+ let iterationCompleted = false; // Track if we completed a build-verify cycle
57
+ // Ensure project artifacts directory exists
58
+ const porchDir = getPorchDir(projectRoot, state);
59
+ if (!fs.existsSync(porchDir)) {
60
+ fs.mkdirSync(porchDir, { recursive: true });
61
+ }
62
+ console.log('');
63
+ console.log(chalk.bold('PORCH - Protocol Orchestrator'));
64
+ console.log(chalk.dim('Porch is the outer loop. Claude runs under porch control.'));
65
+ console.log('');
66
+ while (true) {
67
+ state = readState(statusPath);
68
+ const protocol = loadProtocol(projectRoot, state.protocol);
69
+ const phaseConfig = getPhaseConfig(protocol, state.phase);
70
+ if (!phaseConfig) {
71
+ console.log(chalk.green.bold('🎉 PROTOCOL COMPLETE'));
72
+ console.log(`\n Project ${state.id} has completed the ${state.protocol} protocol.`);
73
+ break;
74
+ }
75
+ // Check for pending gate
76
+ const gateName = getPhaseGate(protocol, state.phase);
77
+ if (gateName && state.gates[gateName]?.status === 'pending' && state.gates[gateName]?.requested_at) {
78
+ const outputPath = path.join(porchDir, `${state.id}-gate.txt`);
79
+ await handleGate(state, gateName, statusPath, projectRoot, outputPath, protocol);
80
+ continue;
81
+ }
82
+ // Handle build_verify phases
83
+ if (isBuildVerify(protocol, state.phase)) {
84
+ const maxIterations = getMaxIterations(protocol, state.phase);
85
+ // Check if we need to run VERIFY (build just completed)
86
+ if (state.build_complete) {
87
+ // First check if the artifact was actually created
88
+ const artifactPath = getArtifactForPhase(state);
89
+ if (artifactPath) {
90
+ const fullPath = path.join(projectRoot, artifactPath);
91
+ if (!fs.existsSync(fullPath)) {
92
+ console.log('');
93
+ console.log(chalk.yellow(`Artifact not found: ${artifactPath}`));
94
+ console.log(chalk.dim('Claude may have asked questions or encountered an error.'));
95
+ console.log(chalk.dim('Check the output file for details, then respawn.'));
96
+ state.build_complete = false;
97
+ writeState(statusPath, state);
98
+ continue;
99
+ }
100
+ }
101
+ console.log('');
102
+ console.log(chalk.cyan(`[${state.id}] VERIFY - Iteration ${state.iteration}/${maxIterations}`));
103
+ const reviews = await runVerification(projectRoot, state, protocol);
104
+ // Get the build output file from current iteration (stored when we track it)
105
+ const currentBuildOutput = state.history.find(h => h.iteration === state.iteration)?.build_output || '';
106
+ // Update history with reviews
107
+ const existingRecord = state.history.find(h => h.iteration === state.iteration);
108
+ if (existingRecord) {
109
+ existingRecord.reviews = reviews;
110
+ }
111
+ else {
112
+ state.history.push({
113
+ iteration: state.iteration,
114
+ build_output: currentBuildOutput,
115
+ reviews,
116
+ });
117
+ }
118
+ if (allApprove(reviews)) {
119
+ console.log(chalk.green('\nAll reviewers APPROVE!'));
120
+ // Run on_complete actions (commit + push)
121
+ await runOnComplete(projectRoot, state, protocol, reviews);
122
+ // Request gate
123
+ if (gateName) {
124
+ state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() };
125
+ }
126
+ // Reset for next phase
127
+ state.build_complete = false;
128
+ state.iteration = 1;
129
+ state.history = [];
130
+ writeState(statusPath, state);
131
+ // Single iteration mode: exit after completing a build-verify cycle
132
+ if (singleIteration) {
133
+ console.log(chalk.dim('\n[--single-iteration] Build-verify cycle complete. Exiting.'));
134
+ return;
135
+ }
136
+ continue;
137
+ }
138
+ // Some reviewers requested changes
139
+ console.log(chalk.yellow('\nChanges requested. Feeding back to Claude...'));
140
+ if (state.iteration >= maxIterations) {
141
+ // Max iterations reached without unanimity - summarize and interrupt user
142
+ console.log('');
143
+ console.log(chalk.red('═'.repeat(60)));
144
+ console.log(chalk.red.bold(' MAX ITERATIONS REACHED - NO UNANIMITY'));
145
+ console.log(chalk.red('═'.repeat(60)));
146
+ console.log('');
147
+ console.log(chalk.yellow(`After ${maxIterations} iterations, reviewers did not reach unanimity.`));
148
+ console.log('');
149
+ console.log(chalk.bold('Summary of reviewer positions:'));
150
+ // Group reviews by verdict
151
+ const byVerdict = {};
152
+ for (const r of reviews) {
153
+ if (!byVerdict[r.verdict])
154
+ byVerdict[r.verdict] = [];
155
+ byVerdict[r.verdict].push(r.model);
156
+ }
157
+ for (const [verdict, models] of Object.entries(byVerdict)) {
158
+ const color = verdict === 'APPROVE' ? chalk.green :
159
+ verdict === 'CONSULT_ERROR' ? chalk.red :
160
+ verdict === 'REQUEST_CHANGES' ? chalk.yellow : chalk.blue;
161
+ console.log(` ${color(verdict)}: ${models.join(', ')}`);
162
+ }
163
+ console.log('');
164
+ console.log(chalk.dim('Review files:'));
165
+ for (const r of reviews) {
166
+ console.log(` ${r.model}: ${r.file}`);
167
+ }
168
+ console.log('');
169
+ // Check for identical REQUEST_CHANGES (may indicate missing context)
170
+ const requestChangesReviews = reviews.filter(r => r.verdict === 'REQUEST_CHANGES');
171
+ if (requestChangesReviews.length >= 2) {
172
+ console.log(chalk.yellow('Note: Multiple REQUEST_CHANGES may indicate missing file context.'));
173
+ console.log(chalk.dim('Check if the artifact path is correct and files are committed.'));
174
+ console.log('');
175
+ }
176
+ // Wait for user decision
177
+ const readline = await import('node:readline');
178
+ const rl = readline.createInterface({
179
+ input: process.stdin,
180
+ output: process.stdout,
181
+ });
182
+ console.log('Options:');
183
+ console.log(" 'c' or 'continue' - Proceed to gate anyway (let human decide)");
184
+ console.log(" 'r' or 'retry' - Reset iteration counter and try again");
185
+ console.log(" 'q' or 'quit' - Exit porch");
186
+ console.log('');
187
+ const action = await new Promise((resolve) => {
188
+ rl.question(chalk.cyan(`[${state.id}] > `), (input) => {
189
+ rl.close();
190
+ resolve(input.trim().toLowerCase());
191
+ });
192
+ });
193
+ switch (action) {
194
+ case 'c':
195
+ case 'continue':
196
+ console.log(chalk.dim('\nProceeding to gate...'));
197
+ break;
198
+ case 'r':
199
+ case 'retry':
200
+ console.log(chalk.dim('\nResetting iteration counter...'));
201
+ state.iteration = 1;
202
+ state.build_complete = false;
203
+ state.history = [];
204
+ writeState(statusPath, state);
205
+ continue;
206
+ case 'q':
207
+ case 'quit':
208
+ console.log(chalk.yellow('\nExiting porch.'));
209
+ return;
210
+ default:
211
+ console.log(chalk.yellow('\nUnknown option. Proceeding to gate.'));
212
+ }
213
+ // Run on_complete actions
214
+ await runOnComplete(projectRoot, state, protocol, reviews);
215
+ // Request gate
216
+ if (gateName) {
217
+ state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() };
218
+ }
219
+ state.build_complete = false;
220
+ state.iteration = 1;
221
+ state.history = [];
222
+ writeState(statusPath, state);
223
+ // Single iteration mode: exit after max iterations
224
+ if (singleIteration) {
225
+ console.log(chalk.dim('\n[--single-iteration] Max iterations reached. Exiting.'));
226
+ return;
227
+ }
228
+ continue;
229
+ }
230
+ // Increment iteration and continue to BUILD
231
+ state.iteration++;
232
+ state.build_complete = false;
233
+ writeState(statusPath, state);
234
+ // Single iteration mode: exit after storing feedback
235
+ if (singleIteration) {
236
+ console.log(chalk.dim('\n[--single-iteration] Feedback stored for next iteration. Exiting.'));
237
+ console.log(chalk.dim(` Next run will be iteration ${state.iteration} with reviewer feedback.`));
238
+ return;
239
+ }
240
+ // Fall through to BUILD phase
241
+ }
242
+ // BUILD phase
243
+ console.log('');
244
+ console.log(chalk.cyan(`[${state.id}] BUILD - ${phaseConfig.name} - Iteration ${state.iteration}/${maxIterations}`));
245
+ }
246
+ // Generate output file for this iteration
247
+ const outputFileName = getOutputFileName(state);
248
+ const outputPath = path.join(porchDir, outputFileName);
249
+ // Track this build output in history (for feedback to next iteration)
250
+ if (isBuildVerify(protocol, state.phase)) {
251
+ const existingRecord = state.history.find(h => h.iteration === state.iteration);
252
+ if (existingRecord) {
253
+ existingRecord.build_output = outputPath;
254
+ }
255
+ else {
256
+ state.history.push({
257
+ iteration: state.iteration,
258
+ build_output: outputPath,
259
+ reviews: [],
260
+ });
261
+ }
262
+ writeState(statusPath, state);
263
+ }
264
+ // Build prompt for current phase (includes history file paths if iteration > 1)
265
+ const prompt = buildPhasePrompt(projectRoot, state, protocol);
266
+ // Create output file
267
+ fs.writeFileSync(outputPath, '');
268
+ console.log(chalk.dim(`Output: ${outputFileName}`));
269
+ // Show status
270
+ showStatus(state, protocol);
271
+ // Print the prompt being sent to Claude
272
+ console.log('');
273
+ console.log(chalk.cyan('═'.repeat(60)));
274
+ console.log(chalk.cyan.bold(' PROMPT TO CLAUDE'));
275
+ console.log(chalk.cyan('═'.repeat(60)));
276
+ console.log(chalk.dim(prompt.substring(0, 2000)));
277
+ if (prompt.length > 2000) {
278
+ console.log(chalk.dim(`... (${prompt.length - 2000} more chars)`));
279
+ }
280
+ console.log(chalk.cyan('═'.repeat(60)));
281
+ console.log('');
282
+ // Spawn Claude
283
+ console.log(chalk.dim('Starting Claude...'));
284
+ const claude = spawnClaude(prompt, outputPath, projectRoot);
285
+ // Run REPL while Claude works
286
+ const action = await runRepl(state, claude, outputPath, statusPath, projectRoot, protocol);
287
+ // Handle REPL result
288
+ switch (action.type) {
289
+ case 'quit':
290
+ claude.kill();
291
+ console.log(chalk.yellow('\nPorch terminated by user.'));
292
+ return;
293
+ case 'signal':
294
+ const shouldRespawn = await handleSignal(action.signal, state, statusPath, projectRoot, protocol);
295
+ if (shouldRespawn) {
296
+ console.log(chalk.dim('\nRespawning Claude for retry...'));
297
+ await sleep(1000);
298
+ }
299
+ break;
300
+ case 'claude_exit':
301
+ // For build_verify phases, ANY Claude exit = build complete
302
+ // Don't respawn - go straight to verification
303
+ if (isBuildVerify(protocol, state.phase)) {
304
+ console.log(chalk.dim('\nClaude finished. Moving to verification...'));
305
+ state.build_complete = true;
306
+ writeState(statusPath, state);
307
+ // Continue loop - will hit build_complete check and run verify
308
+ }
309
+ else if (action.exitCode !== 0) {
310
+ console.log(chalk.red(`\nClaude exited with code ${action.exitCode}`));
311
+ console.log(chalk.dim('Restarting in 3 seconds...'));
312
+ await sleep(3000);
313
+ }
314
+ break;
315
+ case 'approved':
316
+ // Gate was approved, continue to next phase
317
+ break;
318
+ case 'manual_claude':
319
+ // User wants to intervene - just continue loop to respawn
320
+ console.log(chalk.dim('\nRespawning Claude...'));
321
+ break;
322
+ }
323
+ }
324
+ }
325
+ // ============================================================================
326
+ // Verification (3-way consultation)
327
+ // ============================================================================
328
+ /**
329
+ * Run 3-way verification on the current phase artifact.
330
+ * Writes each consultation output to a file.
331
+ * Returns array of review results with file paths.
332
+ */
333
+ async function runVerification(projectRoot, state, protocol) {
334
+ const verifyConfig = getVerifyConfig(protocol, state.phase);
335
+ if (!verifyConfig) {
336
+ return []; // No verification configured
337
+ }
338
+ console.log(chalk.dim(`Running ${verifyConfig.models.length}-way consultation...`));
339
+ const porchDir = getPorchDir(projectRoot, state);
340
+ const reviews = [];
341
+ // Run consultations in parallel
342
+ const promises = verifyConfig.models.map(async (model) => {
343
+ console.log(chalk.dim(` ${model}: starting...`));
344
+ // Output file for this review
345
+ const reviewFile = path.join(porchDir, `${state.id}-${state.phase}-iter${state.iteration}-${model}.txt`);
346
+ const result = await runConsult(projectRoot, model, verifyConfig.type, state, reviewFile);
347
+ reviews.push(result);
348
+ const verdictColor = result.verdict === 'APPROVE' ? chalk.green :
349
+ result.verdict === 'COMMENT' ? chalk.blue : chalk.yellow;
350
+ console.log(` ${model}: ${verdictColor(result.verdict)}`);
351
+ });
352
+ await Promise.all(promises);
353
+ return reviews;
354
+ }
355
+ /**
356
+ * Get the consult artifact type for a phase.
357
+ */
358
+ function getConsultArtifactType(phaseId) {
359
+ switch (phaseId) {
360
+ case 'specify':
361
+ return 'spec';
362
+ case 'plan':
363
+ return 'plan';
364
+ case 'implement':
365
+ return 'impl'; // Implementation reviews the code diff
366
+ case 'review':
367
+ return 'spec'; // Review phase reviews overall work
368
+ default:
369
+ return 'spec';
370
+ }
371
+ }
372
+ /**
373
+ * Run a single consultation with retry on failure.
374
+ * Writes output to file and returns result with file path.
375
+ *
376
+ * Retry logic:
377
+ * - Non-zero exit code = consultation failed (API key missing, network error, etc.)
378
+ * - Retry up to 3 times with exponential backoff
379
+ * - If all retries fail, return CONSULT_ERROR (not REQUEST_CHANGES)
380
+ */
381
+ const CONSULT_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
382
+ const CONSULT_MAX_RETRIES = 3;
383
+ const CONSULT_RETRY_DELAYS = [5000, 15000, 30000]; // 5s, 15s, 30s
384
+ async function runConsult(projectRoot, model, reviewType, state, outputFile) {
385
+ for (let attempt = 0; attempt < CONSULT_MAX_RETRIES; attempt++) {
386
+ const result = await runConsultOnce(projectRoot, model, reviewType, state, outputFile);
387
+ // Success - got a valid verdict
388
+ if (result.verdict !== 'CONSULT_ERROR') {
389
+ return result;
390
+ }
391
+ // Consultation failed - retry if attempts remaining
392
+ if (attempt < CONSULT_MAX_RETRIES - 1) {
393
+ const delay = CONSULT_RETRY_DELAYS[attempt];
394
+ console.log(chalk.yellow(` ${model}: failed, retrying in ${delay / 1000}s... (attempt ${attempt + 2}/${CONSULT_MAX_RETRIES})`));
395
+ await sleep(delay);
396
+ }
397
+ }
398
+ // All retries failed
399
+ console.log(chalk.red(` ${model}: FAILED after ${CONSULT_MAX_RETRIES} attempts`));
400
+ return { model, verdict: 'CONSULT_ERROR', file: outputFile };
401
+ }
402
+ async function runConsultOnce(projectRoot, model, reviewType, state, outputFile) {
403
+ const { spawn } = await import('node:child_process');
404
+ const artifactType = getConsultArtifactType(state.phase);
405
+ return new Promise((resolve) => {
406
+ const args = ['--model', model, '--type', reviewType, artifactType, state.id];
407
+ const proc = spawn('consult', args, {
408
+ cwd: projectRoot,
409
+ stdio: ['pipe', 'pipe', 'pipe'],
410
+ });
411
+ let output = '';
412
+ let resolved = false;
413
+ let exitCode = null;
414
+ // Timeout after 1 hour
415
+ const timeout = setTimeout(() => {
416
+ if (!resolved) {
417
+ resolved = true;
418
+ proc.kill('SIGTERM');
419
+ const timeoutOutput = output + '\n\n[TIMEOUT: Consultation exceeded 1 hour limit]';
420
+ fs.writeFileSync(outputFile, timeoutOutput);
421
+ console.log(chalk.yellow(` ${model}: timeout (1 hour limit)`));
422
+ resolve({ model, verdict: 'CONSULT_ERROR', file: outputFile });
423
+ }
424
+ }, CONSULT_TIMEOUT_MS);
425
+ proc.stdout.on('data', (data) => { output += data.toString(); });
426
+ proc.stderr.on('data', (data) => { output += data.toString(); });
427
+ proc.on('close', (code) => {
428
+ if (!resolved) {
429
+ resolved = true;
430
+ clearTimeout(timeout);
431
+ exitCode = code;
432
+ // Write output to file
433
+ fs.writeFileSync(outputFile, output);
434
+ // Non-zero exit code = consultation failed (API key missing, etc.)
435
+ if (code !== 0) {
436
+ console.log(chalk.yellow(` ${model}: exit code ${code}`));
437
+ resolve({ model, verdict: 'CONSULT_ERROR', file: outputFile });
438
+ return;
439
+ }
440
+ // Parse verdict from output
441
+ const verdict = parseVerdict(output);
442
+ resolve({ model, verdict, file: outputFile });
443
+ }
444
+ });
445
+ proc.on('error', (err) => {
446
+ if (!resolved) {
447
+ resolved = true;
448
+ clearTimeout(timeout);
449
+ const errorOutput = `Error: ${err.message}`;
450
+ fs.writeFileSync(outputFile, errorOutput);
451
+ console.log(chalk.red(` ${model}: error - ${err.message}`));
452
+ resolve({ model, verdict: 'CONSULT_ERROR', file: outputFile });
453
+ }
454
+ });
455
+ });
456
+ }
457
+ /**
458
+ * Parse verdict from consultation output.
459
+ *
460
+ * Looks for the verdict line in format:
461
+ * VERDICT: APPROVE
462
+ * VERDICT: REQUEST_CHANGES
463
+ * VERDICT: COMMENT
464
+ *
465
+ * Also handles markdown formatting like:
466
+ * **VERDICT: APPROVE**
467
+ * *VERDICT: APPROVE*
468
+ *
469
+ * Safety: If no explicit verdict found (empty output, crash, malformed),
470
+ * defaults to REQUEST_CHANGES to prevent proceeding with unverified code.
471
+ */
472
+ function parseVerdict(output) {
473
+ // Empty or very short output = something went wrong
474
+ if (!output || output.trim().length < 50) {
475
+ return 'REQUEST_CHANGES';
476
+ }
477
+ // Look for actual verdict line (not template text like "[APPROVE | REQUEST_CHANGES | COMMENT]")
478
+ // Match lines like "VERDICT: APPROVE" or "**VERDICT: APPROVE**"
479
+ const lines = output.split('\n');
480
+ for (const line of lines) {
481
+ // Strip markdown formatting (**, *, __, _) and trim
482
+ const stripped = line.trim().replace(/^[\*_]+|[\*_]+$/g, '').trim().toUpperCase();
483
+ // Match "VERDICT: <value>" but NOT "VERDICT: [APPROVE | ...]"
484
+ if (stripped.startsWith('VERDICT:') && !stripped.includes('[')) {
485
+ if (stripped.includes('REQUEST_CHANGES')) {
486
+ return 'REQUEST_CHANGES';
487
+ }
488
+ if (stripped.includes('APPROVE')) {
489
+ return 'APPROVE';
490
+ }
491
+ if (stripped.includes('COMMENT')) {
492
+ return 'COMMENT';
493
+ }
494
+ }
495
+ }
496
+ // Fallback: look anywhere in output (legacy behavior)
497
+ const upperOutput = output.toUpperCase();
498
+ if (upperOutput.includes('REQUEST_CHANGES')) {
499
+ return 'REQUEST_CHANGES';
500
+ }
501
+ if (upperOutput.includes('APPROVE')) {
502
+ return 'APPROVE';
503
+ }
504
+ // No explicit verdict = default to REQUEST_CHANGES for safety
505
+ return 'REQUEST_CHANGES';
506
+ }
507
+ /**
508
+ * Check if all reviewers approved (unanimity required).
509
+ *
510
+ * Returns true only if ALL reviewers explicitly APPROVE.
511
+ * COMMENT counts as approve (non-blocking feedback).
512
+ * CONSULT_ERROR and REQUEST_CHANGES block approval.
513
+ */
514
+ function allApprove(reviews) {
515
+ if (reviews.length === 0)
516
+ return true; // No verification = auto-approve
517
+ // Unanimity: ALL must be APPROVE or COMMENT
518
+ return reviews.every(r => r.verdict === 'APPROVE' || r.verdict === 'COMMENT');
519
+ }
520
+ /**
521
+ * Run on_complete actions (commit + push).
522
+ */
523
+ async function runOnComplete(projectRoot, state, protocol, reviews) {
524
+ const onComplete = getOnCompleteConfig(protocol, state.phase);
525
+ if (!onComplete)
526
+ return;
527
+ const buildConfig = getBuildConfig(protocol, state.phase);
528
+ if (!buildConfig)
529
+ return;
530
+ // Resolve artifact path
531
+ const artifact = buildConfig.artifact
532
+ .replace('${PROJECT_ID}', state.id)
533
+ .replace('${PROJECT_TITLE}', state.title);
534
+ const { exec } = await import('node:child_process');
535
+ const { promisify } = await import('node:util');
536
+ const execAsync = promisify(exec);
537
+ if (onComplete.commit) {
538
+ console.log(chalk.dim('Committing...'));
539
+ try {
540
+ // Stage artifact
541
+ await execAsync(`git add ${artifact}`, { cwd: projectRoot });
542
+ // Commit
543
+ const message = `[Spec ${state.id}] ${state.phase}: ${state.title}
544
+
545
+ Iteration ${state.iteration}
546
+ 3-way review: ${formatVerdicts(reviews)}`;
547
+ await execAsync(`git commit -m "${message}"`, { cwd: projectRoot });
548
+ console.log(chalk.green('Committed.'));
549
+ }
550
+ catch (err) {
551
+ console.log(chalk.yellow('Commit failed (may be nothing to commit).'));
552
+ }
553
+ }
554
+ if (onComplete.push) {
555
+ console.log(chalk.dim('Pushing...'));
556
+ try {
557
+ await execAsync('git push', { cwd: projectRoot });
558
+ console.log(chalk.green('Pushed.'));
559
+ }
560
+ catch (err) {
561
+ console.log(chalk.yellow('Push failed.'));
562
+ }
563
+ }
564
+ }
565
+ /**
566
+ * Format verdicts for commit message.
567
+ */
568
+ function formatVerdicts(reviews) {
569
+ return reviews
570
+ .map(r => `${r.model}=${r.verdict}`)
571
+ .join(', ') || 'N/A';
572
+ }
573
+ /**
574
+ * Display current status.
575
+ */
576
+ function showStatus(state, protocol) {
577
+ const phaseConfig = getPhaseConfig(protocol, state.phase);
578
+ console.log('');
579
+ console.log(chalk.bold(`[${state.id}] ${state.title}`));
580
+ console.log(` Phase: ${state.phase} (${phaseConfig?.name || 'unknown'})`);
581
+ if (isBuildVerify(protocol, state.phase)) {
582
+ const maxIterations = getMaxIterations(protocol, state.phase);
583
+ console.log(` Iteration: ${state.iteration}/${maxIterations}`);
584
+ }
585
+ if (isPhased(protocol, state.phase) && state.plan_phases.length > 0) {
586
+ const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
587
+ if (currentPlanPhase) {
588
+ console.log(` Plan Phase: ${currentPlanPhase.id} - ${currentPlanPhase.title}`);
589
+ }
590
+ }
591
+ console.log('');
592
+ }
593
+ /**
594
+ * Handle gate approval flow.
595
+ */
596
+ async function handleGate(state, gateName, statusPath, projectRoot, outputPath, protocol) {
597
+ // E2E testing: Auto-approve gates when PORCH_AUTO_APPROVE is set
598
+ if (process.env.PORCH_AUTO_APPROVE === 'true') {
599
+ console.log(chalk.yellow(`[E2E] Auto-approving gate: ${gateName}`));
600
+ state.gates[gateName].status = 'approved';
601
+ state.gates[gateName].approved_at = new Date().toISOString();
602
+ writeState(statusPath, state);
603
+ return;
604
+ }
605
+ console.log('');
606
+ console.log(chalk.yellow('═'.repeat(60)));
607
+ console.log(chalk.yellow.bold(` GATE: ${gateName}`));
608
+ console.log(chalk.yellow('═'.repeat(60)));
609
+ console.log('');
610
+ // Show artifact path
611
+ const artifact = getArtifactForPhase(state);
612
+ if (artifact) {
613
+ console.log(` Review: ${artifact}`);
614
+ }
615
+ console.log('');
616
+ console.log(" Type 'a' or 'approve' to approve and continue.");
617
+ console.log(" Type 'q' or 'quit' to exit.");
618
+ console.log('');
619
+ // Wait for user input
620
+ const readline = await import('node:readline');
621
+ const rl = readline.createInterface({
622
+ input: process.stdin,
623
+ output: process.stdout,
624
+ });
625
+ return new Promise((resolve) => {
626
+ const prompt = () => {
627
+ rl.question(chalk.cyan(`[${state.id}] WAITING FOR APPROVAL > `), (input) => {
628
+ const cmd = input.trim().toLowerCase();
629
+ switch (cmd) {
630
+ case 'a':
631
+ case 'approve':
632
+ state.gates[gateName].status = 'approved';
633
+ state.gates[gateName].approved_at = new Date().toISOString();
634
+ writeState(statusPath, state);
635
+ console.log(chalk.green(`\nGate ${gateName} approved.`));
636
+ rl.close();
637
+ resolve();
638
+ break;
639
+ case 'q':
640
+ case 'quit':
641
+ console.log(chalk.yellow('\nExiting without approval.'));
642
+ rl.close();
643
+ process.exit(0);
644
+ break;
645
+ default:
646
+ console.log(chalk.dim("Unknown command. Type 'a' to approve or 'q' to quit."));
647
+ prompt();
648
+ }
649
+ });
650
+ };
651
+ prompt();
652
+ });
653
+ }
654
+ /**
655
+ * Handle signal from Claude output.
656
+ * Returns true if should respawn Claude (for build-verify iteration), false otherwise.
657
+ */
658
+ async function handleSignal(signal, state, statusPath, projectRoot, protocol) {
659
+ console.log('');
660
+ switch (signal.type) {
661
+ case 'PHASE_COMPLETE':
662
+ console.log(chalk.green('Signal: PHASE_COMPLETE'));
663
+ // For build_verify phases, we'll run verification in the main loop
664
+ // Mark build as complete so main loop knows to run verify
665
+ if (isBuildVerify(protocol, state.phase)) {
666
+ state.build_complete = true;
667
+ writeState(statusPath, state);
668
+ return false; // Main loop will handle verify
669
+ }
670
+ // For non-build_verify phases, advance state directly
671
+ const { done } = await import('./index.js');
672
+ await done(projectRoot, state.id);
673
+ return false;
674
+ case 'GATE_NEEDED':
675
+ console.log(chalk.yellow('Signal: GATE_NEEDED'));
676
+ const gateName = getPhaseGate(protocol, state.phase);
677
+ if (gateName && !state.gates[gateName]) {
678
+ state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() };
679
+ writeState(statusPath, state);
680
+ }
681
+ return false;
682
+ case 'BLOCKED':
683
+ console.log(chalk.red(`Signal: BLOCKED - ${signal.reason}`));
684
+ console.log(chalk.dim('Human intervention required.'));
685
+ return false;
686
+ case 'AWAITING_INPUT':
687
+ console.log(chalk.yellow('═'.repeat(60)));
688
+ console.log(chalk.yellow.bold(' CLAUDE NEEDS INPUT'));
689
+ console.log(chalk.yellow('═'.repeat(60)));
690
+ console.log('');
691
+ console.log(signal.content);
692
+ console.log('');
693
+ console.log(chalk.dim('Answer the questions above, then Claude will be respawned.'));
694
+ console.log(chalk.dim('Your answers will be included in the next prompt.'));
695
+ console.log('');
696
+ // Wait for user input
697
+ const readline = await import('node:readline');
698
+ const rl = readline.createInterface({
699
+ input: process.stdin,
700
+ output: process.stdout,
701
+ });
702
+ const answers = await new Promise((resolve) => {
703
+ console.log(chalk.cyan('Enter your answers (end with a blank line):'));
704
+ let input = '';
705
+ rl.on('line', (line) => {
706
+ if (line === '') {
707
+ rl.close();
708
+ resolve(input.trim());
709
+ }
710
+ else {
711
+ input += line + '\n';
712
+ }
713
+ });
714
+ });
715
+ // Store answers in state for next iteration
716
+ if (!state.context)
717
+ state.context = {};
718
+ state.context.user_answers = answers;
719
+ writeState(statusPath, state);
720
+ console.log(chalk.green('\nAnswers recorded. Respawning Claude...'));
721
+ return true; // Respawn Claude
722
+ }
723
+ return false;
724
+ }
725
+ /**
726
+ * Get artifact path for current phase.
727
+ */
728
+ function getArtifactForPhase(state) {
729
+ switch (state.phase) {
730
+ case 'specify':
731
+ return `codev/specs/${state.id}-${state.title}.md`;
732
+ case 'plan':
733
+ return `codev/plans/${state.id}-${state.title}.md`;
734
+ case 'review':
735
+ return `codev/reviews/${state.id}-${state.title}.md`;
736
+ default:
737
+ return null;
738
+ }
739
+ }
740
+ function sleep(ms) {
741
+ return new Promise((resolve) => setTimeout(resolve, ms));
742
+ }
743
+ //# sourceMappingURL=run.js.map