@cluesmith/codev 2.0.0-rc.57 → 2.0.0-rc.59

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 (104) hide show
  1. package/dashboard/dist/assets/index-CXloFYpB.css +32 -0
  2. package/dashboard/dist/assets/index-Ca2fjOJf.js +131 -0
  3. package/dashboard/dist/assets/index-Ca2fjOJf.js.map +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  6. package/dist/agent-farm/commands/attach.js +31 -8
  7. package/dist/agent-farm/commands/attach.js.map +1 -1
  8. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  9. package/dist/agent-farm/commands/spawn.js +38 -28
  10. package/dist/agent-farm/commands/spawn.js.map +1 -1
  11. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  12. package/dist/agent-farm/commands/start.js +3 -226
  13. package/dist/agent-farm/commands/start.js.map +1 -1
  14. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  15. package/dist/agent-farm/commands/status.js +5 -2
  16. package/dist/agent-farm/commands/status.js.map +1 -1
  17. package/dist/agent-farm/db/index.d.ts.map +1 -1
  18. package/dist/agent-farm/db/index.js +45 -0
  19. package/dist/agent-farm/db/index.js.map +1 -1
  20. package/dist/agent-farm/db/schema.d.ts +1 -1
  21. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  22. package/dist/agent-farm/db/schema.js +2 -2
  23. package/dist/agent-farm/hq-connector.d.ts +0 -4
  24. package/dist/agent-farm/hq-connector.d.ts.map +1 -1
  25. package/dist/agent-farm/hq-connector.js +0 -15
  26. package/dist/agent-farm/hq-connector.js.map +1 -1
  27. package/dist/agent-farm/servers/tower-server.js +82 -22
  28. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +2 -0
  31. package/dist/cli.js.map +1 -1
  32. package/dist/commands/consult/index.d.ts +1 -0
  33. package/dist/commands/consult/index.d.ts.map +1 -1
  34. package/dist/commands/consult/index.js +23 -4
  35. package/dist/commands/consult/index.js.map +1 -1
  36. package/dist/commands/porch/checks.d.ts +3 -2
  37. package/dist/commands/porch/checks.d.ts.map +1 -1
  38. package/dist/commands/porch/checks.js +8 -2
  39. package/dist/commands/porch/checks.js.map +1 -1
  40. package/dist/commands/porch/index.d.ts.map +1 -1
  41. package/dist/commands/porch/index.js +21 -23
  42. package/dist/commands/porch/index.js.map +1 -1
  43. package/dist/commands/porch/next.d.ts +22 -0
  44. package/dist/commands/porch/next.d.ts.map +1 -0
  45. package/dist/commands/porch/next.js +475 -0
  46. package/dist/commands/porch/next.js.map +1 -0
  47. package/dist/commands/porch/protocol.d.ts +3 -3
  48. package/dist/commands/porch/protocol.d.ts.map +1 -1
  49. package/dist/commands/porch/protocol.js +40 -6
  50. package/dist/commands/porch/protocol.js.map +1 -1
  51. package/dist/commands/porch/types.d.ts +36 -1
  52. package/dist/commands/porch/types.d.ts.map +1 -1
  53. package/dist/commands/porch/verdict.d.ts +31 -0
  54. package/dist/commands/porch/verdict.d.ts.map +1 -0
  55. package/dist/commands/porch/verdict.js +59 -0
  56. package/dist/commands/porch/verdict.js.map +1 -0
  57. package/package.json +5 -7
  58. package/skeleton/porch/prompts/defend.md +1 -1
  59. package/skeleton/porch/prompts/evaluate.md +2 -2
  60. package/skeleton/porch/prompts/implement.md +1 -1
  61. package/skeleton/porch/prompts/plan.md +1 -1
  62. package/skeleton/porch/prompts/review.md +4 -4
  63. package/skeleton/porch/prompts/specify.md +1 -1
  64. package/skeleton/porch/prompts/understand.md +2 -2
  65. package/skeleton/protocol-schema.json +3 -3
  66. package/skeleton/protocols/bugfix/builder-prompt.md +1 -1
  67. package/skeleton/protocols/experiment/protocol.md +3 -3
  68. package/skeleton/protocols/experiment/templates/notes.md +1 -1
  69. package/skeleton/protocols/maintain/protocol.md +1 -1
  70. package/skeleton/protocols/protocol-schema.json +1 -1
  71. package/skeleton/protocols/{spider → spir}/builder-prompt.md +1 -1
  72. package/skeleton/protocols/{spider → spir}/prompts/implement.md +1 -1
  73. package/skeleton/protocols/{spider → spir}/prompts/plan.md +2 -2
  74. package/skeleton/protocols/{spider → spir}/prompts/review.md +2 -2
  75. package/skeleton/protocols/{spider → spir}/prompts/specify.md +1 -1
  76. package/skeleton/protocols/{spider → spir}/protocol.json +2 -2
  77. package/skeleton/protocols/{spider → spir}/protocol.md +6 -8
  78. package/skeleton/protocols/{spider → spir}/templates/review.md +1 -1
  79. package/skeleton/protocols/tick/builder-prompt.md +1 -1
  80. package/skeleton/protocols/tick/protocol.md +18 -18
  81. package/skeleton/protocols/tick/templates/review.md +1 -1
  82. package/skeleton/resources/commands/overview.md +1 -1
  83. package/skeleton/resources/workflow-reference.md +2 -2
  84. package/skeleton/roles/architect.md +2 -2
  85. package/skeleton/roles/builder.md +2 -2
  86. package/skeleton/templates/AGENTS.md +1 -1
  87. package/skeleton/templates/CLAUDE.md +1 -1
  88. package/skeleton/templates/cheatsheet.md +3 -3
  89. package/skeleton/templates/projectlist.md +1 -1
  90. package/templates/dashboard/js/main.js +1 -1
  91. package/templates/open.html +26 -0
  92. package/dashboard/dist/assets/index-BV7KQvFU.css +0 -32
  93. package/dashboard/dist/assets/index-bhDjF0Oa.js +0 -131
  94. package/dashboard/dist/assets/index-bhDjF0Oa.js.map +0 -1
  95. package/dist/commands/porch/claude.d.ts +0 -27
  96. package/dist/commands/porch/claude.d.ts.map +0 -1
  97. package/dist/commands/porch/claude.js +0 -107
  98. package/dist/commands/porch/claude.js.map +0 -1
  99. package/dist/commands/porch/run.d.ts +0 -40
  100. package/dist/commands/porch/run.d.ts.map +0 -1
  101. package/dist/commands/porch/run.js +0 -893
  102. package/dist/commands/porch/run.js.map +0 -1
  103. /package/skeleton/protocols/{spider → spir}/templates/plan.md +0 -0
  104. /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
