fix(flaky): the PTY floor runner ends on an idle plan-mode turn and reports an API error as api_error, not observer_timeout

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-29 14:10:08 +00:00
1 parent d6fa0d8cc3
commit 4a5d72dc3d
5 files changed
+53 -6

No files matched your search

+9 -1
View File
@@ -1,5 +1,5 @@
{
"provenance": "terminal.visible.log captures (session.visibleSince(command mark), byte-exact, ESC/CR residue kept) from paid-slice-5-a1 artifacts of test/skill-e2e-plan-mode-no-op.test.ts.",
"provenance": "terminal.visible.log captures (session.visibleSince(command mark), byte-exact, ESC/CR residue kept); each entry names its run, head and test. The first three are paid-slice-5-a1 artifacts of test/skill-e2e-plan-mode-no-op.test.ts; planModeApiErrorIdle is paid-slice-9-a1 of test/skill-e2e-plan-devex-finding-floor.test.ts.",
"scopePendingIdle": {
"runId": 37082906717,
"headSha": "988e985d",
@@ -23,5 +23,13 @@
"recordedPassed": true,
"note": "Whole capture. The pending line followed by the full prose menu, then the idle prompt; classified 'asked'.",
"visible": "\u000f\u001b[?2004h\u001b[?1000h\u001b[?1002h\u001b[?1003h\u001b[?1006h\u001b[?25l\n❯ /plan-eng-review \n*Computing… \n❯ \nesc to interrupt · ← for agents\u001b[?25h\u001b[?25l\n /plan-eng-review\u001b[?25h\u001b[?25l\n✢\u001b[?25h\u001b[?25l\n·\u001b[?25h\u001b[?25l\n…\u001b[?25h\u001b[?25l\n ● high · /effort\u001b[?25h\u001b[?25l\n✢g\u001b[?25h\u001b[?25l\n*n\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\ni…\u001b[?25h\u001b[?25l\n✻\u001b[?25h\u001b[?25l\n✽tg\u001b[?25h\u001b[?25l\nun\u001b[?25h\u001b[?25l\n✻pi\u001b[?25h\u001b[?25l\nGallop\u001b[?25h\u001b[?25l\n(1s · thinking)\u001b[?25h\u001b[?25l\n✶lpthinking\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✢ao2\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n·Glthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nlthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✢athinking\u001b[?25h\u001b[?25l\n*Gthinking\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n✻3\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✽thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✻thinking\u001b[?25h\u001b[?25l\n✶thinking\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✢4\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n·thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✢thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n*thinking\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n✻5\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✽thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✻thinking\u001b[?25h\u001b[?25l\n✶thinking\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✢6thinking\u001b[?25h\u001b[?25l\n·thinking\u001b[?25h\u001b[?25l\n…thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✢gthinking\u001b[?25h\u001b[?25l\n*nthinking\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n✻i…7thinking\u001b[?25h\u001b[?25l\n✽pgthinking\u001b[?25h\u001b[?25l\nonthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✻lithinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✶lp\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✢ao8thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n·Glthinking\u001b[?25h\u001b[?25l\nlthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✢athinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n*G\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n✻9thinking\u001b[?25h\u001b[?25l\n✽thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✻thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✢10s · thinking)\u001b[?25h\u001b[?25l\n·thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✢thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n✻1thinking\u001b[?25h\u001b[?25l\n✽thinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\nthinking\u001b[?25h\u001b[?25l\n✻thinking\u001b[?25h\u001b[?25l\n✶still thinking)\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n✢2still thinking\u001b[?25h\u001b[?25l\n·still thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\n✢still thinking\u001b[?25h\u001b[?25l\n*still thinking\u001b[?25h\u001b[?25l\n*Galloping… still thinking\u001b[?25h\u001b[?25l\n✶3\u001b[?25h\u001b[?25l\n✻still thinking\u001b[?25h\u001b[?25l\n✽still thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\n✻still thinking\u001b[?25h\u001b[?25l\n✶still thinking\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n4\u001b[?25h\u001b[?25l\n✢Galloping… still thinking\u001b[?25h\u001b[?25l\n·still thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\n✢still thinking\u001b[?25h\u001b[?25l\n*still thinking\u001b[?25h\u001b[?25l\n✶\u001b[?25h\u001b[?25l\n5\u001b[?25h\u001b[?25l\n✻still thinking\u001b[?25h\u001b[?25l\n✽still thinking\u001b[?25h\u001b[?25l\n✽Galloping… still thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\nstill thinking\u001b[?25h\u001b[?25l\n✻still thinking\u001b[?25h\u001b[?25l\n✶still thinking\u001b[?25h\u001b[?25l\n*\u001b[?25h\u001b[?25l\n*Galloping… ↓ 1.1k tokenLine truncated
},
"planModeApiErrorIdle": {
"runId": 40851840940,
"headSha": "6c67e023",
"testName": "seeded forcing finding causes the agent to fire at least one AskUserQuestion",
"recordedPassed": false,
"note": "Last 4000 characters. Plan mode. After the D1 setup reply the provider dropped the response mid-turn; the turn ended at the idle plan-mode prompt and the floor runner waited out its 600000ms budget (outcome=timeout, ledger end observer_timeout).",
"visible": "escing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rCoalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻Coalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✽Coalescing…1m 0s· ↓ 3.8k tokens)\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rCoalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rCoalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rCoalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻Coalescing…1\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶Coalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢Coalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rCoalescing…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r · thinking)\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r·thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢2thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶g\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✽nthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\ri…thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻cg3thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*sn\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rei\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r·lcthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rasthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢oe4thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*Clthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\ra\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✽othinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rCthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻5thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r·thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢6thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✽thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻7thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r·thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r8thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻…\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✽gthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rnthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻i…thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r9\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶cgthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢snthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r·eithinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rlcthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢asthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r10s · ↓ 3.8k tokens · thinking)\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*oethinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻Clthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✽athinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rothinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✻C1thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rthinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✶\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r*\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rstill thinking)\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r·still thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rstill thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rstill thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r✢2still thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\rstill thinking\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r●I pulled comparison data via WebSearch since Aside isn'tinstalled,treatingtheresultsasreportedindustryclaims\r rather than hard measurements. I built a table comparing Stripe, typical token-auth RESTAPIs,andthisplan's\ronboardingflow—butsincenonesharethesamestart/endpoints,I'mflaggingthatthiscomparesDXchoices,notactual\r times.\r\r✢ Coalescing… (1m 12s · ↓ 4.7k tokens · thought for 11s)\r ⎿  Tip: Use /btw to ask aquik side questin withot interrupting Claude's cLine truncated
}
}
@@ -209,6 +209,19 @@ describe('runPlanSkillFloorCheck through the fake driver', () => {
expect(obs.summary).toBe('no qualifying finding question within 30000ms');
});
test('the 40851840940 provider error at an idle plan-mode prompt ends the floor check with its panel', async () => {
const plan: string = JSON.parse(fs.readFileSync(new URL('../fixtures/pty-idle-turn-end.json', import.meta.url), 'utf8')).planModeApiErrorIdle.visible;
const { obs } = await floor([{ onInput: '/plan-devex-review PLAN.md\r', screen: plan, effect: delivered }]);
expect(obs.outcome).toBe('api_error');
expect(obs.auqObserved).toBe(false);
expect(obs.elapsedMs).toBeLessThan(30_000);
expect(obs.summary).toBe('the CLI turn ended at an idle prompt (✻Brewed for 1m 13s · done 9:51 AM) after its public error panel: ' +
'●API Error: Connection lost mid-response. The response above may be incomplete.');
const running = plan.replace('⏸planmodeon (shift+tabtocycle)·←foragents', '⏸planmodeon (shift+tabtocycle)·esctointerrupt·←foragents');
const live = await floor([{ onInput: '/plan-devex-review PLAN.md\r', screen: running, effect: delivered }]);
expect(live.obs).toMatchObject({ outcome: 'timeout', summary: 'no qualifying finding question within 30000ms' });
});
test('permission prompt: an unowned permission is neither granted nor counted', async () => {
const { obs, fake } = await floor([
{ onInput: '/plan-devex-review PLAN.md\r', screen: PERMISSION, effect: delivered },
@@ -13,6 +13,7 @@ import {
classifyPlanCountFrame,
idleTurnEnd,
} from './claude-pty-runner';
import { screenPanelEnd } from './pty/screen';
describe('saved preference annotation', () => {
test('recognizes the explicit preference attribution from the timed-out CEO capture', () => {
@@ -252,6 +253,25 @@ describe('idle turn end (captured CI screens)', () => {
expect(idleTurnEnd(scope + '\r' + question)).toBeNull();
});
test('the 40851840940 plan-mode idle footer ends the turn and names its provider error for the ledger', () => {
const plan: string = captures.planModeApiErrorIdle.visible;
const panel = '●API Error: Connection lost mid-response. The response above may be incomplete.';
expect(idleTurnEnd(plan)).toEqual({ done: '✻Brewed for 1m 13s · done 9:51 AM', apiError: panel });
expect(screenPanelEnd(plan)).toEqual({ end: 'api_error', evidence: panel });
expect(idleTurnEnd(plan.replace('⏸planmodeon (shift+tabtocycle)·←foragents', '⏸ plan mode on (shift+tab to cycle) · ← for agents')))
.toMatchObject({ apiError: panel });
});
test('negative controls: the running plan-mode footer and extra plan-mode footer text are not idle', () => {
const plan: string = captures.planModeApiErrorIdle.visible;
const footer = '⏸planmodeon (shift+tabtocycle)·←foragents';
expect(plan.split(footer)).toHaveLength(2);
expect(idleTurnEnd(plan.replace(footer, '⏸planmodeon (shift+tabtocycle)·esctointerrupt·←foragents'))).toBeNull();
expect(idleTurnEnd(plan.replace(footer, footer + ' · 1 background task'))).toBeNull();
expect(idleTurnEnd(plan.replace(footer, '⏸planmodeon (shift+tabtocycle)'))).toBeNull();
expect(screenPanelEnd(plan.replace(footer, '⏸planmodeon (shift+tabtocycle)·esctointerrupt·←foragents'))).toBeNull();
});
test('an API Error mention in prose or in an earlier turn is not this turn\'s error panel', () => {
expect(idleTurnEnd('The docs mention API Error: as an example.\r✻ Worked for 3s · done 1:00 PM\r❯ \r← for agents')).toEqual({ done: '✻ Worked for 3s · done 1:00 PM' });
const earlier = api.replace(/✻ Sautéedfor1m 20s · done11:34PM[\s\S]*$/, '✻ Sautéedfor1m 20s · done11:34PM\r❯ /plan-design-review\r●Done.\r✻ Worked for 2s · done 11:35PM\r❯ \r← for agents');
+7 -2
View File
@@ -18,7 +18,7 @@ import { capturePlanCountQuestion, createPlanCountPermissionGuard, matchesNative
import { resolveClaudeBinary } from '../binary';
import { SANCTIONED_WRITE_SUBSTRINGS, isProseAUQVisible, planCountSubmissionInput } from '../classify';
import type { ClaudePtySession } from '../launch';
import { isNumberedOptionListVisible, isPermissionDialogVisible, isPlanReadyVisible, isRejectedSlashCommand, stripPtyResidue } from '../screen';
import { idleTurnEnd, isNumberedOptionListVisible, isPermissionDialogVisible, isPlanReadyVisible, isRejectedSlashCommand, stripPtyResidue } from '../screen';
import { realPtyDriver, runPtySession, type PtyDriver, type PtyStep } from '../session';
// ────────────────────────────────────────────────────────────────────────────
@@ -41,7 +41,8 @@ export interface PlanSkillFloorObservation {
| 'exited'
| 'timeout'
| 'assessment_error'
| 'permission_unanswered';
| 'permission_unanswered'
| 'api_error';
summary: string;
/** Public current viewport for an accepted finding; terminal tail otherwise. */
evidence: string;
@@ -351,6 +352,10 @@ async function floorTick(run: FloorRun, session: ClaudePtySession): Promise<PtyS
if (isRejectedSlashCommand(visible, opts.slashCommand)) return finish({ auqObserved: false, outcome: 'exited',
summary: `claude rejected ${opts.slashCommand} as unknown command`,
evidence: visible.slice(-3000), elapsedMs: driver.now() - run.startedAt });
const idle = idleTurnEnd(visible);
if (idle?.apiError) return finish({ auqObserved: false, outcome: 'api_error',
summary: `the CLI turn ended at an idle prompt (${idle.done}) after its public error panel: ${idle.apiError}`,
evidence: visible.slice(-3000), elapsedMs: driver.now() - run.startedAt });
if (run.targetDelivery.status !== 'ready') {
run.targetDelivery = readPlanFloorTarget(session.hermeticConfigDir, fixture.cwd,
+4 -3
View File
@@ -424,7 +424,7 @@ export function idlePanelEnd(visible: string): { end: 'refusal' | 'api_error'; e
}
/** The last idle turn's error panel, else the CLI's rejected-credentials line anywhere on screen: after one the
* CLI can sit at a footer the idle check does not know (plan mode, manual mode), and no turn can run. */
* CLI can sit at a footer the idle check does not know (manual mode), and no turn can run. */
export function screenPanelEnd(visible: string): { end: 'refusal' | 'api_error'; evidence: string } | null {
const credential = visible.split(/\r\n?|\n/).reverse().find(line => CREDENTIAL_REJECTION.test(line))?.trim();
return idlePanelEnd(visible) ?? (credential ? { end: 'api_error', evidence: credential } : null);
@@ -432,8 +432,9 @@ export function screenPanelEnd(visible: string): { end: 'refusal' | 'api_error';
/** The CLI's end-of-turn line, e.g. "✻ Cooked for 24s · done 12:46 AM" (spaces may collapse in the PTY text). */
const TURN_DONE_RE = /✻[ \t]*\p{L}[\p{L}'’-]*[ \t]*for[ \t]*(?=\d)(?:\d+h[ \t]*)?(?:\d+m[ \t]*)?(?:\d+s)?[ \t]*·[ \t]*done[^\n]*/gu;
/** An empty input prompt and its known idle footers, nothing else. */
const IDLE_PROMPT_RE = /^[\s─]*❯[\s─]*(?:(?:←[ \t]*for[ \t]*agents|\?[ \t]*for[ \t]*shortcuts)[\s─]*)*$/;
/** An empty input prompt and its known idle footers, nothing else. The plan-mode footer is idle only
* without its running "esc to interrupt" segment (run 40851840940, plan-devex-finding-floor). */
const IDLE_PROMPT_RE = /^[\s─]*❯[\s─]*(?:(?:(?:⏸[ \t]*plan[ \t]*mode[ \t]*on[ \t]*\(shift\+tab[ \t]*to[ \t]*cycle\)[ \t]*·[ \t]*)?←[ \t]*for[ \t]*agents|\?[ \t]*for[ \t]*shortcuts)[\s─]*)*$/;
/**
* The CLI ended its turn and sits at an empty prompt: the last end-of-turn