Files
gstack/scripts/preflight-agent-sdk.ts
T
Garry Tanandcapy-ai[bot] daf661374f v1.93.10.0 wave(docs): #3590 #3591 #3819 #3618 #3824 #3828 (#3847)
* Attribute every paid trial's cost: bind label-named records by testName, price aborted SDK sessions from streamed usage, model-free cases cost $0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: case cost adds streamed sessions to record sums and keeps record cost_known

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: the paid report headline counts ledger-only session cost once

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: a cost-only record never sets the trial model

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: recordE2E writes cost_case_id only for a registered label-named case

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: CHANGELOG for paid-report cost attribution (Unreleased)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: the headline adds only streamed session cost; cache writes without a 5m key

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: how the paid report prices a trial and what stays cost-unknown

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Price PTY sessions from their transcripts: pin a session id, sum the session's and its subagents' usage into the ledger's streamed_cost_usd

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: transcript pricing never throws from a PTY ledger writer

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: pin transcript-cost.ts as non-behavioral at HARNESS_VERSION 6

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: recognise --session-id= and --resume= forms

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: CHANGELOG for PTY transcript pricing (Unreleased)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: PTY sessions priced from their transcripts

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: price PTY transcripts without adding a launch argument; native AUQ capture opts out

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: harness notes and CHANGELOG match the transcript claim

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(flaky): gstack-flaky run reruns a failed JUnit test once and ledgers the flake (GSTA-158)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(flaky): stream output, keep unreported runner failures red, fail-closed duplicates and nonzero reruns

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

gstack-flaky never greens an incomplete run: a targeted rerun needs the
runner's summary proving the first run complete, otherwise (and always for
generic runners) the rerun repeats the whole command. A rerun clears a test
only when it exits 0 and the test passes once on its first attempt. Killed
runners exit 3 (FLAKY_COMMAND_KILLED) and signals are forwarded. The first
report is restored at --junit with reruns kept beside it; ids are redacted at
every output; the ledger gets its own GSTACK_FLAKE_LEDGER_JUNIT; fixtures are
registered in FREE_FIXTURES.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: latch cancellation and check whole-command rerun reports

A cancel forwarded to a runner that traps the signal now counts as killed for
every later attempt, so a cancelled job is never rerun or greened. A
whole-command rerun clears nothing when its report has a failed test, misses a
first-run test, or its runner counted failures outside the report.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: whole-command rerun when coverage or runner-flaky tests can fail the run

A coverage threshold or a fail-on-flaky policy fails a run for a reason a
narrowed rerun cannot reproduce, so either forces a whole-command rerun.
--junit refuses an existing file that is not a JUnit report instead of
deleting it.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(flaky): rerun the whole command by default; targeted rerun is opt-in