@@ -1,893 +0,0 @@
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 { buildWithTimeout } from './claude.js';
17
- import { buildPhasePrompt } from './prompts.js';
18
- import { globSync } from 'node:fs';
19
- /**
20
- * Check if an artifact file has YAML frontmatter indicating it was
21
- * already approved and validated (3-way review).
22
- *
23
- * Frontmatter format:
24
- * ---
25
- * approved: 2026-01-29
26
- * validated: [gemini, codex, claude]
27
- * ---
28
- */
29
- function isArtifactPreApproved(projectRoot, artifactGlob) {
30
- // Resolve glob pattern (e.g., "codev/specs/0085-*.md")
31
- const matches = globSync(artifactGlob, { cwd: projectRoot });
32
- if (matches.length === 0)
33
- return false;
34
- const filePath = path.join(projectRoot, matches[0]);
35
- try {
36
- const content = fs.readFileSync(filePath, 'utf-8');
37
- // Check for YAML frontmatter with approved and validated fields
38
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
39
- if (!frontmatterMatch)
40
- return false;
41
- const frontmatter = frontmatterMatch[1];
42
- const hasApproved = /^approved:\s*.+$/m.test(frontmatter);
43
- const hasValidated = /^validated:\s*\[.+\]$/m.test(frontmatter);
44
- return hasApproved && hasValidated;
45
- }
46
- catch {
47
- return false;
48
- }
49
- }
50
- // Runtime artifacts go in project directory, not a hidden folder
51
- function getPorchDir(projectRoot, state) {
52
- return path.join(projectRoot, 'codev', 'projects', `${state.id}-${state.title}`);
53
- }
54
- /**
55
- * Generate output file name with phase and iteration info.
56
- * e.g., "0074-specify-iter-1.txt" or "0074-phase_1-iter-2.txt"
57
- *
58
- * Uses state.iteration which is persisted and survives porch restarts.
59
- */
60
- function getOutputFileName(state) {
61
- const planPhase = getCurrentPlanPhase(state.plan_phases);
62
- // Build filename using persisted iteration from state
63
- const parts = [state.id];
64
- if (planPhase) {
65
- parts.push(planPhase.id);
66
- }
67
- else {
68
- parts.push(state.phase);
69
- }
70
- parts.push(`iter-${state.iteration}`);
71
- return `${parts.join('-')}.txt`;
72
- }
73
- /** Exit code when AWAITING_INPUT is detected in non-interactive mode */
74
- export const EXIT_AWAITING_INPUT = 3;
75
- // Build constants — all build-loop settings centralized here (spec §Configuration)
76
- const BUILD_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
77
- const BUILD_MAX_RETRIES = 3;
78
- const BUILD_RETRY_DELAYS = [5000, 15000, 30000]; // 5s, 15s, 30s
79
- const CIRCUIT_BREAKER_THRESHOLD = 5;
80
- /**
81
- * Main run loop for porch.
82
- * Spawns Claude for each phase and monitors until protocol complete.
83
- */
84
- export async function run(projectRoot, projectId, options = {}) {
85
- const statusPath = findStatusPath(projectRoot, projectId);
86
- if (!statusPath) {
87
- throw new Error(`Project ${projectId} not found.\nRun 'porch init' to create a new project.`);
88
- }
89
- // Read initial state to get project directory
90
- let state = readState(statusPath);
91
- const singleIteration = options.singleIteration || false;
92
- const singlePhase = options.singlePhase || false;
93
- // Ensure project artifacts directory exists
94
- const porchDir = getPorchDir(projectRoot, state);
95
- if (!fs.existsSync(porchDir)) {
96
- fs.mkdirSync(porchDir, { recursive: true });
97
- }
98
- console.log('');
99
- console.log(chalk.bold('PORCH - Protocol Orchestrator'));
100
- console.log(chalk.dim('Porch is the outer loop. Claude runs under porch control.'));
101
- console.log('');
102
- let consecutiveFailures = 0;
103
- while (true) {
104
- state = readState(statusPath);
105
- // AWAITING_INPUT resume guard — prevent infinite resume loops (spec §AWAITING_INPUT resume guard)
106
- if (state.awaiting_input) {
107
- // If we have a previous output hash, check if the file has changed.
108
- // If unchanged, the human hasn't resolved the blocker — halt.
109
- if (state.awaiting_input_output && state.awaiting_input_hash && fs.existsSync(state.awaiting_input_output)) {
110
- const crypto = await import('node:crypto');
111
- const currentHash = crypto.createHash('sha256').update(fs.readFileSync(state.awaiting_input_output)).digest('hex');
112
- if (currentHash === state.awaiting_input_hash) {
113
- console.error(chalk.red('[PORCH] AWAITING_INPUT output unchanged since last run. Resolve the blocker before restarting.'));
114
- console.error(chalk.dim(` Output file: ${state.awaiting_input_output}`));
115
- process.exit(EXIT_AWAITING_INPUT);
116
- }
117
- }
118
- console.log(chalk.yellow('[PORCH] Resuming from AWAITING_INPUT state'));
119
- state.awaiting_input = false;
120
- delete state.awaiting_input_output;
121
- delete state.awaiting_input_hash;
122
- writeState(statusPath, state);
123
- // Continue normally — will re-run the build phase
124
- }
125
- // Circuit breaker check
126
- if (consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
127
- console.error(chalk.red(`[PORCH] Circuit breaker: ${consecutiveFailures} consecutive build failures. Halting.`));
128
- process.exit(2);
129
- }
130
- const protocol = loadProtocol(projectRoot, state.protocol);
131
- const phaseConfig = getPhaseConfig(protocol, state.phase);
132
- if (!phaseConfig) {
133
- console.log(chalk.green.bold('🎉 PROTOCOL COMPLETE'));
134
- console.log(`\n Project ${state.id} has completed the ${state.protocol} protocol.`);
135
- break;
136
- }
137
- // Check for pending gate
138
- const gateName = getPhaseGate(protocol, state.phase);
139
- if (gateName && state.gates[gateName]?.status === 'pending' && state.gates[gateName]?.requested_at) {
140
- // --single-phase: return gate status to Builder, let it handle human interaction
141
- if (singlePhase) {
142
- console.log(chalk.yellow(`\n[--single-phase] Gate '${gateName}' pending. Needs human approval.`));
143
- outputSinglePhaseResult(state, 'gate_needed', gateName);
144
- return;
145
- }
146
- const outputPath = path.join(porchDir, `${state.id}-gate.txt`);
147
- await handleGate(state, gateName, statusPath, projectRoot, outputPath, protocol);
148
- continue;
149
- }
150
- // Gate approved → advance to next phase
151
- if (gateName && state.gates[gateName]?.status === 'approved') {
152
- const { done } = await import('./index.js');
153
- await done(projectRoot, state.id);
154
- // --single-phase: exit after phase advances
155
- if (singlePhase) {
156
- const newState = readState(statusPath);
157
- console.log(chalk.dim(`\n[--single-phase] Phase complete. Now at: ${newState.phase}`));
158
- outputSinglePhaseResult(newState, 'advanced', undefined, undefined);
159
- return;
160
- }
161
- continue;
162
- }
163
- // Handle build_verify phases
164
- if (isBuildVerify(protocol, state.phase)) {
165
- const maxIterations = getMaxIterations(protocol, state.phase);
166
- // Check if artifact already exists and was pre-approved + validated
167
- // (e.g., spec/plan created by architect before builder was spawned)
168
- if (!state.build_complete && state.iteration === 1) {
169
- const buildConfig = getBuildConfig(protocol, state.phase);
170
- if (buildConfig?.artifact) {
171
- const artifactGlob = buildConfig.artifact.replace('${PROJECT_ID}', state.id);
172
- if (isArtifactPreApproved(projectRoot, artifactGlob)) {
173
- console.log(chalk.green(`[${state.id}] ${phaseConfig.name}: artifact exists with approval metadata - skipping build+verify`));
174
- // Auto-approve gate and advance
175
- if (gateName) {
176
- state.gates[gateName] = { status: 'approved', approved_at: new Date().toISOString() };
177
- writeState(statusPath, state);
178
- }
179
- const { done } = await import('./index.js');
180
- await done(projectRoot, state.id);
181
- continue;
182
- }
183
- }
184
- }
185
- // Check if we need to run VERIFY (build just completed)
186
- if (state.build_complete) {
187
- // First check if the artifact was actually created
188
- const artifactPath = getArtifactForPhase(state);
189
- if (artifactPath) {
190
- const fullPath = path.join(projectRoot, artifactPath);
191
- if (!fs.existsSync(fullPath)) {
192
- console.log('');
193
- console.log(chalk.yellow(`Artifact not found: ${artifactPath}`));
194
- console.log(chalk.dim('Claude may have asked questions or encountered an error.'));
195
- console.log(chalk.dim('Check the output file for details, then respawn.'));
196
- state.build_complete = false;
197
- writeState(statusPath, state);
198
- continue;
199
- }
200
- }
201
- console.log('');
202
- console.log(chalk.cyan(`[${state.id}] VERIFY - Iteration ${state.iteration}/${maxIterations}`));
203
- const verifyStartMs = Date.now();
204
- const reviews = await runVerification(projectRoot, state, protocol);
205
- const verifyDurationMs = Date.now() - verifyStartMs;
206
- // Structured timing output for e2e test parsing
207
- const verdictMap = {};
208
- for (const r of reviews) {
209
- verdictMap[r.model] = r.verdict;
210
- }
211
- console.log(`__PORCH_TIMING__${JSON.stringify({
212
- event: 'verify',
213
- phase: state.phase,
214
- plan_phase: state.current_plan_phase || null,
215
- iteration: state.iteration,
216
- duration_ms: verifyDurationMs,
217
- verdicts: verdictMap,
218
- })}`);
219
- // Get the build output file from current iteration (stored when we track it)
220
- const currentBuildOutput = state.history.find(h => h.iteration === state.iteration)?.build_output || '';
221
- // Update history with reviews
222
- const existingRecord = state.history.find(h => h.iteration === state.iteration);
223
- if (existingRecord) {
224
- existingRecord.reviews = reviews;
225
- }
226
- else {
227
- state.history.push({
228
- iteration: state.iteration,
229
- build_output: currentBuildOutput,
230
- reviews,
231
- });
232
- }
233
- if (allApprove(reviews)) {
234
- console.log(chalk.green('\nAll reviewers APPROVE!'));
235
- // Run on_complete actions (commit + push)
236
- await runOnComplete(projectRoot, state, protocol, reviews);
237
- // Request gate or advance plan phase
238
- if (gateName) {
239
- state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() };
240
- }
241
- else if (isPhased(protocol, state.phase)) {
242
- // No gate on a per_plan_phase type — advance plan phase directly
243
- const { done } = await import('./index.js');
244
- await done(projectRoot, state.id);
245
- state = readState(statusPath); // Re-read after done() modifies state
246
- }
247
- // Reset for next iteration
248
- state.build_complete = false;
249
- state.iteration = 1;
250
- state.history = [];
251
- writeState(statusPath, state);
252
- // Single iteration mode: exit after completing a build-verify cycle
253
- if (singleIteration) {
254
- console.log(chalk.dim('\n[--single-iteration] Build-verify cycle complete. Exiting.'));
255
- return;
256
- }
257
- // --single-phase: exit after build-verify passes
258
- if (singlePhase) {
259
- if (gateName) {
260
- console.log(chalk.dim(`\n[--single-phase] Build-verify passed. Gate '${gateName}' requested.`));
261
- outputSinglePhaseResult(state, 'gate_needed', gateName, reviews);
262
- }
263
- else {
264
- console.log(chalk.dim(`\n[--single-phase] Build-verify passed. No gate needed.`));
265
- outputSinglePhaseResult(state, 'verified', undefined, reviews);
266
- }
267
- return;
268
- }
269
- continue;
270
- }
271
- // Some reviewers requested changes
272
- console.log(chalk.yellow('\nChanges requested. Feeding back to Claude...'));
273
- // --single-phase: return control to Builder with iterating status
274
- if (singlePhase) {
275
- console.log(chalk.dim(`\n[--single-phase] Changes requested. Returning control to Builder.`));
276
- outputSinglePhaseResult(state, 'iterating', undefined, reviews);
277
- return;
278
- }
279
- if (state.iteration >= maxIterations) {
280
- // Max iterations reached without unanimity - summarize and interrupt user
281
- console.log('');
282
- console.log(chalk.red('═'.repeat(60)));
283
- console.log(chalk.red.bold(' MAX ITERATIONS REACHED - NO UNANIMITY'));
284
- console.log(chalk.red('═'.repeat(60)));
285
- console.log('');
286
- console.log(chalk.yellow(`After ${maxIterations} iterations, reviewers did not reach unanimity.`));
287
- console.log('');
288
- console.log(chalk.bold('Summary of reviewer positions:'));
289
- // Group reviews by verdict
290
- const byVerdict = {};
291
- for (const r of reviews) {
292
- if (!byVerdict[r.verdict])
293
- byVerdict[r.verdict] = [];
294
- byVerdict[r.verdict].push(r.model);
295
- }
296
- for (const [verdict, models] of Object.entries(byVerdict)) {
297
- const color = verdict === 'APPROVE' ? chalk.green :
298
- verdict === 'CONSULT_ERROR' ? chalk.red :
299
- verdict === 'REQUEST_CHANGES' ? chalk.yellow : chalk.blue;
300
- console.log(` ${color(verdict)}: ${models.join(', ')}`);
301
- }
302
- console.log('');
303
- console.log(chalk.dim('Review files:'));
304
- for (const r of reviews) {
305
- console.log(` ${r.model}: ${r.file}`);
306
- }
307
- console.log('');
308
- // Check for identical REQUEST_CHANGES (may indicate missing context)
309
- const requestChangesReviews = reviews.filter(r => r.verdict === 'REQUEST_CHANGES');
310
- if (requestChangesReviews.length >= 2) {
311
- console.log(chalk.yellow('Note: Multiple REQUEST_CHANGES may indicate missing file context.'));
312
- console.log(chalk.dim('Check if the artifact path is correct and files are committed.'));
313
- console.log('');
314
- }
315
- // Auto-continue when PORCH_AUTO_APPROVE is set (e2e/non-interactive mode)
316
- let action;
317
- if (process.env.PORCH_AUTO_APPROVE === 'true') {
318
- console.log(chalk.yellow('[E2E] Auto-continuing past max iterations'));
319
- action = 'c';
320
- }
321
- else {
322
- // Wait for user decision
323
- const readline = await import('node:readline');
324
- const rl = readline.createInterface({
325
- input: process.stdin,
326
- output: process.stdout,
327
- });
328
- console.log('Options:');
329
- console.log(" 'c' or 'continue' - Proceed to gate anyway (let human decide)");
330
- console.log(" 'r' or 'retry' - Reset iteration counter and try again");
331
- console.log(" 'q' or 'quit' - Exit porch");
332
- console.log('');
333
- action = await new Promise((resolve) => {
334
- rl.question(chalk.cyan(`[${state.id}] > `), (input) => {
335
- rl.close();
336
- resolve(input.trim().toLowerCase());
337
- });
338
- });
339
- }
340
- switch (action) {
341
- case 'c':
342
- case 'continue':
343
- console.log(chalk.dim('\nProceeding to gate...'));
344
- break;
345
- case 'r':
346
- case 'retry':
347
- console.log(chalk.dim('\nResetting iteration counter...'));
348
- state.iteration = 1;
349
- state.build_complete = false;
350
- state.history = [];
351
- writeState(statusPath, state);
352
- continue;
353
- case 'q':
354
- case 'quit':
355
- console.log(chalk.yellow('\nExiting porch.'));
356
- return;
357
- default:
358
- console.log(chalk.yellow('\nUnknown option. Proceeding to gate.'));
359
- }
360
- // Run on_complete actions
361
- await runOnComplete(projectRoot, state, protocol, reviews);
362
- // Request gate
363
- if (gateName) {
364
- state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() };
365
- }
366
- state.build_complete = false;
367
- state.iteration = 1;
368
- state.history = [];
369
- writeState(statusPath, state);
370
- // Single iteration mode: exit after max iterations
371
- if (singleIteration) {
372
- console.log(chalk.dim('\n[--single-iteration] Max iterations reached. Exiting.'));
373
- return;
374
- }
375
- continue;
376
- }
377
- // Increment iteration and continue to BUILD
378
- state.iteration++;
379
- state.build_complete = false;
380
- writeState(statusPath, state);
381
- // Single iteration mode: exit after storing feedback
382
- if (singleIteration) {
383
- console.log(chalk.dim('\n[--single-iteration] Feedback stored for next iteration. Exiting.'));
384
- console.log(chalk.dim(` Next run will be iteration ${state.iteration} with reviewer feedback.`));
385
- return;
386
- }
387
- // Fall through to BUILD phase
388
- }
389
- // BUILD phase
390
- console.log('');
391
- console.log(chalk.cyan(`[${state.id}] BUILD - ${phaseConfig.name} - Iteration ${state.iteration}/${maxIterations}`));
392
- }
393
- // Generate output file for this iteration
394
- const outputFileName = getOutputFileName(state);
395
- const outputPath = path.join(porchDir, outputFileName);
396
- // Track this build output in history (for feedback to next iteration)
397
- if (isBuildVerify(protocol, state.phase)) {
398
- const existingRecord = state.history.find(h => h.iteration === state.iteration);
399
- if (existingRecord) {
400
- existingRecord.build_output = outputPath;
401
- }
402
- else {
403
- state.history.push({
404
- iteration: state.iteration,
405
- build_output: outputPath,
406
- reviews: [],
407
- });
408
- }
409
- writeState(statusPath, state);
410
- }
411
- // Build prompt for current phase (includes history file paths if iteration > 1)
412
- const prompt = buildPhasePrompt(projectRoot, state, protocol);
413
- console.log(chalk.dim(`Output: ${outputFileName}`));
414
- // Show status
415
- showStatus(state, protocol);
416
- // Print the prompt being sent to the Worker
417
- console.log('');
418
- console.log(chalk.cyan('═'.repeat(60)));
419
- console.log(chalk.cyan.bold(' PROMPT TO WORKER (Agent SDK)'));
420
- console.log(chalk.cyan('═'.repeat(60)));
421
- console.log(chalk.dim(prompt.substring(0, 2000)));
422
- if (prompt.length > 2000) {
423
- console.log(chalk.dim(`... (${prompt.length - 2000} more chars)`));
424
- }
425
- console.log(chalk.cyan('═'.repeat(60)));
426
- console.log('');
427
- // Run the Worker via Agent SDK with retry
428
- console.log(chalk.dim('Starting Worker (Agent SDK)...'));
429
- const buildStartMs = Date.now();
430
- let actualOutputPath = outputPath;
431
- let result = await buildWithTimeout(prompt, outputPath, projectRoot, BUILD_TIMEOUT_MS);
432
- // Retry on failure (timeout or SDK error)
433
- if (!result.success && isBuildVerify(protocol, state.phase)) {
434
- for (let attempt = 1; attempt <= BUILD_MAX_RETRIES && !result.success; attempt++) {
435
- const delay = BUILD_RETRY_DELAYS[attempt - 1] || BUILD_RETRY_DELAYS[BUILD_RETRY_DELAYS.length - 1];
436
- console.log(chalk.yellow(`\nBuild failed. Retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${BUILD_MAX_RETRIES + 1})`));
437
- await sleep(delay);
438
- // Each retry attempt gets a distinct output file
439
- actualOutputPath = outputPath.replace(/\.txt$/, `-try-${attempt + 1}.txt`);
440
- result = await buildWithTimeout(prompt, actualOutputPath, projectRoot, BUILD_TIMEOUT_MS);
441
- }
442
- }
443
- const buildDurationMs = Date.now() - buildStartMs;
444
- if (result.cost) {
445
- console.log(chalk.dim(` Cost: $${result.cost.toFixed(4)}`));
446
- }
447
- if (result.duration) {
448
- console.log(chalk.dim(` Duration: ${(result.duration / 1000).toFixed(1)}s`));
449
- }
450
- // Structured timing output for e2e test parsing
451
- console.log(`__PORCH_TIMING__${JSON.stringify({
452
- event: 'build',
453
- phase: state.phase,
454
- plan_phase: state.current_plan_phase || null,
455
- iteration: state.iteration,
456
- duration_ms: buildDurationMs,
457
- cost: result.cost || null,
458
- success: result.success,
459
- })}`);
460
- // AWAITING_INPUT detection
461
- if (result.output && (/^<signal>BLOCKED:/im.test(result.output) || /^<signal>AWAITING_INPUT<\/signal>/im.test(result.output))) {
462
- console.error(chalk.yellow(`[PORCH] Worker needs human input — check output file: ${actualOutputPath}`));
463
- state.awaiting_input = true;
464
- state.awaiting_input_output = actualOutputPath;
465
- // Store hash of output for resume guard comparison
466
- if (fs.existsSync(actualOutputPath)) {
467
- const crypto = await import('node:crypto');
468
- state.awaiting_input_hash = crypto.createHash('sha256').update(fs.readFileSync(actualOutputPath)).digest('hex');
469
- }
470
- writeState(statusPath, state);
471
- process.exit(EXIT_AWAITING_INPUT);
472
- }
473
- // For build_verify phases, only proceed to verify on success
474
- if (isBuildVerify(protocol, state.phase)) {
475
- if (result.success) {
476
- console.log(chalk.dim('\nWorker finished. Moving to verification...'));
477
- // Update history to point at the actual successful attempt's output file
478
- const historyRecord = state.history.find(h => h.iteration === state.iteration);
479
- if (historyRecord) {
480
- historyRecord.build_output = actualOutputPath;
481
- }
482
- state.build_complete = true;
483
- consecutiveFailures = 0;
484
- writeState(statusPath, state);
485
- }
486
- else {
487
- // All retries exhausted — increment circuit breaker, do NOT set build_complete
488
- console.log(chalk.red('\nWorker failed after all retries.'));
489
- console.log(chalk.dim(`Check output: ${actualOutputPath}`));
490
- consecutiveFailures++;
491
- // In single-phase or single-iteration mode, return control to the caller
492
- if (singlePhase) {
493
- outputSinglePhaseResult(state, 'failed');
494
- return;
495
- }
496
- if (singleIteration) {
497
- console.log(chalk.dim('\n[--single-iteration] Build failed. Exiting.'));
498
- return;
499
- }
500
- // Loop back to top where circuit breaker check will halt if threshold reached
501
- continue;
502
- }
503
- // Continue loop - will hit build_complete check and run verify
504
- }
505
- else if (!result.success) {
506
- console.log(chalk.red('\nWorker failed.'));
507
- console.log(chalk.dim(`Check output: ${actualOutputPath}`));
508
- if (singlePhase) {
509
- outputSinglePhaseResult(state, 'failed');
510
- }
511
- return;
512
- }
513
- }
514
- }
515
- // ============================================================================
516
- // Verification (3-way consultation)
517
- // ============================================================================
518
- /**
519
- * Run 3-way verification on the current phase artifact.
520
- * Writes each consultation output to a file.
521
- * Returns array of review results with file paths.
522
- */
523
- async function runVerification(projectRoot, state, protocol) {
524
- const verifyConfig = getVerifyConfig(protocol, state.phase);
525
- if (!verifyConfig) {
526
- return []; // No verification configured
527
- }
528
- console.log(chalk.dim(`Running ${verifyConfig.models.length}-way consultation...`));
529
- const porchDir = getPorchDir(projectRoot, state);
530
- const reviews = [];
531
- // Run consultations in parallel
532
- const promises = verifyConfig.models.map(async (model) => {
533
- console.log(chalk.dim(` ${model}: starting...`));
534
- // Output file for this review
535
- const reviewFile = path.join(porchDir, `${state.id}-${state.phase}-iter${state.iteration}-${model}.txt`);
536
- const result = await runConsult(projectRoot, model, verifyConfig.type, state, reviewFile);
537
- reviews.push(result);
538
- const verdictColor = result.verdict === 'APPROVE' ? chalk.green :
539
- result.verdict === 'COMMENT' ? chalk.blue : chalk.yellow;
540
- console.log(` ${model}: ${verdictColor(result.verdict)}`);
541
- });
542
- await Promise.all(promises);
543
- return reviews;
544
- }
545
- /**
546
- * Get the consult artifact type for a phase.
547
- */
548
- function getConsultArtifactType(phaseId) {
549
- switch (phaseId) {
550
- case 'specify':
551
- return 'spec';
552
- case 'plan':
553
- return 'plan';
554
- case 'implement':
555
- return 'impl'; // Implementation reviews the code diff
556
- case 'review':
557
- return 'spec'; // Review phase reviews overall work
558
- default:
559
- return 'spec';
560
- }
561
- }
562
- /**
563
- * Run a single consultation with retry on failure.
564
- * Writes output to file and returns result with file path.
565
- *
566
- * Retry logic:
567
- * - Non-zero exit code = consultation failed (API key missing, network error, etc.)
568
- * - Retry up to 3 times with exponential backoff
569
- * - If all retries fail, return CONSULT_ERROR (not REQUEST_CHANGES)
570
- */
571
- const CONSULT_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
572
- const CONSULT_MAX_RETRIES = 3;
573
- const CONSULT_RETRY_DELAYS = [5000, 15000, 30000]; // 5s, 15s, 30s
574
- async function runConsult(projectRoot, model, reviewType, state, outputFile) {
575
- for (let attempt = 0; attempt < CONSULT_MAX_RETRIES; attempt++) {
576
- const result = await runConsultOnce(projectRoot, model, reviewType, state, outputFile);
577
- // Success - got a valid verdict
578
- if (result.verdict !== 'CONSULT_ERROR') {
579
- return result;
580
- }
581
- // Consultation failed - retry if attempts remaining
582
- if (attempt < CONSULT_MAX_RETRIES - 1) {
583
- const delay = CONSULT_RETRY_DELAYS[attempt];
584
- console.log(chalk.yellow(` ${model}: failed, retrying in ${delay / 1000}s... (attempt ${attempt + 2}/${CONSULT_MAX_RETRIES})`));
585
- await sleep(delay);
586
- }
587
- }
588
- // All retries failed
589
- console.log(chalk.red(` ${model}: FAILED after ${CONSULT_MAX_RETRIES} attempts`));
590
- return { model, verdict: 'CONSULT_ERROR', file: outputFile };
591
- }
592
- async function runConsultOnce(projectRoot, model, reviewType, state, outputFile) {
593
- const { spawn } = await import('node:child_process');
594
- const artifactType = getConsultArtifactType(state.phase);
595
- return new Promise((resolve) => {
596
- // Load .env from projectRoot so API keys (e.g. GEMINI_API_KEY) propagate
597
- // to consultation subprocesses regardless of CWD
598
- const env = { ...process.env };
599
- const envFile = path.join(projectRoot, '.env');
600
- if (fs.existsSync(envFile)) {
601
- for (const line of fs.readFileSync(envFile, 'utf-8').split('\n')) {
602
- const trimmed = line.trim();
603
- if (trimmed && !trimmed.startsWith('#')) {
604
- const eq = trimmed.indexOf('=');
605
- if (eq > 0) {
606
- env[trimmed.substring(0, eq)] = trimmed.substring(eq + 1);
607
- }
608
- }
609
- }
610
- }
611
- const args = ['--model', model, '--type', reviewType, artifactType, state.id];
612
- const proc = spawn('consult', args, {
613
- cwd: projectRoot,
614
- env,
615
- stdio: ['pipe', 'pipe', 'pipe'],
616
- });
617
- let output = '';
618
- let resolved = false;
619
- let exitCode = null;
620
- // Timeout after 1 hour
621
- const timeout = setTimeout(() => {
622
- if (!resolved) {
623
- resolved = true;
624
- proc.kill('SIGTERM');
625
- const timeoutOutput = output + '\n\n[TIMEOUT: Consultation exceeded 1 hour limit]';
626
- fs.writeFileSync(outputFile, timeoutOutput);
627
- console.log(chalk.yellow(` ${model}: timeout (1 hour limit)`));
628
- resolve({ model, verdict: 'CONSULT_ERROR', file: outputFile });
629
- }
630
- }, CONSULT_TIMEOUT_MS);
631
- proc.stdout.on('data', (data) => { output += data.toString(); });
632
- proc.stderr.on('data', (data) => { output += data.toString(); });
633
- proc.on('close', (code) => {
634
- if (!resolved) {
635
- resolved = true;
636
- clearTimeout(timeout);
637
- exitCode = code;
638
- // Write output to file
639
- fs.writeFileSync(outputFile, output);
640
- // Non-zero exit code = consultation failed (API key missing, etc.)
641
- if (code !== 0) {
642
- console.log(chalk.yellow(` ${model}: exit code ${code}`));
643
- resolve({ model, verdict: 'CONSULT_ERROR', file: outputFile });
644
- return;
645
- }
646
- // Parse verdict from output
647
- const verdict = parseVerdict(output);
648
- resolve({ model, verdict, file: outputFile });
649
- }
650
- });
651
- proc.on('error', (err) => {
652
- if (!resolved) {
653
- resolved = true;
654
- clearTimeout(timeout);
655
- const errorOutput = `Error: ${err.message}`;
656
- fs.writeFileSync(outputFile, errorOutput);
657
- console.log(chalk.red(` ${model}: error - ${err.message}`));
658
- resolve({ model, verdict: 'CONSULT_ERROR', file: outputFile });
659
- }
660
- });
661
- });
662
- }
663
- /**
664
- * Parse verdict from consultation output.
665
- *
666
- * Looks for the verdict line in format:
667
- * VERDICT: APPROVE
668
- * VERDICT: REQUEST_CHANGES
669
- * VERDICT: COMMENT
670
- *
671
- * Also handles markdown formatting like:
672
- * **VERDICT: APPROVE**
673
- * *VERDICT: APPROVE*
674
- *
675
- * Safety: If no explicit verdict found (empty output, crash, malformed),
676
- * defaults to REQUEST_CHANGES to prevent proceeding with unverified code.
677
- */
678
- export function parseVerdict(output) {
679
- // Empty or very short output = something went wrong
680
- if (!output || output.trim().length < 50) {
681
- return 'REQUEST_CHANGES';
682
- }
683
- // Scan lines LAST→FIRST so the actual verdict (at the end) takes priority
684
- // over template text echoed by codex CLI at the start of output.
685
- // Skip template lines containing "[" (e.g., "VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT]")
686
- const lines = output.split('\n');
687
- for (let i = lines.length - 1; i >= 0; i--) {
688
- // Strip markdown formatting (**, *, __, _, `) and trim
689
- const stripped = lines[i].trim().replace(/^[\*_`-]+|[\*_`-]+$/g, '').trim().toUpperCase();
690
- // Match "VERDICT: <value>" but NOT template "VERDICT: [APPROVE | ...]"
691
- if (stripped.startsWith('VERDICT:') && !stripped.includes('[')) {
692
- const value = stripped.substring('VERDICT:'.length).trim();
693
- if (value.startsWith('REQUEST_CHANGES'))
694
- return 'REQUEST_CHANGES';
695
- if (value.startsWith('APPROVE'))
696
- return 'APPROVE';
697
- if (value.startsWith('COMMENT'))
698
- return 'COMMENT';
699
- }
700
- }
701
- // No valid VERDICT: line found — default to REQUEST_CHANGES for safety
702
- return 'REQUEST_CHANGES';
703
- }
704
- /**
705
- * Check if all reviewers approved (unanimity required).
706
- *
707
- * Returns true only if ALL reviewers explicitly APPROVE.
708
- * COMMENT counts as approve (non-blocking feedback).
709
- * CONSULT_ERROR and REQUEST_CHANGES block approval.
710
- */
711
- function allApprove(reviews) {
712
- if (reviews.length === 0)
713
- return true; // No verification = auto-approve
714
- // Unanimity: ALL must be APPROVE or COMMENT
715
- return reviews.every(r => r.verdict === 'APPROVE' || r.verdict === 'COMMENT');
716
- }
717
- /**
718
- * Run on_complete actions (commit + push).
719
- */
720
- async function runOnComplete(projectRoot, state, protocol, reviews) {
721
- const onComplete = getOnCompleteConfig(protocol, state.phase);
722
- if (!onComplete)
723
- return;
724
- const buildConfig = getBuildConfig(protocol, state.phase);
725
- if (!buildConfig)
726
- return;
727
- // Resolve artifact path
728
- const artifact = buildConfig.artifact
729
- .replace('${PROJECT_ID}', state.id)
730
- .replace('${PROJECT_TITLE}', state.title);
731
- const { exec } = await import('node:child_process');
732
- const { promisify } = await import('node:util');
733
- const execAsync = promisify(exec);
734
- if (onComplete.commit) {
735
- console.log(chalk.dim('Committing...'));
736
- try {
737
- // Stage artifact
738
- await execAsync(`git add ${artifact}`, { cwd: projectRoot });
739
- // Commit
740
- const message = `[Spec ${state.id}] ${state.phase}: ${state.title}
741
-
742
- Iteration ${state.iteration}
743
- 3-way review: ${formatVerdicts(reviews)}`;
744
- await execAsync(`git commit -m "${message}"`, { cwd: projectRoot });
745
- console.log(chalk.green('Committed.'));
746
- }
747
- catch (err) {
748
- console.log(chalk.yellow('Commit failed (may be nothing to commit).'));
749
- }
750
- }
751
- if (onComplete.push) {
752
- console.log(chalk.dim('Pushing...'));
753
- try {
754
- await execAsync('git push', { cwd: projectRoot });
755
- console.log(chalk.green('Pushed.'));
756
- }
757
- catch (err) {
758
- console.log(chalk.yellow('Push failed.'));
759
- }
760
- }
761
- }
762
- /**
763
- * Format verdicts for commit message.
764
- */
765
- function formatVerdicts(reviews) {
766
- return reviews
767
- .map(r => `${r.model}=${r.verdict}`)
768
- .join(', ') || 'N/A';
769
- }
770
- /**
771
- * Display current status.
772
- */
773
- function showStatus(state, protocol) {
774
- const phaseConfig = getPhaseConfig(protocol, state.phase);
775
- console.log('');
776
- console.log(chalk.bold(`[${state.id}] ${state.title}`));
777
- console.log(` Phase: ${state.phase} (${phaseConfig?.name || 'unknown'})`);
778
- if (isBuildVerify(protocol, state.phase)) {
779
- const maxIterations = getMaxIterations(protocol, state.phase);
780
- console.log(` Iteration: ${state.iteration}/${maxIterations}`);
781
- }
782
- if (isPhased(protocol, state.phase) && state.plan_phases.length > 0) {
783
- const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
784
- if (currentPlanPhase) {
785
- console.log(` Plan Phase: ${currentPlanPhase.id} - ${currentPlanPhase.title}`);
786
- }
787
- }
788
- console.log('');
789
- }
790
- /**
791
- * Handle gate approval flow.
792
- */
793
- async function handleGate(state, gateName, statusPath, projectRoot, outputPath, protocol) {
794
- // E2E testing: Auto-approve gates when PORCH_AUTO_APPROVE is set
795
- if (process.env.PORCH_AUTO_APPROVE === 'true') {
796
- console.log(chalk.yellow(`[E2E] Auto-approving gate: ${gateName}`));
797
- state.gates[gateName].status = 'approved';
798
- state.gates[gateName].approved_at = new Date().toISOString();
799
- writeState(statusPath, state);
800
- return;
801
- }
802
- console.log('');
803
- console.log(chalk.yellow('═'.repeat(60)));
804
- console.log(chalk.yellow.bold(` GATE: ${gateName}`));
805
- console.log(chalk.yellow('═'.repeat(60)));
806
- console.log('');
807
- // Show artifact path
808
- const artifact = getArtifactForPhase(state);
809
- if (artifact) {
810
- console.log(` Review: ${artifact}`);
811
- }
812
- console.log('');
813
- console.log(" Type 'a' or 'approve' to approve and continue.");
814
- console.log(" Type 'q' or 'quit' to exit.");
815
- console.log('');
816
- // Wait for user input
817
- const readline = await import('node:readline');
818
- const rl = readline.createInterface({
819
- input: process.stdin,
820
- output: process.stdout,
821
- });
822
- return new Promise((resolve) => {
823
- const prompt = () => {
824
- rl.question(chalk.cyan(`[${state.id}] WAITING FOR APPROVAL > `), (input) => {
825
- const cmd = input.trim().toLowerCase();
826
- switch (cmd) {
827
- case 'a':
828
- case 'approve':
829
- state.gates[gateName].status = 'approved';
830
- state.gates[gateName].approved_at = new Date().toISOString();
831
- writeState(statusPath, state);
832
- console.log(chalk.green(`\nGate ${gateName} approved.`));
833
- rl.close();
834
- resolve();
835
- break;
836
- case 'q':
837
- case 'quit':
838
- console.log(chalk.yellow('\nExiting without approval.'));
839
- rl.close();
840
- process.exit(0);
841
- break;
842
- default:
843
- console.log(chalk.dim("Unknown command. Type 'a' to approve or 'q' to quit."));
844
- prompt();
845
- }
846
- });
847
- };
848
- prompt();
849
- });
850
- }
851
- /**
852
- * Get artifact path for current phase.
853
- */
854
- function getArtifactForPhase(state) {
855
- switch (state.phase) {
856
- case 'specify':
857
- return `codev/specs/${state.id}-${state.title}.md`;
858
- case 'plan':
859
- return `codev/plans/${state.id}-${state.title}.md`;
860
- case 'review':
861
- return `codev/reviews/${state.id}-${state.title}.md`;
862
- default:
863
- return null;
864
- }
865
- }
866
- /**
867
- * Output structured result for --single-phase mode.
868
- * The Builder (outer Claude) parses this to understand what happened.
869
- */
870
- function outputSinglePhaseResult(state, status, gateName, reviews) {
871
- const result = {
872
- phase: state.phase,
873
- plan_phase: state.current_plan_phase,
874
- iteration: state.iteration,
875
- status,
876
- gate: gateName || null,
877
- };
878
- // Include verdicts if reviews were run
879
- if (reviews && reviews.length > 0) {
880
- result.verdicts = Object.fromEntries(reviews.map(r => [r.model, r.verdict]));
881
- }
882
- // Include artifact path
883
- const artifact = getArtifactForPhase(state);
884
- if (artifact) {
885
- result.artifact = artifact;
886
- }
887
- // Output as JSON on a single line for easy parsing
888
- console.log(`\n__PORCH_RESULT__${JSON.stringify(result)}`);
889
- }
890
- function sleep(ms) {
891
- return new Promise((resolve) => setTimeout(resolve, ms));
892
- }
893
- //# sourceMappingURL=run.js.map