Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions shared/lib/agent-adapters.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,7 @@ agent_verify_launch() {
local baseline_command="${5:-}"
local baseline_children="${6:-}"
local target="$session:$window"
local saw_probe_data=0

local attempts
attempts=$(awk "BEGIN { v = $max_wait / $poll_interval; if (v < 1) v = 1; printf \"%d\", (v == int(v) ? v : int(v) + 1) }")
Expand All @@ -1656,15 +1657,19 @@ agent_verify_launch() {
current_command=$(_pane_current_command "$target")
children=$(_pane_child_count "$target")

if [[ -z "$current_command" && -z "$children" ]]; then
_agent_log_debug "Launch verification unavailable for $target; accepting best-effort dispatch"
if (( attempt == 1 )) \
&& [[ -z "$baseline_command" ]] \
&& [[ -z "${baseline_children:-}" ]] \
&& [[ -z "$current_command" ]] \
&& [[ -z "$children" ]]; then
_agent_log_warn "Launch could not be verified: tmux pane metadata unavailable for $target; assuming dispatch succeeded"
return 0
fi

if [[ -n "$current_command" || -n "$children" ]]; then
saw_probe_data=1
introspection_available=1
fi

if [[ -n "$baseline_command" ]] || [[ -n "$baseline_children" ]]; then
if [[ "$current_command" != "$baseline_command" ]] || [[ "$children" != "${baseline_children:-}" ]]; then
state_changed=1
Expand All @@ -1689,6 +1694,11 @@ agent_verify_launch() {
(( attempt += 1 ))
done

if (( ! saw_probe_data )); then
_agent_log_warn "Launch could not be verified after retries: tmux pane metadata unavailable for $target; assuming dispatch succeeded"
return 0
fi

_agent_log_warn "Launch not verified: pane $target remained at an idle shell for ${max_wait}s"
return 1
}
Expand Down
68 changes: 68 additions & 0 deletions shared/lib/ready-stage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ describe('ready-stage', () => {
});

describe('checkCIStatus', () => {
it('suppresses gh stderr when fetching checks', () => {
let receivedCommand = '';
const execMock = mock.method(readyStage.readyStageDeps, 'execShellCommand', (cmd: string) => {
receivedCommand = cmd;
return JSON.stringify([]);
});

try {
checkCIStatus(42, '/tmp/test');
assert.match(receivedCommand, /gh pr checks '?42'? --json state,name 2>\/dev\/null$/);
} finally {
execMock.mock.restore();
}
});

it('returns pending for queued checks', () => {
const execMock = mock.method(readyStage.readyStageDeps, 'execShellCommand', () =>
JSON.stringify([{ name: 'Shell and Unit Tests', state: 'QUEUED' }])
Expand Down Expand Up @@ -674,6 +689,59 @@ describe('ready-stage', () => {
});

describe('runReadyStage - integration', () => {
it('suppresses gh stderr while gathering PR context CI metadata', async () => {
const repoDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ready-stage-'));
await fs.writeFile(
path.join(repoDir, '.wavemill-config.json'),
JSON.stringify({ ready: { checks: [], requiredChecks: [] } }),
'utf-8'
);

const commands: string[] = [];
const execMock = mock.method(readyStage.readyStageDeps, 'execShellCommand', (cmd: string) => {
commands.push(cmd);

if (cmd.includes('gh pr view')) {
if (cmd.includes('mergeable,mergeStateStatus')) {
return JSON.stringify({
mergeable: 'MERGEABLE',
mergeStateStatus: 'CLEAN',
});
}

return JSON.stringify({
number: 42,
headRefName: 'feature-branch',
baseRefName: 'main',
url: 'https://github.com/test/repo/pull/42',
files: [],
});
}
if (cmd.includes('gh pr diff')) {
return '';
}
if (cmd.includes('gh pr checks')) {
return JSON.stringify([]);
}
return '';
});

try {
await runReadyStage({
prNumber: 42,
repoDir,
});

assert.ok(
commands.some((cmd) => /gh pr checks '?42'? --json state 2>\/dev\/null$/.test(cmd)),
'expected gatherPRContext to suppress gh stderr'
);
} finally {
execMock.mock.restore();
await fs.rm(repoDir, { recursive: true, force: true });
}
});

it('includes merge conflict status independently from the readiness verdict', async () => {
const repoDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ready-stage-'));
await fs.writeFile(
Expand Down
4 changes: 2 additions & 2 deletions shared/lib/ready-stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async function gatherPRContext(prNumber: number, repoDir: string): Promise<PRCon
let ciStatus = 'unknown';
try {
const checksJson = readyStageDeps.execShellCommand(
`gh pr checks ${escapeShellArg(String(prNumber))} --json state`,
`gh pr checks ${escapeShellArg(String(prNumber))} --json state 2>/dev/null`,
{ encoding: 'utf-8', cwd: repoDir }
);
const checks = JSON.parse(checksJson.toString());
Expand Down Expand Up @@ -537,7 +537,7 @@ function checkReleaseRequirements(
export function checkCIStatus(prNumber: number, repoDir: string): ReadyCheck {
try {
const checksJson = readyStageDeps.execShellCommand(
`gh pr checks ${escapeShellArg(String(prNumber))} --json state,name`,
`gh pr checks ${escapeShellArg(String(prNumber))} --json state,name 2>/dev/null`,
{ encoding: 'utf-8', cwd: repoDir }
);
const checks = JSON.parse(checksJson.toString());
Expand Down
14 changes: 12 additions & 2 deletions shared/lib/wavemill-startup-runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ save_task_state() {
| if $reviewerModel != "" then .tasks[$issue].reviewerModel = $reviewerModel else . end
| if $planDepth != "" then .tasks[$issue].planDepth = $planDepth else . end
| if $codeDepth != "" then .tasks[$issue].codeDepth = $codeDepth else . end
| if $reviewMode != "" then .tasks[$issue].reviewMode = $reviewMode else . end' \
| if $reviewMode != "" then .tasks[$issue].reviewMode = $reviewMode else . end
| if $phase != "" then .tasks[$issue].phase = $phase else . end' \
"$STATE_FILE" > "$tmp" 2>/dev/null; then
mv "$tmp" "$STATE_FILE"
return 0
Expand Down Expand Up @@ -421,7 +422,6 @@ $details_context"
# Persist launched tasks as active coding work in the initial state write so
# downstream startup checks do not depend on a second jq update succeeding.
local persisted_phase="coding"

if ! save_task_state "$issue" "$slug" "$branch" "$wt_dir" "" "" "$task_agent" "$linear_issue" "$challenge" "$challenge_pair" "$challenge_role" "$challenge_model" \
"$planner_model" "$coder_model" "$reviewer_model" "$plan_depth" "$code_depth" "$review_mode" "$persisted_phase"; then
startup_log "✗ $issue FAILED at step [5/7]: saving workflow state"
Expand All @@ -447,6 +447,16 @@ $details_context"
startup_log "✗ $issue FAILED at step [6/7]: launching coding agent"
return 1
fi

# Re-persist the launched task after the pane handoff succeeds so the final
# workflow record reflects a fully launched coding session.
if ! save_task_state "$issue" "$slug" "$branch" "$wt_dir" "" "" "$task_agent" "$linear_issue" "$challenge" "$challenge_pair" "$challenge_role" "$challenge_model" \
"$planner_model" "$coder_model" "$reviewer_model" "$plan_depth" "$code_depth" "$review_mode" "$persisted_phase"; then
[[ -n "${state_written:-}" ]] && remove_task_state "$issue" >/dev/null 2>&1 || true
tmux kill-window -t "$SESSION:$win" >/dev/null 2>&1 || true
startup_log "✗ $issue FAILED after step [6/7]: re-saving workflow state"
return 1
fi
startup_step "[6/7] Launching agent... ✓"

if should_update_linear_for_task "$challenge_role"; then
Expand Down
4 changes: 1 addition & 3 deletions tests/launch-ready-phase.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ run_launch_case() {
;;
clean_with_stderr)
printf "%s\n" "{\"prNumber\":304,\"branch\":\"task/fix-failing-ci-tests\",\"verdict\":\"pass\",\"checks\":[{\"name\":\"ci-status\",\"status\":\"pass\",\"message\":\"All CI checks passing\",\"details\":{\"totalChecks\":3}}],\"timestamp\":\"2026-04-16T14:12:00.431Z\",\"summary\":\"All checks passed\",\"mergeConflict\":{\"status\":\"CLEAN\",\"message\":\"No merge conflicts detected\",\"mergeable\":\"MERGEABLE\",\"mergeStateStatus\":\"CLEAN\",\"attempts\":1}}"
printf "%s\n" "⚠️ MERGE CONFLICT: PR #304 has conflicts with main" >&2
return 0
;;
fail_with_stderr)
Expand Down Expand Up @@ -276,9 +275,8 @@ check_contains "pass after remediation writes completed stage" "$output" "|ready
check_not_contains "pass after remediation clears remediation artifacts" "$output" "\"remediationAttempts\":"

output="$(run_launch_case clean_with_stderr)"
check_contains "success stderr stays in debug logs" "$output" "debug [ready stderr] ⚠️ MERGE CONFLICT: PR #304 has conflicts with main"
check_not_contains "success stderr does not leak to terminal" "$output" $'\n⚠️ MERGE CONFLICT: PR #304 has conflicts with main\n'
check_contains "success stderr is not treated as error" "$output" "error_count=0"
check_not_contains "success path does not log ready stderr" "$output" "[ready stderr] ⚠️ MERGE CONFLICT: PR #304 has conflicts with main"

output="$(run_launch_case fail_with_stderr)"
check_contains "failure stderr is logged as error" "$output" "error_payload= [ready stderr] TypeError: ready crashed"
Expand Down
22 changes: 0 additions & 22 deletions tools/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ runTool({
const repoDir = args['repo-dir'] || process.cwd();
const result = await runReadyStage({ prNumber, repoDir });

if (result.mergeConflict) {
printMergeConflictStatus(result);
}

// Output JSON for scripting
console.log(JSON.stringify(result, null, 2));

Expand Down Expand Up @@ -66,21 +62,3 @@ function extractPrNumber(input: string): number {

throw new Error(`Invalid PR number or URL: ${input}`);
}

function printMergeConflictStatus(result: Awaited<ReturnType<typeof runReadyStage>>): void {
const status = result.mergeConflict?.status;

switch (status) {
case 'CONFLICTED':
console.error(`⚠️ MERGE CONFLICT: PR #${result.prNumber} has conflicts with main`);
break;
case 'UNKNOWN':
console.error(`⏳ MERGE STATUS UNKNOWN: PR #${result.prNumber} - GitHub computing mergeability`);
break;
case 'ERROR':
console.error(`⚠️ MERGE STATUS ERROR: PR #${result.prNumber} - ${result.mergeConflict?.message}`);
break;
default:
break;
}
}
Loading