Skip to content

Commit 9536a25

Browse files
committed
Fix porch/consult review file mismatch, builder terminal persistence, and PR merge instruction
- Add --output flag to consult CLI so porch can direct review output to expected file paths - Update porch next.ts to emit consult commands with --output pointing to review file locations - Stop deleting SQLite terminal rows on failed tmux reconnection (retry on next poll instead) - Add PR merge task to protocol completion response so builders merge after pr-ready approval
1 parent 38af34f commit 9536a25

4 files changed

Lines changed: 54 additions & 9 deletions

File tree

packages/codev/src/agent-farm/servers/tower-server.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -943,8 +943,7 @@ async function getTerminalsForProject(
943943
session = manager.getSession(newSession.id);
944944
log('INFO', `Reconnected to tmux "${dbSession.tmux_session}" on-the-fly → ${newSession.id}`);
945945
} catch (err) {
946-
log('WARN', `Failed to reconnect to tmux "${dbSession.tmux_session}": ${(err as Error).message}`);
947-
deleteTerminalSession(dbSession.id);
946+
log('WARN', `Failed to reconnect to tmux "${dbSession.tmux_session}": ${(err as Error).message} — will retry on next poll`);
948947
continue;
949948
}
950949
} else if (!session) {

packages/codev/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ program
9090
.option('-n, --dry-run', 'Show what would execute without running')
9191
.option('-t, --type <type>', 'Review type: spec-review, plan-review, impl-review, pr-ready, integration-review')
9292
.option('-r, --role <role>', 'Custom role from codev/roles/<name>.md (e.g., gtm-specialist, security-reviewer)')
93+
.option('--output <path>', 'Write consultation output to file (used by porch for review file collection)')
9394
.allowUnknownOption(true)
9495
.action(async (subcommand, args, options) => {
9596
try {
@@ -100,6 +101,7 @@ program
100101
dryRun: options.dryRun,
101102
reviewType: options.type,
102103
role: options.role,
104+
output: options.output,
103105
});
104106
} catch (error) {
105107
console.error(error instanceof Error ? error.message : String(error));

packages/codev/src/commands/consult/index.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface ConsultOptions {
4040
dryRun?: boolean;
4141
reviewType?: string;
4242
role?: string;
43+
output?: string;
4344
}
4445

4546
// Valid review types
@@ -263,7 +264,8 @@ async function runConsultation(
263264
projectRoot: string,
264265
dryRun: boolean,
265266
reviewType?: string,
266-
customRole?: string
267+
customRole?: string,
268+
outputPath?: string,
267269
): Promise<void> {
268270
// Use custom role if specified, otherwise use default consultant role
269271
let role = customRole ? loadCustomRole(projectRoot, customRole) : loadRole(projectRoot);
@@ -343,15 +345,26 @@ async function runConsultation(
343345

344346
// Execute with passthrough stdio
345347
// Use 'ignore' for stdin to prevent blocking when spawned as subprocess
348+
// When outputPath is set, capture stdout to write to file (used by porch)
346349
const fullEnv = { ...process.env, ...env };
347350
const startTime = Date.now();
351+
const stdoutMode = outputPath ? 'pipe' : 'inherit';
348352

349353
return new Promise((resolve, reject) => {
350354
const proc = spawn(cmd[0], cmd.slice(1), {
351355
env: fullEnv,
352-
stdio: ['ignore', 'inherit', 'inherit'],
356+
stdio: ['ignore', stdoutMode, 'inherit'],
353357
});
354358

359+
const chunks: Buffer[] = [];
360+
if (outputPath && proc.stdout) {
361+
proc.stdout.on('data', (chunk: Buffer) => {
362+
chunks.push(chunk);
363+
// Also write to stdout so the user can still see output
364+
process.stdout.write(chunk);
365+
});
366+
}
367+
355368
proc.on('close', (code) => {
356369
const duration = (Date.now() - startTime) / 1000;
357370
logQuery(projectRoot, model, query, duration);
@@ -360,6 +373,16 @@ async function runConsultation(
360373
fs.unlinkSync(tempFile);
361374
}
362375

376+
// Write captured output to file
377+
if (outputPath && chunks.length > 0) {
378+
const outputDir = path.dirname(outputPath);
379+
if (!fs.existsSync(outputDir)) {
380+
fs.mkdirSync(outputDir, { recursive: true });
381+
}
382+
fs.writeFileSync(outputPath, Buffer.concat(chunks).toString('utf-8'));
383+
console.error(`\nOutput written to: ${outputPath}`);
384+
}
385+
363386
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
364387

365388
if (code !== 0) {
@@ -571,7 +594,7 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
571594
* Main consult entry point
572595
*/
573596
export async function consult(options: ConsultOptions): Promise<void> {
574-
const { model: modelInput, subcommand, args, dryRun = false, reviewType, role: customRole } = options;
597+
const { model: modelInput, subcommand, args, dryRun = false, reviewType, role: customRole, output: outputPath } = options;
575598

576599
// Resolve model alias
577600
const model = MODEL_ALIASES[modelInput.toLowerCase()] || modelInput.toLowerCase();
@@ -692,5 +715,5 @@ export async function consult(options: ConsultOptions): Promise<void> {
692715
console.error('='.repeat(60));
693716
console.error('');
694717

695-
await runConsultation(model, query, projectRoot, dryRun, reviewType, customRole);
718+
await runConsultation(model, query, projectRoot, dryRun, reviewType, customRole, outputPath);
696719
}

packages/codev/src/commands/porch/next.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ function findReviewFiles(
103103
return results;
104104
}
105105

106+
/**
107+
* Compute the expected review file path for a given model.
108+
* This must match the pattern used by findReviewFiles().
109+
*/
110+
function getReviewFilePath(
111+
projectRoot: string,
112+
state: ProjectState,
113+
model: string
114+
): string {
115+
const projectDir = getProjectDir(projectRoot, state.id, state.title);
116+
const phase = state.current_plan_phase || state.phase;
117+
const fileName = `${state.id}-${phase}-iter${state.iteration}-${model}.txt`;
118+
return path.join(projectDir, fileName);
119+
}
120+
106121
/**
107122
* Compute the next tasks for a project.
108123
*
@@ -134,6 +149,12 @@ export async function next(projectRoot: string, projectId: string): Promise<Porc
134149
phase: state.phase,
135150
iteration: state.iteration,
136151
summary: `Project ${state.id} has completed the ${state.protocol} protocol.`,
152+
tasks: [{
153+
subject: 'Merge the pull request',
154+
activeForm: 'Merging pull request',
155+
description: `The protocol is complete. Merge the PR using:\n\ngh pr merge --merge\n\nDo NOT squash merge. Use regular merge commits to preserve development history.\n\nAfter merging, notify the architect that the work is complete.`,
156+
sequential: true,
157+
}],
137158
};
138159
}
139160

@@ -311,10 +332,10 @@ async function handleBuildVerify(
311332
if (reviews.length === 0) {
312333
const tasks: PorchTask[] = [];
313334

314-
// Build consultation commands
335+
// Build consultation commands with --output so review files land where porch expects them
315336
const consultType = getConsultArtifactType(state.phase);
316337
const consultCmds = verifyConfig.models.map(
317-
m => `consult --model ${m} --type ${verifyConfig.type} ${consultType} ${state.id}`
338+
m => `consult --model ${m} --type ${verifyConfig.type} --output "${getReviewFilePath(projectRoot, state, m)}" ${consultType} ${state.id}`
318339
);
319340

320341
tasks.push({
@@ -334,7 +355,7 @@ async function handleBuildVerify(
334355
m => !reviews.find(r => r.model === m)
335356
);
336357
const consultCmds = missingModels.map(
337-
m => `consult --model ${m} --type ${verifyConfig.type} ${consultType} ${state.id}`
358+
m => `consult --model ${m} --type ${verifyConfig.type} --output "${getReviewFilePath(projectRoot, state, m)}" ${consultType} ${state.id}`
338359
);
339360

340361
return {

0 commit comments

Comments
 (0)