You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
agent auth failure is not session-fatal — it burns the entire queue in seconds #431
When the spawned agent CLI cannot authenticate (observed: Claude Code CLI returning Failed to authenticate: OAuth session expired and could not be refreshed), the loop treats each agent exit as an ordinary per-issue failure. Result on 2.4.0: it churned through the queue at ~13s/issue, relabeling every issue failed, opening a session PR titled "0/N succeeded" with zero code — instead of stopping at the first sign that no agent can run at all.
Suggested fixes:
Pre-flight agent liveness probe (e.g. echo ping | <agent> -p) before creating the session; abort with a clear auth error if it fails.
Treat consecutive instant agent failures (< a few seconds, identical auth error) as session-fatal: stop, restore labels, no session PR.
Found on alpha-loop 2.4.0, macOS, agent: claude, batch off, flat 5-issue run.
user
You just implemented GitHub issue #431: agent auth failure is not session-fatal — it burns the entire queue in seconds
Original Requirements
When the spawned agent CLI cannot authenticate (observed: Claude Code CLI returning Failed to authenticate: OAuth session expired and could not be refreshed), the loop treats each agent exit as an ordinary per-issue failure. Result on 2.4.0: it churned through the queue at ~13s/issue, relabeling every issue failed, opening a session PR titled "0/N succeeded" with zero code — instead of stopping at the first sign that no agent can run at all.
Suggested fixes:
Pre-flight agent liveness probe (e.g. echo ping | <agent> -p) before creating the session; abort with a clear auth error if it fails.
Treat consecutive instant agent failures (< a few seconds, identical auth error) as session-fatal: stop, restore labels, no session PR.
Found on alpha-loop 2.4.0, macOS, agent: claude, batch off, flat 5-issue run.
Code Changes (first 5000 chars)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e443cd4..667183a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [master]
pull_request:
branches: [master]
branches: [master, 'session/', 'agent/']
jobs:
type-check:
diff --git a/CLAUDE.md b/CLAUDE.md
index df3bf3f..a870ce5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,6 +49,8 @@ pnpm test # Run all tests
pnpm build # Build TypeScript to dist/
+Every non-dry `alpha-loop run` checks the selected agent CLI with a short, text-only liveness prompt before session creation. Agent authentication failure exits before session/git/GitHub mutation; authentication loss during processing requeues affected work and stops the remaining queue without an empty session PR.
+
## Directory Structure
diff --git a/README.md b/README.md
index 7cf55b8..c25d262 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,8 @@ Milestones answer "when should this epic ship?" The epic checklist answers "what
Alpha Loop implements a 12-step pipeline for each issue:
+Before creating a session, every non-dry run checks the configured agent binary and sends a short, text-only liveness prompt. Authentication, spawn, or timeout failures exit with a clear diagnostic before any session directory, branch, worktree, issue label change, or session PR is created. If authentication is lost during an active run, the affected issue or batch is restored to ready/Todo and the remaining queue stops; an empty 0/N succeeded session PR is never opened.
+
Status Update — Labels issue in-progress, assigns to you, updates project board
Worktree — Creates an isolated git worktree so work doesn't conflict with other issues
Plan — Agent analyzes the issue and enriches it with implementation details
diff --git a/review-issue-431.json b/review-issue-431.json
new file mode 100644
index 0000000..2508263
--- /dev/null
+++ b/review-issue-431.json
@@ -0,0 +1,12 @@
+{
"passed": true,
"summary": "Preflight and mid-session agent authentication failures now stop safely without burning the remaining queue or opening an empty session PR.",
"findings": [
{
"severity": "critical",
"description": "Authentication loss during test repair, code review, review repair, live verification, verification repair, or batch review was still treated as an ordinary issue failure, allowing later queued issues to run.",
"fixed": true,
"file": "src/lib/pipeline.ts"
}
]
+}
diff --git a/src/commands/run.ts b/src/commands/run.ts
index 40b971b..1413bd1 100644
--- a/src/commands/run.ts
+++ b/src/commands/run.ts
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
import { log } from '../lib/logger.js';
import { exec, shellQuote } from '../lib/shell.js';
-import { loadConfig, assertSafeShellArg, resolveStepConfig, type Config } from '../lib/config.js';
+import { loadConfig, assertSafeShellArg, resolveRoutingStage, resolveStepConfig, type Config } from '../lib/config.js';
import {
pollIssues, listMilestones, listEpics, getEpicSubIssues, getIssueWithComments,
getMergedPRForIssue, updateEpicChecklist, commentIssue, closeIssue, labelIssue,
@@ -47,7 +47,14 @@ import { runPreflight } from '../lib/preflight.js';
import { syncAgentAssets, resolveHarnesses } from './sync.js';
import { saveCapturedCase, detectFailureStep } from '../lib/eval.js';
import { readGateResult, formatGateFindings } from '../lib/pipeline.js';
-import { spawnAgent } from '../lib/agent.js';
+import {
agentCliCommand,
buildEndpointEnv,
probeAgentLiveness,
spawnAgent,
type AgentFailure,
type AgentType,
+} from '../lib/agent.js';
import { buildSessionReviewPrompt, type EpicPromptContext } from '../lib/prompts.js';
import { writeTraceToSubdir } from '../lib/traces.js';
import { validateGeneratedMarkdownForCommit } from '../lib/scan-validation.js';
@@ -141,7 +148,8 @@ export type EpicExecutionFailureCode =
| 'epic-run-error'
| 'session-test-failed'
| 'session-review-failed'
| 'quick-finalize-failed';
| 'quick-finalize-failed'
| 'agent-unavailable';
export type EpicExecutionFailure = {
code: EpicExecutionFailureCode;
@@ -388,17 +396,18 @@ async function pauseIssueForAutomationPolicy(args: {
Check that required CLI tools are installed.
Also warns about optional tools (playwright-cli) that improve the pipeline.
*/
-function checkPrerequisites(config: Config): void {
+function checkPrerequisites(config: Config, selectedAgent: string = config.agent): void {
const AGENT_INSTALL_URLS: Record<string, string> = {
claude: 'https://claude.ai/code',
codex: 'https://developers.openai.com/codex/cli/reference',
opencode: 'https://github.com/sst/opencode',
};
Preflight and mid-session agent authentication failures now stop safely without burning the remaining queue or opening an empty session PR.
Analyze the implementation and list any assumptions or decisions you had to make where the requirements were ambiguous or incomplete. Output ONLY a markdown document with this structure:
Assumptions
(list each assumption made, e.g. "Assumed the date format should be ISO 8601 since it wasn't specified")
If no assumptions were needed, write "None — requirements were fully specified"
Decisions
(list each design/implementation decision where multiple valid approaches existed, e.g. "Chose to validate on the server side rather than client side for security")
If no notable decisions, write "None — implementation was straightforward"
Items to Validate
(list specific things the user should check, e.g. "Verify the error message wording matches your team's style guide")
If nothing needs validation, write "None"
Keep it concise. Only include genuinely ambiguous items, not obvious implementation choices.
codex
I’m using the code-review skill to inspect the implementation’s actual behavior and isolate only genuinely ambiguous assumptions and design choices.
exec
/bin/zsh -lc "sed -n '1,240p' .agents/skills/code-review/SKILL.md" in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
name: code-review
description: Code review standards for automated and manual reviews. Use when reviewing code changes.
auto_load: true
priority: high
Code Review Skill
Trigger
When reviewing code changes (PR review, post-implementation review).
Review Checklist
1. Requirements Compliance
All acceptance criteria met
No scope creep (unnecessary changes)
Edge cases handled
2. Code Quality
Follows project conventions (check CLAUDE.md)
No any types in TypeScript
Functions are small and focused
No dead code or commented-out code
No console.log left in production code
3. Security (OWASP Top 10)
No SQL injection (use parameterized queries)
No XSS (sanitize user input)
No command injection (validate shell args)
Auth/authz checks in place
No secrets in code
4. Testing
New code has tests
Tests cover happy path AND error cases
Tests are deterministic (no flaky tests)
Test names describe behavior, not implementation
5. Performance
No N+1 queries
No unnecessary re-renders
Large lists paginated
Action on Findings
CRITICAL: Fix immediately before merge
WARNING: Fix now if possible, create issue if not
SUGGESTION: Note for future improvement
Output Format
### Review Summary**Status**: PASS | FAIL
**Issues fixed**: N
**Issues deferred**: N (with issue links)
exec
/bin/zsh -lc 'git status --short && git diff --stat && git diff -- src/lib/agent.ts src/commands/run.ts src/lib/pipeline.ts tests README.md CLAUDE.md .github/workflows/test.yml' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
?? .alpha-loop/learnings/issue-429-20260824-073503.md
?? .alpha-loop/learnings/issue-430-20260824-074500.md
exec
/bin/zsh -lc 'git log --oneline --decorate -8 && git show --stat --oneline --decorate HEAD && git branch --show-current && git merge-base HEAD master' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms: 28f5fd5 (HEAD -> session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue, origin/session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue) feat: agent auth failure is not session-fatal — it burns the entire queue in seconds (closes #431) (#436) 847dae3 feat: worktree setup unconditionally runs pnpm install, breaking zero-dependency repos (closes #430) (#435) 93976f6 feat: fix: CI never runs on issue PRs (session-branch base), so #404's merge gate will stall every bundle (closes #429) (#434) 9af431d chore: start session session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue b69a5be (origin/master, origin/HEAD, release-staging, master) Checkpoint before M3 migration (2026-08-19) 613d641 fix: make epic verification evidence authoritative (#398) (#399) 1f59697 Merge pull request #393 from bradtaylorsf/fix/GH-392-fail-closed-review-gates fca7824 (origin/fix/GH-392-fail-closed-review-gates, fix/GH-392-fail-closed-review-gates) fix: fail closed on missing review gates (#392) 28f5fd5 (HEAD -> session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue, origin/session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue) feat: agent auth failure is not session-fatal — it burns the entire queue in seconds (closes #431) (#436)
CLAUDE.md | 2 +
README.md | 2 +
review-issue-431.json | 12 ++
src/commands/run.ts | 171 ++++++++++++++++++++++--
src/lib/agent.ts | 160 +++++++++++++++++++++-
src/lib/pipeline.ts | 208 +++++++++++++++++++++++++++--
src/lib/traces.ts | 2 +-
tests/commands/epic-first-workflow.test.ts | 5 +
tests/commands/run.test.ts | 201 +++++++++++++++++++++++++++-
tests/lib/agent.test.ts | 152 +++++++++++++++++++++
tests/lib/pipeline.test.ts | 182 +++++++++++++++++++++++++
11 files changed, 1073 insertions(+), 24 deletions(-)
session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue b69a5be
exec
/bin/zsh -lc 'git show --format=fuller --find-renames 28f5fd5 -- src/lib/agent.ts tests/lib/agent.test.ts' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
commit 28f5fd5
Author: Brad Taylor bradtaylorsf@gmail.com
AuthorDate: Mon Aug 24 08:16:27 2026 -0700
Commit: GitHub noreply@github.com
CommitDate: Mon Aug 24 08:16:27 2026 -0700
feat: agent auth failure is not session-fatal — it burns the entire queue in seconds (closes #431) (#436)
* feat: agent auth failure is not session-fatal — it burns the entire queue in seconds (closes #431)
* fix: address review findings for #431
Safe-to-log agent failure metadata. The fingerprint is deterministic but
never contains raw CLI output, which can include account or endpoint data.
*/
+export type AgentFailure = {
kind: AgentFailureKind;
fingerprint: string;
diagnostic: string;
durationMs: number;
+};
+export type AgentProbeResult = {
ok: boolean;
duration: number;
failure?: AgentFailure;
};
/**
@@ -127,8 +149,26 @@ export type AgentOptions = {
* frontier stage does not inherit a local-endpoint env from a prior stage.
*/
env?: Record<string, string>;
/** Restrict the invocation to a stdout-only/read-only liveness check. */
textOnly?: boolean;
};
+/** Resolve aliases to the CLI binary actually spawned. */
+export function agentCliCommand(agent: AgentType): string {
switch (agent) {
case 'claude':
case 'lmstudio':
return 'claude';
case 'codex':
case 'ollama':
return 'codex';
case 'opencode':
return 'opencode';
default:
throw new Error(`Unknown agent type: ${agent}`);
}
+}
/**
Extra sandbox config the codex CLI needs when running inside a linked git
worktree: the worktree's index and metadata live in the PARENT repo's .git
@@ -177,6 +217,10 @@ export function buildAgentArgs(options: AgentOptions): { command: string; args:
if (options.resume) args.push('--continue');
args.push('-p');
if (options.model) args.push('--model', options.model);
if (failure) result.failure = failure;
if (parsedResultSubtype !== undefined) result.resultSubtype = parsedResultSubtype;
if (parsedResultIsError) result.resultIsError = true;
if (logStream) {
'Re-authenticate or repair the configured agent CLI, then retry.';
throw new CommandExitError({
code: 'agent-unavailable',
message,
exitCode: 1,
});
+}
/**
Print the startup banner showing all configuration.
*/
@@ -912,6 +949,12 @@ async function runIssueSession(
log.info(Filtering issues by milestone: ${activeMilestone});
}
// This boundary must remain before createSession: a dead or unauthenticated
// CLI must not create session directories, branches, worktrees, or PR state.
if (!config.dryRun) {
await assertAgentAvailable(config);
}
// Create session (named after epic or milestone if selected). Session names
// are deterministic per epic/milestone, so createSession locks the session
// directory — a concurrently started run of the same target fails fast here
@@ -1118,9 +1161,6 @@ async function executeSessionRun(
};
}
// Check prerequisites
checkPrerequisites(config);
// Track active worktree for cleanup on signal
let activeIssueNum: number | null = null;
const runAbortController = new AbortController();
@@ -1351,6 +1391,7 @@ async function executeSessionRun(
// must not add a second entry for each of them.
const demotedQuickIssueNums = new Set();
let epicVerificationSummary: string | null = null;
let agentUnavailableFailure: AgentFailure | undefined;
if (issues.length === 0) {
log.info('No issues found. Nothing to do.');
@@ -1495,6 +1536,17 @@ async function executeSessionRun(
if (runAbortController.signal.aborted) break;
session.results.push(...results);
// Flip epic checklist for each successful sub-issue.
// Skipped in dry-run: `processIssue` returns status='success' when tests are
// stubbed in dry-run, which would otherwise mutate the live epic body.
@@ -1608,6 +1660,15 @@ async function executeSessionRun(
);
if (runAbortController.signal.aborted) break;
session.results.push(result);
log.error(Session stopped after ${successfulResults.length} issue(s) succeeded; remaining work was not attempted);
process.off('SIGINT', handleSigint);
process.off('SIGTERM', handleSigterm);
return {
session,
sessionPrUrl,
failures,
verificationClosedEpic,
waiting: false,
};
}
// Changed scope keeps per-issue runs focused, so require one aggregate full
// suite pass before epic verification, post-session review, and finalization.
// Quick mode already runs this same gate inside finalizeQuickRun.
@@ -1832,6 +1982,7 @@ async function executeSessionRun(
// quick-finalize-failed entry — a per-issue duplicate is manifest noise.
if (demotedQuickIssueNums.has(result.issueNum)) continue;
if (result.status === 'failure' && !isRecoveredRunResult(result)) {
test('passes compact parent epic context to sub-issue processing for --epic runs', async () => {
const epicBody = `## Goal
Ship the epic as a coordinated set of sub-issues.
exec
/bin/zsh -lc 'git show --format= 28f5fd5 -- src/lib/pipeline.ts tests/lib/pipeline.test.ts src/lib/traces.ts' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
diff --git a/src/lib/pipeline.ts b/src/lib/pipeline.ts
index b5a6e9b..49c2b24 100644
--- a/src/lib/pipeline.ts
+++ b/src/lib/pipeline.ts
@@ -5,7 +5,7 @@ import { mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from '
import { basename, join, relative } from 'node:path';
import { log } from './logger.js';
import { exec, shellQuote } from './shell.js';
-import { spawnAgent, buildEndpointEnv } from './agent.js';
+import { spawnAgent, buildEndpointEnv, classifyAgentFailure } from './agent.js';
import type { AgentOptions } from './agent.js';
import {
classifyToolError,
@@ -91,7 +91,7 @@ import {
import type { StepCost, PipelineResultForScores } from './traces.js';
import { estimateCost, getFallbackPolicy, resolveRoutingStage, resolveStepConfig, selectRoutingProfile } from './config.js';
import type { Config, PipelineStepName, RoutingStageName, RoutingEndpointType } from './config.js';
-import type { AgentResult } from './agent.js';
+import type { AgentFailure, AgentResult } from './agent.js';
import type { SessionContext } from './session.js';
import { buildStageTelemetry, writeStageTelemetry } from './telemetry.js';
import { emitLifecycleEvent } from './events.js';
@@ -469,8 +469,10 @@ export type PipelineResult = {
consoleErrors?: string[];
/** Failed network requests captured during web/app verification. */
networkErrors?: string[];
/** Why the issue failed — 'transient' means re-queue (e.g. usage limit), 'permanent' means label failed. */
failureReason?: 'transient' | 'permanent';
/** Why the issue failed; unavailable agents are re-queued and stop the owning session. */
// Detect transient errors (usage limits) during planning
if (planResult.exitCode !== 0 && isTransientError(planResult.output)) {
log.warn(`Agent hit a transient error during planning for #${issueNum} — re-queuing`);
// Commit any work the agent left uncommitted BEFORE honoring pause
// requests or failure handling. A sandboxed agent that cannot write the
// parent repo's git metadata (linked worktrees keep their index under the
@@ -1760,7 +1843,7 @@ export async function processIssue(
worktreePath, feat: implement issue #${issueNum} - ${title},
);
} else if (!isTransientError(implResult.output)) {
} else if (!implementationAgentFailure && !isTransientError(implResult.output)) {
// Permanent failures keep a wip commit so the worktree is preserved for
// recovery. Transient failures (rate limits) are re-queued for a fresh
// retry — a wip commit here would pin the worktree via preserveIfCommits
@@ -1775,6 +1858,9 @@ export async function processIssue(
if (pauseResult) return pauseResult;
}
if (isTransientError(implResult.output)) {
log.warn(`Agent hit a transient error during implementation for #${issueNum} — re-queuing`);
requeueIssue(config, issueNum);
// Auto-commit before the pause check so sandboxed agents that cannot
// commit never strand review fixes uncommitted.
autoCommitDirtyWorktree(worktreePath, `fix(#${issueNum}): address review findings (attempt ${attempt})`);
@@ -2226,6 +2326,12 @@ export async function processIssue(
...epicOption,
Activity
bradtaylorsf commentedon Aug 24, 2026
Automated implementation complete.
PR: #436
Tests: PASSING
Review: Attached to PR body.
Processed by alpha-loop in 1872s
feat: agent auth failure is not session-fatal — it burns the entire q…
bradtaylorsf commentedon Aug 24, 2026
AI Implementation Notes
warning:
--full-autois deprecated; use--sandbox workspace-writeinstead.Reading prompt from stdin...
2026-08-24T15:16:43.094180Z ERROR codex_core::session::session: failed to load skill /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue/.agents/skills/playwright-testing/SKILL.md: missing YAML frontmatter delimited by ---
OpenAI Codex v0.144.1
workdir: /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
model: gpt-5.6-sol
provider: openai
approval: never
sandbox: workspace-write [workdir, /tmp, $TMPDIR, /Users/bradtaylor/Documents/GitHub/alpha-loop/.git] (network access enabled)
reasoning effort: high
reasoning summaries: none
session id: 01a03458-6396-7b20-bf4a-081b9b2d3ffd
user
You just implemented GitHub issue #431: agent auth failure is not session-fatal — it burns the entire queue in seconds
Original Requirements
When the spawned agent CLI cannot authenticate (observed: Claude Code CLI returning
Failed to authenticate: OAuth session expired and could not be refreshed), the loop treats each agent exit as an ordinary per-issue failure. Result on 2.4.0: it churned through the queue at ~13s/issue, relabeling every issuefailed, opening a session PR titled "0/N succeeded" with zero code — instead of stopping at the first sign that no agent can run at all.Suggested fixes:
echo ping | <agent> -p) before creating the session; abort with a clear auth error if it fails.Found on alpha-loop 2.4.0, macOS, agent: claude, batch off, flat 5-issue run.
Code Changes (first 5000 chars)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e443cd4..667183a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [master]
pull_request:
jobs:
type-check:
diff --git a/CLAUDE.md b/CLAUDE.md
index df3bf3f..a870ce5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,6 +49,8 @@ pnpm test # Run all tests
pnpm build # Build TypeScript to dist/
diff --git a/README.md b/README.md
index 7cf55b8..c25d262 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,8 @@ Milestones answer "when should this epic ship?" The epic checklist answers "what
Alpha Loop implements a 12-step pipeline for each issue:
+Before creating a session, every non-dry run checks the configured agent binary and sends a short, text-only liveness prompt. Authentication, spawn, or timeout failures exit with a clear diagnostic before any session directory, branch, worktree, issue label change, or session PR is created. If authentication is lost during an active run, the affected issue or batch is restored to
ready/Todo and the remaining queue stops; an empty0/N succeededsession PR is never opened.+
in-progress, assigns to you, updates project boarddiff --git a/review-issue-431.json b/review-issue-431.json
new file mode 100644
index 0000000..2508263
--- /dev/null
+++ b/review-issue-431.json
@@ -0,0 +1,12 @@
+{
+}
diff --git a/src/commands/run.ts b/src/commands/run.ts
index 40b971b..1413bd1 100644
--- a/src/commands/run.ts
+++ b/src/commands/run.ts
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
import { log } from '../lib/logger.js';
import { exec, shellQuote } from '../lib/shell.js';
-import { loadConfig, assertSafeShellArg, resolveStepConfig, type Config } from '../lib/config.js';
+import { loadConfig, assertSafeShellArg, resolveRoutingStage, resolveStepConfig, type Config } from '../lib/config.js';
import {
pollIssues, listMilestones, listEpics, getEpicSubIssues, getIssueWithComments,
getMergedPRForIssue, updateEpicChecklist, commentIssue, closeIssue, labelIssue,
@@ -47,7 +47,14 @@ import { runPreflight } from '../lib/preflight.js';
import { syncAgentAssets, resolveHarnesses } from './sync.js';
import { saveCapturedCase, detectFailureStep } from '../lib/eval.js';
import { readGateResult, formatGateFindings } from '../lib/pipeline.js';
-import { spawnAgent } from '../lib/agent.js';
+import {
+} from '../lib/agent.js';
import { buildSessionReviewPrompt, type EpicPromptContext } from '../lib/prompts.js';
import { writeTraceToSubdir } from '../lib/traces.js';
import { validateGeneratedMarkdownForCommit } from '../lib/scan-validation.js';
@@ -141,7 +148,8 @@ export type EpicExecutionFailureCode =
| 'epic-run-error'
| 'session-test-failed'
| 'session-review-failed'
export type EpicExecutionFailure = {
code: EpicExecutionFailureCode;
@@ -388,17 +396,18 @@ async function pauseIssueForAutomationPolicy(args: {
*/
-function checkPrerequisites(config: Config): void {
+function checkPrerequisites(config: Config, selectedAgent: string = config.agent): void {
const AGENT_INSTALL_URLS: Record<string, string> = {
claude: 'https://claude.ai/code',
codex: 'https://developers.openai.com/codex/cli/reference',
opencode: 'https://github.com/sst/opencode',
};
Review Summary
Preflight and mid-session agent authentication failures now stop safely without burning the remaining queue or opening an empty session PR.
Analyze the implementation and list any assumptions or decisions you had to make where the requirements were ambiguous or incomplete. Output ONLY a markdown document with this structure:
Assumptions
Decisions
Items to Validate
Keep it concise. Only include genuinely ambiguous items, not obvious implementation choices.
codex
I’m using the code-review skill to inspect the implementation’s actual behavior and isolate only genuinely ambiguous assumptions and design choices.
exec
/bin/zsh -lc "sed -n '1,240p' .agents/skills/code-review/SKILL.md" in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
name: code-review
description: Code review standards for automated and manual reviews. Use when reviewing code changes.
auto_load: true
priority: high
Code Review Skill
Trigger
When reviewing code changes (PR review, post-implementation review).
Review Checklist
1. Requirements Compliance
2. Code Quality
anytypes in TypeScriptconsole.logleft in production code3. Security (OWASP Top 10)
4. Testing
5. Performance
Action on Findings
Output Format
exec
/bin/zsh -lc 'git status --short && git diff --stat && git diff -- src/lib/agent.ts src/commands/run.ts src/lib/pipeline.ts tests README.md CLAUDE.md .github/workflows/test.yml' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
?? .alpha-loop/learnings/issue-429-20260824-073503.md
?? .alpha-loop/learnings/issue-430-20260824-074500.md
exec
/bin/zsh -lc 'git log --oneline --decorate -8 && git show --stat --oneline --decorate HEAD && git branch --show-current && git merge-base HEAD master' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
28f5fd5 (HEAD -> session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue, origin/session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue) feat: agent auth failure is not session-fatal — it burns the entire queue in seconds (closes #431) (#436)
847dae3 feat: worktree setup unconditionally runs pnpm install, breaking zero-dependency repos (closes #430) (#435)
93976f6 feat: fix: CI never runs on issue PRs (session-branch base), so #404's merge gate will stall every bundle (closes #429) (#434)
9af431d chore: start session session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
b69a5be (origin/master, origin/HEAD, release-staging, master) Checkpoint before M3 migration (2026-08-19)
613d641 fix: make epic verification evidence authoritative (#398) (#399)
1f59697 Merge pull request #393 from bradtaylorsf/fix/GH-392-fail-closed-review-gates
fca7824 (origin/fix/GH-392-fail-closed-review-gates, fix/GH-392-fail-closed-review-gates) fix: fail closed on missing review gates (#392)
28f5fd5 (HEAD -> session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue, origin/session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue) feat: agent auth failure is not session-fatal — it burns the entire queue in seconds (closes #431) (#436)
CLAUDE.md | 2 +
README.md | 2 +
review-issue-431.json | 12 ++
src/commands/run.ts | 171 ++++++++++++++++++++++--
src/lib/agent.ts | 160 +++++++++++++++++++++-
src/lib/pipeline.ts | 208 +++++++++++++++++++++++++++--
src/lib/traces.ts | 2 +-
tests/commands/epic-first-workflow.test.ts | 5 +
tests/commands/run.test.ts | 201 +++++++++++++++++++++++++++-
tests/lib/agent.test.ts | 152 +++++++++++++++++++++
tests/lib/pipeline.test.ts | 182 +++++++++++++++++++++++++
11 files changed, 1073 insertions(+), 24 deletions(-)
session/epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
b69a5be
exec
/bin/zsh -lc 'git show --format=fuller --find-renames 28f5fd5 -- src/lib/agent.ts tests/lib/agent.test.ts' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
commit 28f5fd5
Author: Brad Taylor bradtaylorsf@gmail.com
AuthorDate: Mon Aug 24 08:16:27 2026 -0700
Commit: GitHub noreply@github.com
CommitDate: Mon Aug 24 08:16:27 2026 -0700
diff --git a/src/lib/agent.ts b/src/lib/agent.ts
index fbab5e8..79e9e64 100644
--- a/src/lib/agent.ts
+++ b/src/lib/agent.ts
@@ -2,6 +2,7 @@
/
import { spawn } from 'node:child_process';
+import { createHash } from 'node:crypto';
import { createWriteStream, type WriteStream } from 'node:fs';
import { resolve, relative, isAbsolute } from 'node:path';
import { log } from './logger.js';
@@ -36,6 +37,27 @@ export type AgentResult = {
resultSubtype?: string;
/* Whether the Claude stream-json result reported an error. */
resultIsError?: boolean;
+};
+export type AgentFailureKind = 'authentication' | 'spawn' | 'timeout' | 'execution';
+
+/**
+export type AgentFailure = {
+};
+export type AgentProbeResult = {
};
/**
@@ -127,8 +149,26 @@ export type AgentOptions = {
* frontier stage does not inherit a local-endpoint env from a prior stage.
*/
env?: Record<string, string>;
};
+/** Resolve aliases to the CLI binary actually spawned. */
+export function agentCliCommand(agent: AgentType): string {
+}
/**
@@ -177,6 +217,10 @@ export function buildAgentArgs(options: AgentOptions): { command: string; args:
if (options.resume) args.push('--continue');
args.push('-p');
if (options.model) args.push('--model', options.model);
@@ -185,7 +229,7 @@ export function buildAgentArgs(options: AgentOptions): { command: string; args:
if (options.maxTurns) {
args.push('--max-turns', String(options.maxTurns));
}
case 'codex':
case 'ollama': {
@@ -196,20 +240,103 @@ export function buildAgentArgs(options: AgentOptions): { command: string; args:
args.push('exec');
}
if (options.model) args.push('--model', options.model);
case 'opencode': {
const args = ['run'];
if (options.model) args.push('--model', options.model);
default:
throw new Error(
Unknown agent type: ${options.agent});}
}
+const AUTH_FAILURES: Array<{
+}> = [
+];
+/**
+export function classifyAgentFailure(
+): AgentFailure | undefined {
exit:${exitCode}).digest('hex').slice(0, 12);execution:${digest},Agent CLI exited with code ${exitCode},+}
/**
@@ -555,6 +682,8 @@ export async function spawnAgent(options: AgentOptions): Promise {
toolCalls: toolUseCount,
toolErrors: toolErrorsFinal,
};
@@ -589,3 +718,28 @@ export async function spawnAgent(options: AgentOptions): Promise {
});
});
}
+
+/**
+export async function probeAgentLiveness(
+): Promise {
+}
diff --git a/tests/lib/agent.test.ts b/tests/lib/agent.test.ts
index 7b50a8c..0d58257 100644
--- a/tests/lib/agent.test.ts
+++ b/tests/lib/agent.test.ts
@@ -2,7 +2,9 @@ import {
buildAgentArgs,
buildEndpointEnv,
buildOneShotCommand,
codexSandboxArgs,
spawnAgent,
DEFAULT_LMSTUDIO_BASE_URL,
DEFAULT_OLLAMA_BASE_URL,
@@ -52,6 +54,30 @@ beforeEach(() => {
});
describe('buildAgentArgs', () => {
const result = buildAgentArgs({
agent: 'claude',
@@ -180,6 +206,132 @@ describe('buildAgentArgs', () => {
});
});
+describe('classifyAgentFailure', () => {
+});
+describe('probeAgentLiveness', () => {
+});
describe('buildOneShotCommand', () => {
test('builds claude command with model', () => {
const cmd = buildOneShotCommand('claude', 'opus');
exec
/bin/zsh -lc 'git show --format= --find-renames 28f5fd5 -- src/commands/run.ts tests/commands/run.test.ts tests/commands/epic-first-workflow.test.ts' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
diff --git a/src/commands/run.ts b/src/commands/run.ts
index 40b971b..1413bd1 100644
--- a/src/commands/run.ts
+++ b/src/commands/run.ts
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
import { log } from '../lib/logger.js';
import { exec, shellQuote } from '../lib/shell.js';
-import { loadConfig, assertSafeShellArg, resolveStepConfig, type Config } from '../lib/config.js';
+import { loadConfig, assertSafeShellArg, resolveRoutingStage, resolveStepConfig, type Config } from '../lib/config.js';
import {
pollIssues, listMilestones, listEpics, getEpicSubIssues, getIssueWithComments,
getMergedPRForIssue, updateEpicChecklist, commentIssue, closeIssue, labelIssue,
@@ -47,7 +47,14 @@ import { runPreflight } from '../lib/preflight.js';
import { syncAgentAssets, resolveHarnesses } from './sync.js';
import { saveCapturedCase, detectFailureStep } from '../lib/eval.js';
import { readGateResult, formatGateFindings } from '../lib/pipeline.js';
-import { spawnAgent } from '../lib/agent.js';
+import {
+} from '../lib/agent.js';
import { buildSessionReviewPrompt, type EpicPromptContext } from '../lib/prompts.js';
import { writeTraceToSubdir } from '../lib/traces.js';
import { validateGeneratedMarkdownForCommit } from '../lib/scan-validation.js';
@@ -141,7 +148,8 @@ export type EpicExecutionFailureCode =
| 'epic-run-error'
| 'session-test-failed'
| 'session-review-failed'
export type EpicExecutionFailure = {
code: EpicExecutionFailureCode;
@@ -388,17 +396,18 @@ async function pauseIssueForAutomationPolicy(args: {
*/
-function checkPrerequisites(config: Config): void {
+function checkPrerequisites(config: Config, selectedAgent: string = config.agent): void {
const AGENT_INSTALL_URLS: Record<string, string> = {
claude: 'https://claude.ai/code',
codex: 'https://developers.openai.com/codex/cli/reference',
opencode: 'https://github.com/sst/opencode',
};
${config.agent} CLI not found.${agentUrl ?Install: ${agentUrl}: ''};${selectedAgent} CLI not found.${agentUrl ?Install: ${agentUrl}: ''};const cliCommand = agentCliCommand(selectedAgent as AgentType);
const safeAgent = assertSafeShellArg(cliCommand, 'agent');
const tools = [
{ name: 'gh', message: 'GitHub CLI not found. Install: https://cli.github.com/' },
@@ -430,6 +439,34 @@ function checkPrerequisites(config: Config): void {
}
}
+async function assertAgentAvailable(config: Config): Promise {
Checking ${implementStep.agent} agent authentication);Agent liveness probe failed for ${implementStep.agent} (${model || 'default model'}): ${diagnostic}.++}
/**
*/
@@ -912,6 +949,12 @@ async function runIssueSession(
log.info(
Filtering issues by milestone: ${activeMilestone});}
// are deterministic per epic/milestone, so createSession locks the session
// directory — a concurrently started run of the same target fails fast here
@@ -1118,9 +1161,6 @@ async function executeSessionRun(
};
}
let activeIssueNum: number | null = null;
const runAbortController = new AbortController();
@@ -1351,6 +1391,7 @@ async function executeSessionRun(
// must not add a second entry for each of them.
const demotedQuickIssueNums = new Set();
let epicVerificationSummary: string | null = null;
let agentUnavailableFailure: AgentFailure | undefined;
if (issues.length === 0) {
log.info('No issues found. Nothing to do.');
@@ -1495,6 +1536,17 @@ async function executeSessionRun(
if (runAbortController.signal.aborted) break;
session.results.push(...results);
@@ -1608,6 +1660,15 @@ async function executeSessionRun(
);
if (runAbortController.signal.aborted) break;
session.results.push(result);
@@ -1758,6 +1819,95 @@ async function executeSessionRun(
};
}
Agent became unavailable during the session: ${agentUnavailableFailure.diagnostic};Session stopped after ${successfulResults.length} issue(s) succeeded; remaining work was not attempted);// suite pass before epic verification, post-session review, and finalization.
// Quick mode already runs this same gate inside finalizeQuickRun.
@@ -1832,6 +1982,7 @@ async function executeSessionRun(
// quick-finalize-failed entry — a per-issue duplicate is manifest noise.
if (demotedQuickIssueNums.has(result.issueNum)) continue;
if (result.status === 'failure' && !isRecoveredRunResult(result)) {
diff --git a/tests/commands/epic-first-workflow.test.ts b/tests/commands/epic-first-workflow.test.ts
index b424e99..b7628b3 100644
--- a/tests/commands/epic-first-workflow.test.ts
+++ b/tests/commands/epic-first-workflow.test.ts
@@ -71,10 +71,15 @@ jest.mock('../../src/lib/config', () => ({
...overrides,
})),
assertSafeShellArg: jest.fn((val: string) => val),
}));
jest.mock('../../src/lib/agent', () => ({
buildOneShotCommand: jest.fn(() => 'claude -p --output-format text'),
spawnAgent: jest.fn(),
}));
diff --git a/tests/commands/run.test.ts b/tests/commands/run.test.ts
index f422e92..a833b05 100644
--- a/tests/commands/run.test.ts
+++ b/tests/commands/run.test.ts
@@ -22,7 +22,11 @@ jest.mock('../../src/lib/logger', () => ({
jest.mock('../../src/lib/config', () => ({
loadConfig: jest.fn(),
assertSafeShellArg: jest.fn((value: string) => value),
}));
jest.mock('../../src/lib/github', () => ({
@@ -49,6 +53,9 @@ jest.mock('../../src/lib/pipeline', () => ({
jest.mock('../../src/lib/agent', () => ({
spawnAgent: jest.fn(),
}));
jest.mock('../../src/lib/session', () => ({
@@ -151,7 +158,7 @@ import { syncAgentAssets } from '../../src/commands/sync';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { emitLifecycleEvent } from '../../src/lib/events';
-import { spawnAgent } from '../../src/lib/agent';
+import { probeAgentLiveness, spawnAgent } from '../../src/lib/agent';
const mockExec = exec as jest.MockedFunction;
const mockLog = log as jest.Mocked;
@@ -190,6 +197,7 @@ const mockExistsSync = existsSync as jest.MockedFunction;
const mockReadFileSync = readFileSync as jest.MockedFunction;
const mockEmitLifecycleEvent = emitLifecycleEvent as jest.MockedFunction;
const mockSpawnAgent = spawnAgent as jest.MockedFunction;
+const mockProbeAgentLiveness = probeAgentLiveness as jest.MockedFunction;
const mockSpawn = spawn as jest.MockedFunction;
function makeConfig(overrides: Record<string, unknown> = {}) {
@@ -273,6 +281,7 @@ beforeEach(() => {
});
mockCleanupWorktree.mockResolvedValue({ status: 'removed', path: '/tmp/session-worktree' });
mockSpawnAgent.mockResolvedValue({ exitCode: 0, output: 'Review passed', duration: 1 });
mockProbeAgentLiveness.mockResolvedValue({ ok: true, duration: 1 });
mockPollIssues.mockReturnValue([]);
mockListEpics.mockReturnValue([]);
mockGetEpicSubIssues.mockReturnValue([]);
@@ -2447,6 +2456,194 @@ Coordinate hosted work.
expect(mockExec).toHaveBeenCalledWith('command -v "claude"');
});
test('aborts on expired agent OAuth before creating any session state', async () => {
mockProbeAgentLiveness.mockResolvedValue({
});
await runCommand({ skipEpic: true });
expect(process.exitCode).toBe(1);
expect(mockLog.error).toHaveBeenCalledWith(expect.stringContaining('OAuth session expired'));
expect(mockCreateSession).not.toHaveBeenCalled();
expect(mockEnsureSessionWorktree).not.toHaveBeenCalled();
expect(mockPollIssues).not.toHaveBeenCalled();
expect(mockProcessIssue).not.toHaveBeenCalled();
expect(mockFinalizeSession).not.toHaveBeenCalled();
expect(mockExec.mock.invocationCallOrder.at(-1)).toBeLessThan(
);
});
test('dry runs do not invoke the agent liveness probe', async () => {
await runCommand({ skipEpic: true, dryRun: true });
expect(mockProbeAgentLiveness).not.toHaveBeenCalled();
});
test('checks the backing CLI for local-agent aliases', async () => {
mockLoadConfig.mockImplementation((overrides: any = {}) => makeConfig({
}) as any);
await runCommand({ skipEpic: true });
expect(mockExec).toHaveBeenCalledWith('command -v "claude"');
expect(mockExec).not.toHaveBeenCalledWith('command -v "lmstudio"');
expect(mockProbeAgentLiveness).toHaveBeenCalledWith(expect.objectContaining({
}));
});
test('checks and probes the effective implementation agent override', async () => {
mockLoadConfig.mockImplementation((overrides: any = {}) => makeConfig({
}) as any);
await runCommand({ skipEpic: true });
expect(mockExec).toHaveBeenCalledWith('command -v "codex"');
expect(mockExec).not.toHaveBeenCalledWith('command -v "claude"');
expect(mockProbeAgentLiveness).toHaveBeenCalledWith(expect.objectContaining({
}));
});
test('stops the sequential queue and suppresses a zero-success session PR when auth is lost', async () => {
mockPollIssues.mockReturnValue([
]);
mockProcessIssue.mockResolvedValue({
});
await runCommand({ skipEpic: true });
expect(mockProcessIssue).toHaveBeenCalledTimes(1);
expect(mockProcessIssue).toHaveBeenCalledWith(42, expect.anything(), expect.anything(), expect.anything(), expect.anything(), expect.anything());
expect(mockFinalizeSession).not.toHaveBeenCalled();
expect(mockRecordSessionError).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
}));
expect(process.exitCode).toBe(1);
});
test('stops the batch queue and suppresses empty-session finalization when auth is lost', async () => {
mockLoadConfig.mockImplementation((overrides: any = {}) => makeConfig({
}) as any);
mockPollIssues.mockReturnValue([
]);
mockProcessBatch.mockResolvedValue([{
}]);
await runCommand({ skipEpic: true, batch: true, batchSize: 1 });
expect(mockProcessBatch).toHaveBeenCalledTimes(1);
expect(mockFinalizeSession).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
});
test('finalizes prior successes without invoking more agents when auth is lost mid-session', async () => {
mockPollIssues.mockReturnValue([
]);
mockProcessIssue
mockFinalizeSession.mockResolvedValue('https://github.com/owner/repo/pull/500');
await runCommand({ skipEpic: true });
expect(mockProcessIssue).toHaveBeenCalledTimes(2);
expect(mockFinalizeSession).toHaveBeenCalledTimes(1);
expect(mockSpawnAgent).not.toHaveBeenCalled();
expect(mockEmitLifecycleEvent).toHaveBeenCalledWith(expect.objectContaining({
}));
expect(process.exitCode).toBe(1);
});
test('passes compact parent epic context to sub-issue processing for --epic runs', async () => {
const epicBody = `## Goal
Ship the epic as a coordinated set of sub-issues.
exec
/bin/zsh -lc 'git show --format= 28f5fd5 -- src/lib/pipeline.ts tests/lib/pipeline.test.ts src/lib/traces.ts' in /Users/bradtaylor/Documents/GitHub/alpha-loop/.worktrees/session-epic-432-epic-make-an-unattended-run-safe-no-checks-on-issue-prs-install-aborts-dep-free-repos-auth-failure-burns-the-queue
succeeded in 0ms:
diff --git a/src/lib/pipeline.ts b/src/lib/pipeline.ts
index b5a6e9b..49c2b24 100644
--- a/src/lib/pipeline.ts
+++ b/src/lib/pipeline.ts
@@ -5,7 +5,7 @@ import { mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from '
import { basename, join, relative } from 'node:path';
import { log } from './logger.js';
import { exec, shellQuote } from './shell.js';
-import { spawnAgent, buildEndpointEnv } from './agent.js';
+import { spawnAgent, buildEndpointEnv, classifyAgentFailure } from './agent.js';
import type { AgentOptions } from './agent.js';
import {
classifyToolError,
@@ -91,7 +91,7 @@ import {
import type { StepCost, PipelineResultForScores } from './traces.js';
import { estimateCost, getFallbackPolicy, resolveRoutingStage, resolveStepConfig, selectRoutingProfile } from './config.js';
import type { Config, PipelineStepName, RoutingStageName, RoutingEndpointType } from './config.js';
-import type { AgentResult } from './agent.js';
+import type { AgentFailure, AgentResult } from './agent.js';
import type { SessionContext } from './session.js';
import { buildStageTelemetry, writeStageTelemetry } from './telemetry.js';
import { emitLifecycleEvent } from './events.js';
@@ -469,8 +469,10 @@ export type PipelineResult = {
consoleErrors?: string[];
/** Failed network requests captured during web/app verification. */
networkErrors?: string[];
prUrl?: string;
testsPassing: boolean;
verifyPassing: boolean;
@@ -522,6 +524,33 @@ function throwIfPipelineAborted(signal?: AbortSignal): void {
throw error;
}
+function unavailableAgentFailure(result: AgentResult): AgentFailure | undefined {
+}
+const INSTANT_AGENT_FAILURE_MS = 5_000;
+
+function agentFailureForResult(result: AgentResult): AgentFailure | undefined {
+}
+function matchingInstantAgentFailure(
+): AgentFailure | undefined {
+}
const PAUSE_REQUEST_FILE = 'alpha-loop-pause-request.json';
type PauseRequestType = 'human_input' | 'qa' | 'feedback' | 'new_scope';
@@ -1572,11 +1601,53 @@ export async function processIssue(
});
};
const stopForAgentUnavailable = async (
failure: AgentFailure,
stage: 'plan' | 'implement' | 'test' | 'review' | 'verify',
): Promise => {
log.error(
Agent unavailable during ${stage} for #${issueNum}: ${failure.diagnostic});try {
} catch (err) {
}
if (!quickWorktree) {
}
try {
} catch (err) {
}
return failureResult(issueNum, title, startTime, 'agent-unavailable', failure);
};
try {
// --- Step 3: Plan (structured JSON — controls test/verify steps) ---
currentStep = 'plan';
recordSessionStage(session, 'plan');
log.step('Step 3: Planning');
let priorInstantAgentFailure: AgentFailure | undefined;
let plan: IssuePlan = resumeVerificationOnly
? {
...DEFAULT_PLAN,
@@ -1628,6 +1699,11 @@ export async function processIssue(
stepCosts.push(buildStepCost('plan', issueNum, planResult, config));
recordStageTelemetry(session, issueNum, 'plan', planResult, config, planCtx);
@@ -1644,6 +1720,9 @@ export async function processIssue(
recordSessionIssue(session, issueNum, { status: 'failure', stage: 'plan', failureReason: 'transient' });
return failureResult(issueNum, title, startTime, 'transient');
}
@@ -1750,6 +1829,10 @@ export async function processIssue(
stepCosts.push(buildStepCost('implement', issueNum, implResult, config));
recordStageTelemetry(session, issueNum, 'implement', implResult, config, implCtx);
// requests or failure handling. A sandboxed agent that cannot write the
// parent repo's git metadata (linked worktrees keep their index under the
@@ -1760,7 +1843,7 @@ export async function processIssue(
worktreePath,
feat: implement issue #${issueNum} - ${title},);
} else if (!implementationAgentFailure && !isTransientError(implResult.output)) {
// Permanent failures keep a wip commit so the worktree is preserved for
// recovery. Transient failures (rate limits) are re-queued for a fresh
// retry — a wip commit here would pin the worktree via preserveIfCommits
@@ -1775,6 +1858,9 @@ export async function processIssue(
if (pauseResult) return pauseResult;
if (implResult.exitCode !== 0) {
@@ -1804,7 +1890,7 @@ export async function processIssue(
});
}
recordSessionIssue(session, issueNum, { status: 'failure', stage: 'implement', failureReason: 'permanent' });
}
stepsCompleted.push('implement');
@@ -1926,6 +2012,10 @@ export async function processIssue(
traceOutput(session, issueNum,
fix-${attempt}, fixResult.output);stepCosts.push(buildStepCost('test_fix', issueNum, fixResult, config));
recordStageTelemetry(session, issueNum, 'test_fix', fixResult, config, fixCtx);
@@ -2057,6 +2147,11 @@ export async function processIssue(
stepCosts.push(buildStepCost('review', issueNum, reviewResult, config));
recordStageTelemetry(session, issueNum, 'review', reviewResult, config, reviewCtx);
@@ -2112,6 +2207,11 @@ export async function processIssue(
stepCosts.push(buildStepCost('review', issueNum, reviewFixResult, config));
recordStageTelemetry(session, issueNum, 'review_fix', reviewFixResult, config, reviewFixCtx);
@@ -2226,6 +2326,12 @@ export async function processIssue(
...epicOption,
... (body truncated, see full log)