A rerun counts only on the runner's own full green verdict: exit 0, no failed
or missing test, nothing counted outside the report. That covers every way a
run can fail, which a narrowed rerun cannot (a coverage threshold with an lcov
reporter, a fail-on-flaky policy, a test never reached). --rerun-scope test
reruns the failed test alone, with its preconditions documented and the
complete-run checks kept as a safety net. An unreadable existing --junit file
is a usage error, not a crash.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: gstack-flaky CHANGELOG entry and standalone-binaries row (GSTA-158)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(flaky): quarantine registry for repeat flakes (GSTA-158 P2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(flaky): bound renewal dates; match non-ASCII paths in check --base

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(flaky): reword the FLAKY_QUARANTINE_BASE hint so the egress scanner does not read it as a network op

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(agent-sdk): every SDK caller passes the claude_code preset or names why not

The SDK's default system prompt is a bare stub that drops the line telling the
model to run /skill through the Skill tool. runAgentSdkTest now refuses a
missing, empty or bare-string prompt; non-preset callers name a reason; a free
tripwire scans test/ and scripts/ for the same mistake.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: record the SDK prompt guard as a non-behavioral harness change and baseline its tripwire

harness-version: runAgentSdkTest validates systemPrompt before starting; every
live caller already passed the claude_code preset, so no verdict can change.
test-of-test ratchet: the tripwire's subject is the eval harness by design.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: name the preset literal callers pass and the unnamed-prompt refusal

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(eval-plan): frontier models under test (R54)

Haiku and Sonnet are not models under test. dry-run refuses a plan whose every
arm is Haiku or Sonnet unless --tier-question names what it asks about that
tier, warns on a mix, and re-costs the plan on Opus 5.5 and GPT-6.1 Sol.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(eval-plan): trim R54 skill prose and raise the eval-plan context ceiling

The R54 paragraph and CHECK: frontier bullet were cut to the minimum; the
exemptions and verdict rule stay in docs/TESTING_INTERNALS.md. The remaining
growth is the feature, so only the eval-plan eager ceiling moves (4887 -> 5458,
the capture helper's value); other skills' ceilings are left as they were.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(eval-plan): pilot start keeps the R54 refusal; --tier-question rejects a flag

Review pass 1 (full depth) found the frontier refusal could be skipped by
starting a pilot on Haiku or Sonnet arms behind a dry run made on other arms,
or behind a dry-run.json written before the check existed. startPilot now
refuses a pilot whose every arm is Haiku or Sonnet unless its dry run recorded
a tier question. dry-run --tier-question no longer takes the next flag as its
answer. The testing doc now says the frontier re-cost is printed for
comparison with the brief cap, not enforced.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(support-tiers): Windows CI in the present tense now that windows-smoke is required (#3806)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: empty commit for a clean check set at the same tree (cancelled orphan free-suite check at 97487df)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(skills): /eval-plan entry names the R54 frontier check

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-02 17:44:00 +00:00

163 lines
5.7 KiB
TypeScript

/**
* Preflight for the overlay efficacy harness.
*
* Confirms, before any paid eval runs:
* 1. `@anthropic-ai/claude-agent-sdk` loads and `query()` is the expected shape.
* 2. `claude-opus-4-7` is a live API model ID (not a Claude Code alias).
* 3. The SDK event stream contains the types we assume (system init, assistant,
* result) with the fields we destructure.
* 4. `scripts/resolvers/model-overlay.ts` resolves `{{INHERIT:claude}}` against
* `opus-4-7.md` with no unresolved inheritance directives.
* 5. A local `claude` binary exists at `which claude` so binary pinning is possible.
*
* Run: bun run scripts/preflight-agent-sdk.ts [--live]
*
* Checks 1-2 and 4-5 are free. Check 3 (the live SDK query, ~15 tokens of API
* spend) runs only with --live. Exit 0 on success, 1 on any failed check,
* 2 on a bad argument.
*/
import '../lib/conductor-env-shim';
import { query, type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
import { readOverlay } from './resolvers/model-overlay';
import { resolveClaudeBinary } from '../lib/claude-bin';
import { sdkSystemPrompt } from '../test/helpers/sdk-system-prompt';
const USAGE = `Usage: bun run scripts/preflight-agent-sdk.ts [--live]
Free checks: overlay resolution and local claude binary pinning.
--live also runs one end-to-end SDK query against claude-opus-4-7
(~15 tokens of API spend; needs ANTHROPIC_API_KEY).`;
function parsePreflightArgs(argv: string[]): { help: boolean; live: boolean } {
const parsed = { help: false, live: false };
for (const arg of argv) {
if (arg === '--help' || arg === '-h') parsed.help = true;
else if (arg === '--live') parsed.live = true;
else throw new Error(`unknown argument "${arg}"`);
}
return parsed;
}
async function main() {
let args: { help: boolean; live: boolean };
try {
args = parsePreflightArgs(process.argv.slice(2));
} catch (err) {
console.error(`preflight-agent-sdk: ${(err as Error).message}\n\n${USAGE}`);
process.exit(2);
}
if (args.help) {
console.log(USAGE);
return;
}
const failures: string[] = [];
const pass = (msg: string) => console.log(` ok ${msg}`);
const fail = (msg: string) => {
console.log(` FAIL ${msg}`);
failures.push(msg);
};
// 1. Overlay resolver
console.log('1. Overlay resolver');
const resolved = readOverlay('opus-4-7');
if (!resolved) {
fail("readOverlay('opus-4-7') returned empty");
} else {
pass(`resolved overlay length: ${resolved.length} chars`);
if (resolved.includes('{{INHERIT:')) {
fail('resolved overlay still contains {{INHERIT:...}} directive');
} else {
pass('no unresolved INHERIT directives');
}
}
// 2. Local claude binary exists
console.log('\n2. Binary pinning');
let claudePath: string | null = resolveClaudeBinary();
if (claudePath) {
pass(`local claude binary: ${claudePath}`);
} else {
fail('`Bun.which("claude")` failed — cannot pin binary (set GSTACK_CLAUDE_BIN to override)');
}
// 3. SDK query end-to-end
console.log('\n3. SDK query end-to-end');
if (!args.live) {
console.log(' skip live query not requested — rerun with --live to spend ~15 tokens on it');
} else if (!process.env.ANTHROPIC_API_KEY) {
console.log(' skip ANTHROPIC_API_KEY not set — cannot test live query');
} else {
try {
const events: SDKMessage[] = [];
const q = query({
prompt: 'say pong',
options: {
model: 'claude-opus-4-7',
...sdkSystemPrompt({ type: 'sdk-default', reason: 'connectivity probe with no tools: checks the event stream shape, not skill behavior' }),
tools: [],
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
settingSources: [],
maxTurns: 1,
pathToClaudeCodeExecutable: claudePath ?? undefined,
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
},
});
for await (const ev of q) events.push(ev);
pass(`received ${events.length} events`);
const init = events.find(
(e) => e.type === 'system' && (e as { subtype?: string }).subtype === 'init',
) as { claude_code_version?: string; model?: string } | undefined;
if (!init) {
fail('no system/init event received');
} else {
pass(`system init: claude_code_version=${init.claude_code_version}, model=${init.model}`);
}
const assistantEvents = events.filter((e) => e.type === 'assistant');
if (assistantEvents.length === 0) {
fail('no assistant events received — model ID may be rejected');
} else {
pass(`received ${assistantEvents.length} assistant event(s)`);
const first = assistantEvents[0] as { message?: { content?: unknown[] } };
const content = first.message?.content;
if (!Array.isArray(content)) {
fail('first assistant event has no content[] array');
} else {
pass(`first assistant content[] has ${content.length} block(s)`);
}
}
const result = events.find((e) => e.type === 'result') as
| { subtype?: string; total_cost_usd?: number; num_turns?: number }
| undefined;
if (!result) {
fail('no result event received');
} else {
pass(
`result: subtype=${result.subtype}, cost=$${result.total_cost_usd?.toFixed(4)}, turns=${result.num_turns}`,
);
}
} catch (err) {
fail(`SDK query threw: ${err instanceof Error ? err.message : String(err)}`);
}
}
console.log();
if (failures.length > 0) {
console.log(`PREFLIGHT FAILED: ${failures.length} check(s) failed`);
process.exit(1);
}
console.log('PREFLIGHT OK');
}
if (import.meta.main) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}