Merge origin/main v1.91.18.0 (#3023) into the v1.91.19.0 wave

Resolutions:
- design-doc discovery: main's bin/gstack-design-doc-find replaces the inline
  block; it keeps #2839 (a root DESIGN.md is the design system, never the
  design doc; only docs/designs/ competes on freshness). Its unit test and
  the F2 harness (now installs the helper) follow.
- design-shotgun: main's $D variants batch, staged under ${TMPDIR:-/tmp}.
- memory ingest: main's scoped consent (scope counts, purge of staged pages
  outside the scope, zero-sessions notice) on the wave's locked state flow;
  the purge runs before the state loads, under the lock.
- setup / render library: --model overlay plus --disabled-skills on every
  Claude render, including main's plain overlay render.
- codex template and constants: the wave's sandbox preflight and unverified
  mode; main's issue-number removals.
- docsync interface: ls-tree (main) and check-ignore (wave) are both reads.
- census pins re-measured on the merged tree: 114 paid files; gate
  behavior census 44 files / 76 shards (periodic-only files skip the gate,
  now including two safety-rule evals); periodic floor 35,742 and release
  sum 58,097 (gate-only files no longer count toward periodic).
- parity caps at measured merged sizes: ship 1.4271, review 1.1981, qa 1.1099.
- generated output regenerated (gen:skill-docs --host all, llms.txt,
  context budget, goldens).
This commit is contained in:
garrytan committed 2026-10-04 02:10:47 +00:00
commit 8e2beec2de
412 files changed
+24774 -1012

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
/**
* W2 calibration request configurations, keyed by distinct request (prompt
* builder, model, max_tokens, stream, effort, thresholds), not by test case.
* Each config builds prompts through the same builder the eval sends; "old" is
* today's request and "new" adds only the JSON schema (comparison 1).
*/
import {
ARM_JUDGE_MODEL, ARM_JUDGE_SCHEMA, buildArmJudgePrompt, parseArmJudgeResponse,
buildQaWorkflowJudgePrompt, buildQaHealthRubricJudgePrompt, buildQaAntiRefusalJudgePrompt,
buildCrossSkillConsistencyJudgePrompt, buildVoiceDirectiveJudgePrompt,
JUDGE_SCORE_SCHEMA, QA_ANTI_REFUSAL_JUDGE_SCHEMA, CROSS_SKILL_CONSISTENCY_JUDGE_SCHEMA,
VOICE_DIRECTIVE_JUDGE_SCHEMA, VOICE_DIRECTIVE_DIMENSIONS, JUDGE_SCORE_DIMENSIONS, DEFAULT_JUDGE_MAX_TOKENS,
judgePanelMean, judgePanelMajority,
} from '../test/helpers/llm-judge';
import { buildWorkflowJudgePrompt, WORKFLOW_JUDGE_RESPONSE_SCHEMA } from '../test/helpers/workflow-judge-input';
import { CLAUDE_FRONTIER_EVAL_MODEL } from '../lib/eval-model';
import type { CalibrationConfig, Verdict } from './judge-calibration';
type Sample = Record<string, unknown>;
/** What production rejects: judgePanelMean needs finite numbers, judgePanelMajority needs booleans. */
function requireFields(value: unknown, numbers: readonly string[], booleans: readonly string[] = []): void {
const sample = (value ?? {}) as Sample;
for (const key of numbers) {
if (typeof sample[key] !== 'number' || !Number.isFinite(sample[key])) throw new Error(`non-numeric ${key}: ${JSON.stringify(sample[key])}`);
}
for (const key of booleans) {
if (typeof sample[key] !== 'boolean') throw new Error(`non-boolean ${key}: ${JSON.stringify(sample[key])}`);
}
}
const meets = (mean: Record<string, number>, floors: Record<string, number>): Verdict =>
Object.entries(floors).every(([key, floor]) => mean[key]! >= floor) ? 'pass' : 'fail';
const QA_FLOORS = { clarity: 3, completeness: 3, actionability: 4 };
const scoreJudge = (floors: Record<string, number>) => ({
validate: (value: unknown) => requireFields(value, JUDGE_SCORE_DIMENSIONS),
verdict: (samples: unknown[]) => meets(judgePanelMean<string>(samples as Sample[], JUDGE_SCORE_DIMENSIONS), floors),
});
const workflow = (id: string, priority: number, cases: string[], extra: { agentCapability?: 'frontier'; model?: string; floors?: Record<string, number> } = {}): CalibrationConfig => ({
id, priority, cases,
model: extra.model ?? CLAUDE_FRONTIER_EVAL_MODEL,
build: inputs => buildWorkflowJudgePrompt({ judgeContext: inputs.judgeContext!, judgeGoal: inputs.judgeGoal!, agentCapability: extra.agentCapability },
{ files: [], text: inputs.bundle! }),
oldOptions: { max_tokens: DEFAULT_JUDGE_MAX_TOKENS },
newOptions: { max_tokens: DEFAULT_JUDGE_MAX_TOKENS, jsonSchema: WORKFLOW_JUDGE_RESPONSE_SCHEMA },
...scoreJudge(extra.floors ?? QA_FLOORS),
estimatedOutputTokens: 1200,
});
export const CALIBRATION_CONFIGS: CalibrationConfig[] = [
{
id: 'arm', priority: 1, cases: ['arm-benchmark (skill-e2e-arm-benchmark.test.ts)'], model: ARM_JUDGE_MODEL,
build: inputs => buildArmJudgePrompt(inputs.task!.trimEnd(), inputs.diff!),
oldOptions: {},
newOptions: { jsonSchema: ARM_JUDGE_SCHEMA },
validate: value => { parseArmJudgeResponse(value); },
verdict: samples => {
const scores = samples.map(sample => parseArmJudgeResponse(sample).over_engineering).sort((a, b) => a - b);
return scores[Math.floor(scores.length / 2)]! <= 1 ? 'pass' : 'fail';
},
estimatedOutputTokens: 300,
},
{
id: 'qa-workflow', priority: 2, cases: ['qa/SKILL.md workflow'], model: CLAUDE_FRONTIER_EVAL_MODEL,
build: inputs => buildQaWorkflowJudgePrompt(inputs.section!),
oldOptions: {}, newOptions: { jsonSchema: JUDGE_SCORE_SCHEMA },
...scoreJudge(QA_FLOORS), estimatedOutputTokens: 1200,
},
{
id: 'qa-health-rubric', priority: 3, cases: ['qa/SKILL.md health rubric'], model: CLAUDE_FRONTIER_EVAL_MODEL,
build: inputs => buildQaHealthRubricJudgePrompt(inputs.section!),
oldOptions: {}, newOptions: { jsonSchema: JUDGE_SCORE_SCHEMA },
...scoreJudge(QA_FLOORS), estimatedOutputTokens: 1200,
},
{
id: 'qa-anti-refusal', priority: 4, cases: ['qa/SKILL.md anti-refusal'], model: CLAUDE_FRONTIER_EVAL_MODEL,
build: inputs => buildQaAntiRefusalJudgePrompt(inputs.diffAwareSection!, inputs.rulesSection!),
oldOptions: {}, newOptions: { jsonSchema: QA_ANTI_REFUSAL_JUDGE_SCHEMA },
validate: value => requireFields(value, ['confidence'], ['would_browse']),
verdict: samples => judgePanelMajority<string>(samples as Sample[], 'would_browse')
&& judgePanelMean<string>(samples as Sample[], ['confidence']).confidence >= 4 ? 'pass' : 'fail',
estimatedOutputTokens: 1200,
},
{
id: 'cross-skill', priority: 5, cases: ['cross-skill greptile consistency'], model: CLAUDE_FRONTIER_EVAL_MODEL,
build: inputs => buildCrossSkillConsistencyJudgePrompt(inputs.collected!),
oldOptions: {}, newOptions: { jsonSchema: CROSS_SKILL_CONSISTENCY_JUDGE_SCHEMA },
validate: value => requireFields(value, ['score'], ['consistent']),
verdict: samples => judgePanelMajority<string>(samples as Sample[], 'consistent')
&& judgePanelMean<string>(samples as Sample[], ['score']).score >= 4 ? 'pass' : 'fail',
estimatedOutputTokens: 1200,
},
{
id: 'voice', priority: 6, cases: ['voice directive tone'], model: CLAUDE_FRONTIER_EVAL_MODEL,
build: inputs => buildVoiceDirectiveJudgePrompt(inputs.voiceSection!),
oldOptions: {}, newOptions: { jsonSchema: VOICE_DIRECTIVE_JUDGE_SCHEMA },
validate: value => requireFields(value, VOICE_DIRECTIVE_DIMENSIONS),
verdict: samples => meets(judgePanelMean<string>(samples as Sample[], VOICE_DIRECTIVE_DIMENSIONS),
Object.fromEntries(VOICE_DIRECTIVE_DIMENSIONS.map(key => [key, 4]))),
estimatedOutputTokens: 1200,
},
workflow('workflow-default', 7, ['document-release/SKILL.md workflow', 'plan-ceo-review/SKILL.md modes', 'plan-eng-review/SKILL.md sections',
'plan-design-review/SKILL.md passes', 'design-review/SKILL.md fix loop', 'design-consultation/SKILL.md research',
'land-and-deploy/SKILL.md workflow', 'canary/SKILL.md monitoring loop', 'benchmark/SKILL.md perf collection',
'setup-deploy/SKILL.md platform setup', 'sync-gbrain/SKILL.md read-only readiness', 'retro/SKILL.md instructions',
'qa-only/SKILL.md workflow', 'gstack-upgrade/SKILL.md upgrade flow']),
];
+379
View File
@@ -0,0 +1,379 @@
#!/usr/bin/env bun
/**
* W2 judge calibration harness (comparison 1: schema transport, prose unchanged).
*
* For every corpus item of a request configuration it draws three independent
* 3-sample panels: two with the current request ("old-a", "old-b") and one with
* the schema-transport request ("new"). Each sample is one direct callJudge
* call. Calibration never goes through judgePanel or the workflow passing-panel
* cache: a replayed panel would make old-vs-old flips read zero.
*
* Each sample records a verdict-bearing value or an error kind (refusal,
* truncation, parse, schema, transport), its latency and its priced usage.
* Errors never count as verdicts; flip rates use items where both panels
* completed (all three samples valid).
*
* Usage:
* bun run scripts/judge-calibration.ts manifest [--budget 90]
* bun run scripts/judge-calibration.ts run --config <id> --budget <usd> [--concurrency 6]
* bun run scripts/judge-calibration.ts resume --config <id> --budget <usd> (only never-dispatched planned calls)
* bun run scripts/judge-calibration.ts analyze --config <id>
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { createHash } from 'node:crypto';
import Anthropic from '@anthropic-ai/sdk';
import { callJudge, JudgeRefusalError, type CallJudgeOptions, type JudgeResponseMeta } from '../test/helpers/llm-judge';
import { JUDGE_MS } from '../test/helpers/eval-budgets';
import { estimateCostUsd } from '../test/helpers/pricing';
export const PANEL_SAMPLES = 3;
export const PHASES = ['old-a', 'old-b', 'new'] as const;
export type Phase = typeof PHASES[number];
export type Verdict = 'pass' | 'fail';
export type ErrorKind = 'refusal' | 'truncation' | 'parse' | 'schema' | 'transport';
export const LATENCY_CEILING_MS = 0.8 * JUDGE_MS;
export const CORPUS_ROOT = path.join(import.meta.dir, '..', 'test', 'fixtures', 'judge-calibration');
export interface CorpusItem {
id: string;
category: 'pass' | 'fail' | 'adjacent';
expected: Verdict;
split: 'dev' | 'heldout';
rationale: string;
inputs: Record<string, string>;
}
export interface CalibrationConfig {
id: string;
/** Dispatch order when the priced manifest exceeds the budget (armJudge, inline judges, workflow by case count). */
priority: number;
/** Eval cases that send this request configuration. */
cases: string[];
model: string;
build(inputs: Record<string, string>): string;
/** Today's request options. */
oldOptions: CallJudgeOptions;
/** Comparison (1): today's options plus the JSON schema; prose unchanged. */
newOptions: CallJudgeOptions;
/** Throws when production would reject the sample's shape. */
validate(value: unknown): void;
verdict(samples: unknown[]): Verdict;
/** Manifest pricing estimate per call (output includes adaptive thinking). */
estimatedOutputTokens: number;
}
export interface SampleRecord {
config: string;
item: string;
split: CorpusItem['split'];
phase: Phase;
sample: number;
status: 'ok' | 'error';
error_kind?: ErrorKind;
error?: string;
value?: unknown;
latency_ms: number;
stop_reason: string | null;
usage: { input_tokens: number; output_tokens: number } | null;
cost_usd: number;
}
export type JudgeCall = (prompt: string, model: string, opts: CallJudgeOptions) => Promise<unknown>;
export function loadCorpus(configId: string, root = CORPUS_ROOT): CorpusItem[] {
const dir = path.join(root, configId);
const corpus = JSON.parse(fs.readFileSync(path.join(dir, 'corpus.json'), 'utf8'));
return corpus.items.map((item: any) => ({
...item,
inputs: Object.fromEntries(Object.entries(item.inputs as Record<string, string | { file: string }>).map(([key, value]) =>
[key, typeof value === 'string' ? value : fs.readFileSync(path.join(dir, value.file), 'utf8')])),
}));
}
export function classifyError(error: unknown): ErrorKind {
if (error instanceof JudgeRefusalError) return 'refusal';
const message = error instanceof Error ? error.message : String(error);
if (/truncated at max_tokens|stop_reason=max_tokens/.test(message)) return 'truncation';
if (error instanceof SyntaxError || /non-JSON|did not complete/.test(message)) return 'parse';
return 'transport';
}
/** The fixed request identity: what must not move between dispatch and analysis. */
export function requestHash(config: CalibrationConfig, phase: Phase): string {
const { onResponse: _ignored, signal: _signal, ...options } = phase === 'new' ? config.newOptions : config.oldOptions;
return createHash('sha256').update(JSON.stringify({ model: config.model, options })).digest('hex');
}
export interface PlannedCall { item: CorpusItem; phase: Phase; sample: number }
/** Every paid call for one configuration, interleaved by a fixed seed so phases share time-of-day drift. */
export function planCalls(items: CorpusItem[], split: CorpusItem['split']): PlannedCall[] {
const calls = items.filter(item => item.split === split)
.flatMap(item => PHASES.flatMap(phase => Array.from({ length: PANEL_SAMPLES }, (_, sample) => ({ item, phase, sample }))));
let seed = 0x5eed;
const random = () => (seed = (seed * 1103515245 + 12345) % 2 ** 31) / 2 ** 31;
for (let i = calls.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[calls[i], calls[j]] = [calls[j]!, calls[i]!];
}
return calls;
}
/** Planned calls never dispatched (a budget or infrastructure stop); completing them is not a resample. */
export function undispatched(calls: PlannedCall[], records: SampleRecord[]): PlannedCall[] {
const done = new Set(records.map(record => `${record.item}/${record.phase}/${record.sample}`));
return calls.filter(call => !done.has(`${call.item.id}/${call.phase}/${call.sample}`));
}
export async function runSample(config: CalibrationConfig, planned: PlannedCall, call: JudgeCall = callJudge as JudgeCall,
clock: () => number = () => performance.now()): Promise<SampleRecord> {
let meta: JudgeResponseMeta | null = null;
const options = { ...(planned.phase === 'new' ? config.newOptions : config.oldOptions), onResponse: (m: JudgeResponseMeta) => { meta = m; } };
const base = { config: config.id, item: planned.item.id, split: planned.item.split, phase: planned.phase, sample: planned.sample };
const started = clock();
let value: unknown;
let failure: { kind: ErrorKind; message: string } | null = null;
try {
value = await call(config.build(planned.item.inputs), config.model, options);
try { config.validate(value); }
catch (error) { failure = { kind: 'schema', message: error instanceof Error ? error.message : String(error) }; }
} catch (error) {
failure = { kind: classifyError(error), message: error instanceof Error ? error.message : String(error) };
}
const latency_ms = Math.round(clock() - started);
const response = meta as JudgeResponseMeta | null;
const usage = response?.usage ?? null;
const cost_usd = usage ? estimateCostUsd({ input: usage.input_tokens, output: usage.output_tokens }, config.model) : 0;
const shared = { latency_ms, stop_reason: response?.stop_reason ?? null, usage, cost_usd };
return failure
? { ...base, status: 'error', error_kind: failure.kind, error: failure.message.slice(0, 500), ...shared }
: { ...base, status: 'ok', value, ...shared };
}
export interface RunOptions {
budgetUsd: number;
concurrency?: number;
call?: JudgeCall;
clock?: () => number;
onRecord?: (record: SampleRecord) => void;
}
/**
* Dispatch planned calls with bounded concurrency. Stops dispatching when the
* actual spend plus one in-flight estimate per open slot would pass the budget,
* or after two consecutive transport failures (infrastructure, not a verdict).
*/
export async function runCalls(config: CalibrationConfig, calls: PlannedCall[], opts: RunOptions): Promise<{ records: SampleRecord[]; stopped: string | null }> {
const records: SampleRecord[] = [];
const perCall = estimateCostUsd({ input: 0, output: config.estimatedOutputTokens }, config.model);
let spent = 0, next = 0, consecutiveTransport = 0, inFlight = 0;
let stopped: string | null = null;
const worker = async () => {
while (!stopped && next < calls.length) {
if (spent + (inFlight + 1) * perCall * 2 > opts.budgetUsd) { stopped = `budget: spent $${spent.toFixed(2)} of $${opts.budgetUsd.toFixed(2)}`; break; }
const planned = calls[next++]!;
inFlight++;
const record = await runSample(config, planned, opts.call, opts.clock);
inFlight--;
spent += record.cost_usd;
consecutiveTransport = record.error_kind === 'transport' ? consecutiveTransport + 1 : 0;
records.push(record);
opts.onRecord?.(record);
if (consecutiveTransport >= 2) stopped = `infrastructure: two consecutive transport failures (${record.error})`;
}
};
await Promise.all(Array.from({ length: Math.max(1, opts.concurrency ?? 6) }, worker));
return { records, stopped };
}
export interface PanelResult { verdict: Verdict | null; errors: number }
export function panels(config: CalibrationConfig, records: SampleRecord[]): Map<string, Record<Phase, PanelResult>> {
const byItem = new Map<string, Record<Phase, PanelResult>>();
for (const item of new Set(records.map(record => record.item))) {
const entry = {} as Record<Phase, PanelResult>;
for (const phase of PHASES) {
const samples = records.filter(record => record.item === item && record.phase === phase);
const ok = samples.filter(record => record.status === 'ok');
entry[phase] = { verdict: ok.length === PANEL_SAMPLES && samples.length === PANEL_SAMPLES ? config.verdict(ok.map(record => record.value)) : null,
errors: samples.length - ok.length };
}
byItem.set(item, entry);
}
return byItem;
}
function p95(values: number[]): number | null {
if (values.length === 0) return null;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.ceil(0.95 * sorted.length) - 1)]!;
}
export interface ComparisonSummary {
items: number;
old_flips: { flips: number; compared: number };
new_flips: { flips: number; compared: number };
agreement: Record<Phase, number>;
false_passes: Record<Phase, string[]>;
new_false_passes: string[];
}
export function compare(config: CalibrationConfig, items: CorpusItem[], records: SampleRecord[]): ComparisonSummary {
const byItem = panels(config, records);
const flips = (a: Phase, b: Phase) => {
const both = items.map(item => byItem.get(item.id)).filter(entry => entry?.[a].verdict && entry[b].verdict);
return { flips: both.filter(entry => entry![a].verdict !== entry![b].verdict).length, compared: both.length };
};
const agreement = Object.fromEntries(PHASES.map(phase =>
[phase, items.filter(item => byItem.get(item.id)?.[phase].verdict === item.expected).length])) as Record<Phase, number>;
const falsePasses = Object.fromEntries(PHASES.map(phase =>
[phase, items.filter(item => item.expected === 'fail' && byItem.get(item.id)?.[phase].verdict === 'pass').map(item => item.id)])) as Record<Phase, string[]>;
return { items: items.length, old_flips: flips('old-a', 'old-b'), new_flips: flips('new', 'old-a'), agreement,
false_passes: falsePasses, new_false_passes: falsePasses.new.filter(id => !falsePasses['old-a'].includes(id)) };
}
export interface CalibrationResult {
config: string;
model: string;
request_hash: Record<'old' | 'new', string>;
complete: boolean;
stopped: string | null;
calls: number;
cost_usd: number;
errors: Record<'old' | 'new', Record<ErrorKind, number> & { samples: number; rate: number }>;
latency_p95_ms: Record<'old' | 'new', number | null>;
all: ComparisonSummary;
heldout: ComparisonSummary;
landing: { lands: boolean; reasons: string[]; strict_no_flip_increase: boolean };
prose_step_triggered: { triggered: boolean; reasons: string[] };
}
export function analyze(config: CalibrationConfig, items: CorpusItem[], records: SampleRecord[], stopped: string | null = null): CalibrationResult {
const prompt = (phase: Phase) => phase === 'new' ? 'new' : 'old';
const errors = Object.fromEntries((['old', 'new'] as const).map(which => {
const samples = records.filter(record => prompt(record.phase) === which);
const counts = Object.fromEntries((['refusal', 'truncation', 'parse', 'schema', 'transport'] as const)
.map(kind => [kind, samples.filter(record => record.error_kind === kind).length])) as Record<ErrorKind, number>;
const errored = samples.filter(record => record.status === 'error').length;
return [which, { ...counts, samples: samples.length, rate: samples.length ? errored / samples.length : 0 }];
})) as CalibrationResult['errors'];
const latency = Object.fromEntries((['old', 'new'] as const).map(which =>
[which, p95(records.filter(record => prompt(record.phase) === which && record.status === 'ok').map(record => record.latency_ms))])) as CalibrationResult['latency_p95_ms'];
const expectedCalls = items.length * PHASES.length * PANEL_SAMPLES;
const complete = records.length === expectedCalls && !stopped;
const all = compare(config, items, records);
const heldout = compare(config, items.filter(item => item.split === 'heldout'), records);
const reasons: string[] = [];
if (!complete) reasons.push(`incomplete: ${records.length}/${expectedCalls} calls${stopped ? ` (${stopped})` : ''}`);
if (all.new_flips.flips > all.old_flips.flips + 1) reasons.push(`new flips ${all.new_flips.flips}/${all.new_flips.compared} > old flips ${all.old_flips.flips}/${all.old_flips.compared} + 1`);
if (all.agreement.new < all.agreement['old-a']) reasons.push(`expected-verdict agreement dropped: new ${all.agreement.new} < old ${all.agreement['old-a']}`);
if (all.new_false_passes.length > 0) reasons.push(`new false passes: ${all.new_false_passes.join(', ')}`);
if (latency.new !== null && latency.new > LATENCY_CEILING_MS) reasons.push(`new p95 latency ${latency.new} ms > ${LATENCY_CEILING_MS} ms (80% of JUDGE_MS)`);
const trigger: string[] = [];
if (records.some(record => record.phase === 'new' && record.status === 'error')) trigger.push('new prompt has errored samples');
if (reasons.length > 0) trigger.push('a landing condition failed');
if (all.old_flips.flips >= 3) trigger.push(`old-vs-old flips ${all.old_flips.flips} >= 3`);
return {
config: config.id, model: config.model,
request_hash: { old: requestHash(config, 'old-a'), new: requestHash(config, 'new') },
complete, stopped, calls: records.length,
cost_usd: +records.reduce((sum, record) => sum + record.cost_usd, 0).toFixed(4),
errors, latency_p95_ms: latency, all, heldout,
landing: { lands: reasons.length === 0, reasons, strict_no_flip_increase: all.new_flips.flips <= all.old_flips.flips },
prose_step_triggered: { triggered: trigger.length > 0, reasons: trigger },
};
}
export interface ManifestEntry {
config: string;
priority: number;
cases: number;
model: string;
items: number;
calls: number;
input_tokens: number;
estimated_output_tokens_per_call: number;
estimated_cost_usd: number;
}
/** Price every planned call before dispatch; count_tokens is free. */
export async function priceManifest(configs: CalibrationConfig[], countTokens: (model: string, prompt: string) => Promise<number>,
root = CORPUS_ROOT): Promise<ManifestEntry[]> {
const entries: ManifestEntry[] = [];
for (const config of [...configs].sort((a, b) => a.priority - b.priority)) {
if (!fs.existsSync(path.join(root, config.id, 'corpus.json'))) continue;
const items = loadCorpus(config.id, root);
let input = 0;
for (const item of items) input += await countTokens(config.model, config.build(item.inputs)) * PHASES.length * PANEL_SAMPLES;
const calls = items.length * PHASES.length * PANEL_SAMPLES;
entries.push({ config: config.id, priority: config.priority, cases: config.cases.length, model: config.model, items: items.length, calls,
input_tokens: input, estimated_output_tokens_per_call: config.estimatedOutputTokens,
estimated_cost_usd: estimateCostUsd({ input, output: calls * config.estimatedOutputTokens }, config.model) });
}
return entries;
}
function flag(args: string[], name: string): string | undefined {
const index = args.indexOf(`--${name}`);
return index >= 0 ? args[index + 1] : undefined;
}
async function main(args: string[]): Promise<void> {
// The per-judge table (builders, schemas, verdict rules) is committed separately from this harness.
const { CALIBRATION_CONFIGS } = await import(path.join(import.meta.dir, 'judge-calibration-configs.ts')) as { CALIBRATION_CONFIGS: CalibrationConfig[] };
const command = args[0];
if (command === 'manifest') {
const client = new Anthropic();
const entries = await priceManifest(CALIBRATION_CONFIGS, async (model, prompt) =>
(await client.messages.countTokens({ model, messages: [{ role: 'user', content: prompt }] })).input_tokens);
const budget = Number(flag(args, 'budget') ?? 90);
let running = 0;
const manifest = { generated_at: new Date().toISOString(), budget_usd: budget,
entries: entries.map(entry => ({ ...entry, cumulative_usd: +(running += entry.estimated_cost_usd).toFixed(4), within_budget: running <= budget })) };
fs.writeFileSync(path.join(CORPUS_ROOT, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
console.table(manifest.entries);
return;
}
const config = CALIBRATION_CONFIGS.find(candidate => candidate.id === flag(args, 'config'));
if (!config) throw new Error(`unknown --config; expected one of ${CALIBRATION_CONFIGS.map(candidate => candidate.id).join(', ')}`);
const items = loadCorpus(config.id);
const runsDir = path.join(CORPUS_ROOT, config.id, 'runs');
const samplesPath = path.join(runsDir, 'comparison-1.samples.jsonl');
const resultPath = path.join(runsDir, 'comparison-1.result.json');
if (command === 'run' || command === 'resume') {
// resume dispatches only planned calls a stop left undispatched; recorded samples are never redrawn.
if (command === 'run' && fs.existsSync(samplesPath)) throw new Error(`${samplesPath} exists; calibration runs are never repeated`);
if (command === 'resume' && !fs.existsSync(samplesPath)) throw new Error(`${samplesPath} is missing; nothing to resume`);
const prior: SampleRecord[] = command === 'resume' ? fs.readFileSync(samplesPath, 'utf8').trim().split('\n').map(line => JSON.parse(line)) : [];
fs.mkdirSync(runsDir, { recursive: true });
const budgetUsd = Number(flag(args, 'budget'));
if (!Number.isFinite(budgetUsd) || budgetUsd <= 0) throw new Error('--budget <usd> is required');
const concurrency = Number(flag(args, 'concurrency') ?? 6);
const onRecord = (record: SampleRecord) => fs.appendFileSync(samplesPath, JSON.stringify(record) + '\n');
let spent = 0, stopped: string | null = null;
// Development items first; the held-out third is dispatched only after, with the same frozen requests.
for (const split of ['dev', 'heldout'] as const) {
const outcome = await runCalls(config, undispatched(planCalls(items, split), prior), { budgetUsd: budgetUsd - spent, concurrency, onRecord });
spent += outcome.records.reduce((sum, record) => sum + record.cost_usd, 0);
console.log(`[calibration] ${config.id} ${split}: ${outcome.records.length} calls, $${spent.toFixed(2)} cumulative${outcome.stopped ? `, stopped: ${outcome.stopped}` : ''}`);
if ((stopped = outcome.stopped)) break;
}
fs.writeFileSync(path.join(runsDir, 'comparison-1.stop.json'), JSON.stringify({ stopped }) + '\n');
} else if (command !== 'analyze') {
throw new Error(`unknown command ${command}; expected manifest, run, resume or analyze`);
}
const records: SampleRecord[] = fs.readFileSync(samplesPath, 'utf8').trim().split('\n').map(line => JSON.parse(line));
const stopPath = path.join(runsDir, 'comparison-1.stop.json');
const stopped = fs.existsSync(stopPath) ? JSON.parse(fs.readFileSync(stopPath, 'utf8')).stopped : null;
const result = analyze(config, items, records, stopped);
fs.writeFileSync(resultPath, JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify(result, null, 2));
}
if (import.meta.main) {
main(process.argv.slice(2)).catch(error => { console.error(error); process.exit(1); });
}
+2
View File
@@ -85,6 +85,7 @@ export const CASE_TEST_NAMES: Record<string, string> = {
'cso-full-audit': '/cso persists supported tenant-boundary findings with redacted evidence',
'cso-diff-mode': '/cso --diff records its base and investigates changed security paths',
'cso-infra-scope': '/cso --infra finds an attacker-to-credential execution path',
'plan-ceo-review-plan-mode': 'first terminal outcome is asked (Step 0 fires before any plan write)',
};
export const CASE_KEY_SEPARATOR = '#';
@@ -259,6 +260,7 @@ export const CODEX_CI_FILES: readonly string[] = [
'test/codex-e2e-recommendation-substance.test.ts',
'test/skill-e2e-outside-voice.test.ts',
'test/skill-e2e-outside-plan-disabled.test.ts',
'test/skill-e2e-safety-codex-boundary.test.ts',
];
/**
+76
View File
@@ -101,3 +101,79 @@ export function validateModel(input: string): string | null {
if ((ALL_MODEL_NAMES as readonly string[]).includes(input)) return null;
return `'${input}' is not a known model. Use ${ALL_MODEL_NAMES.join(', ')}.`;
}
/**
* Overlays `./setup --claude-model <id>` may select for the Claude host. The
* list is the Claude subset of ALL_MODEL_NAMES; a new Claude overlay joins
* both lists.
*/
export const CLAUDE_OVERLAY_MODELS = ['claude', 'opus-4-7', 'opus-4-8', 'sonnet-5', 'fable-5'] as const;
export type ClaudeOverlay = (typeof CLAUDE_OVERLAY_MODELS)[number];
export interface ClaudeOverlayError {
kind: 'alias' | 'unknown' | 'non-claude';
problem: string;
cause: string;
fix: string;
}
export type ClaudeOverlayResult =
| { overlay: ClaudeOverlay; generic: boolean }
| { error: ClaudeOverlayError };
const CLAUDE_CODE_ALIASES = ['opus', 'sonnet', 'haiku', 'default'];
/**
* Map a `--claude-model` value to the overlay setup renders. `resolveModel`
* does the family matching (bracketed context suffixes, dated and `-latest`
* snapshots); this adds the Claude-only allowlist and the errors setup prints.
* `generic` is true when a specific model ID has no overlay of its own and
* uses the generic `claude` overlay (for example `claude-opus-5-5`).
*/
export function resolveClaudeOverlay(id: string): ClaudeOverlayResult {
const s = id.trim().toLowerCase().replace(/\[[^\]]*\]$/, '').trim();
const problem = `${JSON.stringify(id)} is not a Claude model ID that gstack can map to a skill overlay.`;
if (CLAUDE_CODE_ALIASES.includes(s)) {
return { error: {
kind: 'alias',
problem,
cause: `"${s}" is a Claude Code alias whose model changes over time, so it names no fixed overlay.`,
fix: 'Pass a full model ID, for example ./setup --claude-model claude-opus-4-8, or ./setup --claude-model claude for the generic overlay.',
} };
}
const family = resolveModel(s);
if (!family) {
return { error: {
kind: 'unknown',
problem,
cause: 'It does not match any model family gstack knows.',
fix: 'Pass a Claude model ID such as claude-opus-4-8 or claude-sonnet-5, or claude for the generic overlay.',
} };
}
if (!(CLAUDE_OVERLAY_MODELS as readonly string[]).includes(family)) {
return { error: {
kind: 'non-claude',
problem,
cause: `It belongs to the ${family} family, and --claude-model selects Claude overlays only.`,
fix: `For Codex skills use ./setup --host codex --model ${id}. For Claude, pass a Claude model ID such as claude-opus-4-8.`,
} };
}
return { overlay: family as ClaudeOverlay, generic: family === 'claude' && s !== 'claude' };
}
// `bun scripts/models.ts claude-overlay <id>` — setup's bridge to the table
// above. Prints "<overlay>\t<generic 0|1>" or the error lines on stderr (exit 1).
if (import.meta.main) {
const [command, id] = process.argv.slice(2);
if (command !== 'claude-overlay' || id === undefined) {
console.error('Usage: bun scripts/models.ts claude-overlay <claude-model-id>');
process.exit(2);
}
const result = resolveClaudeOverlay(id);
if ('error' in result) {
console.error(`${result.error.problem}\n${result.error.cause}\n${result.error.fix}`);
process.exit(1);
}
console.log(`${result.overlay}\t${result.generic ? 1 : 0}`);
}
+13
View File
@@ -66,6 +66,12 @@
"test/skill-e2e-review-army.test.ts#review-army-quality-score": 86836,
"test/skill-e2e-review-attribution.test.ts": 77592,
"test/skill-e2e-review.test.ts": 136132,
"test/skill-e2e-safety-codex-boundary.test.ts": 1000,
"test/skill-e2e-safety-codex-consult.test.ts": 82940,
"test/skill-e2e-safety-design-risk.test.ts": 1000,
"test/skill-e2e-safety-ios-demo.test.ts": 1000,
"test/skill-e2e-safety-pair-agent.test.ts": 39860,
"test/skill-e2e-safety-ship-evidence.test.ts": 106130,
"test/skill-e2e-session-intelligence.test.ts": 55332,
"test/skill-e2e-shared-libs-paths.test.ts": 680000,
"test/skill-e2e-shared-libs-paths.test.ts#shared-libs-review-index-flags": 190202,
@@ -154,6 +160,7 @@
"test/skill-e2e-office-hours.test.ts": 136970,
"test/skill-e2e-outside-plan-disabled.test.ts": 54255,
"test/skill-e2e-outside-voice.test.ts": 186881,
"test/skill-e2e-overlay-harness-claude-dedicated-tools-vs-bash-opus-5-5.test.ts": 56080,
"test/skill-e2e-overlay-harness-claude-dedicated-tools-vs-bash-sonnet.test.ts": 85282,
"test/skill-e2e-overlay-harness-claude-dedicated-tools-vs-bash.test.ts": 80465,
"test/skill-e2e-overlay-harness-opus-4-7-effort-match-trivial.test.ts": 49282,
@@ -187,6 +194,12 @@
"test/skill-e2e-review-army.test.ts#review-army-simplification-precision": 23884,
"test/skill-e2e-review-attribution.test.ts": 1000,
"test/skill-e2e-review.test.ts": 179362,
"test/skill-e2e-safety-codex-boundary.test.ts": 100310,
"test/skill-e2e-safety-codex-consult.test.ts": 1000,
"test/skill-e2e-safety-design-risk.test.ts": 52700,
"test/skill-e2e-safety-ios-demo.test.ts": 53270,
"test/skill-e2e-safety-pair-agent.test.ts": 1000,
"test/skill-e2e-safety-ship-evidence.test.ts": 1000,
"test/skill-e2e-session-intelligence.test.ts": 1000,
"test/skill-e2e-setup-gbrain-bad-token.test.ts": 42912,
"test/skill-e2e-setup-gbrain-path4-local-pglite.test.ts": 165743,
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bun
/**
* Prompt-audit manifest: the files a contributor feeds to Anthropic's
* `/claude-api prompt-audit` (a Claude Code skill, run in your own Claude Code
* session) at each frontier-model release, cut into slices small enough for
* one audit pass each. Procedure: CONTRIBUTING.md "Prompt audit at each
* frontier-model release".
*
* Slices, in order: shared instructions every skill reads (CLAUDE.md, model
* overlays, preamble resolvers), the other resolvers, the skill templates
* (a skill's SKILL.md.tmpl and sections stay in one slice), the code that
* builds model requests, and the tests that pin skill wording (they read a
* template, skill or resolver and assert a literal of 40+ characters).
*
* Usage: bun run audit:manifest [--json] [--budget-kb <n>]
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
const ROOT = resolve(import.meta.dir, '..');
const DEFAULT_BUDGET_KB = 200;
export interface AuditSlice { id: string; group: string; files: string[]; bytes: number }
function walk(dir: string, keep: (rel: string) => boolean): string[] {
const out: string[] = [];
const visit = (d: string) => {
for (const entry of readdirSync(d, { withFileTypes: true })) {
const p = join(d, entry.name);
const rel = relative(ROOT, p).replaceAll('\\', '/');
if (entry.isDirectory()) {
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') visit(p);
} else if (keep(rel)) {
out.push(rel);
}
}
};
visit(dir);
return out.sort();
}
const size = (rel: string) => statSync(join(ROOT, rel)).size;
const read = (rel: string) => readFileSync(join(ROOT, rel), 'utf8');
/** Pack units (each a list of files that must stay together) into slices under the budget. */
function pack(group: string, units: string[][], budget: number): Omit<AuditSlice, 'id'>[] {
const slices: Omit<AuditSlice, 'id'>[] = [];
let current: Omit<AuditSlice, 'id'> | null = null;
for (const unit of units) {
const bytes = unit.reduce((sum, f) => sum + size(f), 0);
if (!current || (current.bytes > 0 && current.bytes + bytes > budget)) {
current = { group, files: [], bytes: 0 };
slices.push(current);
}
current.files.push(...unit);
current.bytes += bytes;
}
return slices;
}
export function buildManifest(budgetKb = DEFAULT_BUDGET_KB): AuditSlice[] {
const budget = budgetKb * 1024;
const preamble = walk(join(ROOT, 'scripts/resolvers/preamble'), rel => rel.endsWith('.ts'));
const shared = ['CLAUDE.md', ...walk(join(ROOT, 'model-overlays'), rel => rel.endsWith('.md')), ...preamble];
const resolvers = walk(join(ROOT, 'scripts/resolvers'), rel => rel.endsWith('.ts') && !preamble.includes(rel));
const templates = walk(ROOT, rel => rel.endsWith('.tmpl') && !rel.startsWith('test/'));
const bySkill = new Map<string, string[]>();
for (const rel of templates) {
const skill = rel.includes('/') ? rel.split('/')[0] : '(root)';
bySkill.set(skill, [...(bySkill.get(skill) ?? []), rel]);
}
const requestCode = [...walk(join(ROOT, 'lib'), rel => rel.endsWith('.ts')),
...walk(join(ROOT, 'design/src'), rel => rel.endsWith('.ts')),
...walk(join(ROOT, 'test/helpers'), rel => rel.endsWith('.ts'))]
.filter(rel => /api\.anthropic\.com|api\.openai\.com|\/v1\/messages|messages\.create|responses\.create/.test(read(rel)));
const pinningTests = walk(join(ROOT, 'test'), rel => /^test\/[^/]+\.test\.ts$/.test(rel))
.filter(rel => {
const src = read(rel);
return /SKILL\.md|\.md\.tmpl|scripts\/resolvers|model-overlays/.test(src) && /\.(toContain|toMatch)\((['"`])[^'"`]{40,}/.test(src);
});
const slices = [
...pack('shared instructions', [shared], Infinity),
...pack('resolvers', resolvers.map(f => [f]), budget),
...pack('skill templates', [...bySkill.values()], budget),
...pack('model-request code', requestCode.map(f => [f]), budget),
...pack('tests that pin skill wording', pinningTests.map(f => [f]), budget),
];
return slices.map((slice, i) => ({ id: `s${String(i + 1).padStart(2, '0')}`, ...slice }));
}
if (import.meta.main) {
const args = process.argv.slice(2);
const budgetArg = args.indexOf('--budget-kb');
const budgetKb = budgetArg >= 0 ? Number(args[budgetArg + 1]) : DEFAULT_BUDGET_KB;
if (!Number.isFinite(budgetKb) || budgetKb <= 0) {
console.error('--budget-kb needs a positive number of kilobytes');
process.exit(2);
}
const manifest = buildManifest(budgetKb);
if (args.includes('--json')) {
console.log(JSON.stringify(manifest, null, 2));
} else {
const files = manifest.reduce((n, s) => n + s.files.length, 0);
console.log(`Prompt-audit manifest: ${manifest.length} slices, ${files} files. Run /claude-api prompt-audit in Claude Code on one slice at a time (CONTRIBUTING.md).`);
for (const slice of manifest) {
console.log(`\n## ${slice.id} ${slice.group} (${slice.files.length} files, ${Math.round(slice.bytes / 1024)} KB)`);
for (const file of slice.files) console.log(file);
}
}
}
+1 -1
View File
@@ -171,7 +171,7 @@ Branch on the echoed \`CODEX_MODE\`:
- **\`under_codex\`** — stale artifact selected its own harness. Print: "Codex outside review unavailable: harness mismatch; no outside process started. Missing coverage. Repair: setup --host codex." Skip the outside invocation and follow the workflow's native-review instructions below. Conflicting inherited harness markers are not grounds to guess another provider.
- **\`not_authed\`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run \`codex login\` or set \`$CODEX_API_KEY\`." ${nativeRoute}
- **\`broken_install\`** — the CLI is on PATH but cannot execute (spawn ENOENT, non-executable binary, missing vendor payload). Print: "Codex is installed but its binary cannot run — Codex passes skipped. Reinstall: \`npm install -g @openai/codex\`." Relay the probe's HINT lines. ${nativeRoute}
- **\`model_unusable\`** — the selected model (see \`CODEX_MODEL:\`) is invalid or unavailable to the account (#2477: HTTP 400 on every call). Relay the probe's HINT lines and the fix (\`GSTACK_CODEX_MODEL=<supported-model>\` or config.toml \`model\`); never substitute a model. ${nativeRoute} The ~10s round trip is cached for 1h.
- **\`model_unusable\`** — the selected model (see \`CODEX_MODEL:\`) is invalid or unavailable to the account (HTTP 400 on every call). Relay the probe's HINT lines and the fix (\`GSTACK_CODEX_MODEL=<supported-model>\` or config.toml \`model\`); never substitute a model. ${nativeRoute} The ~10s round trip is cached for 1h.
- **\`sandbox_unavailable\`** — Codex's sandbox cannot start here (containers without user namespaces); the probe printed the reason and fix. No paid call ran; outside coverage is unavailable. ${nativeRoute}
- **\`ready\`** or **\`unverified\`** — run the Codex pass below. \`unverified\` means the model check timed out; say so, and let the pass's own verdict decide.`;
}
+12 -33
View File
@@ -1,47 +1,26 @@
/**
* {{DESIGN_DOC_DISCOVERY}} — the canonical design-doc discovery block (#703).
* {{DESIGN_DOC_DISCOVERY}} — the canonical design-doc discovery block.
*
* Finds the design doc a plan review should read: newest branch-scoped doc
* under ~/.gstack/projects/<slug>/, falling back to newest project-scoped
* doc, then lets a repo-local feature doc (docs/designs/*.md) win when
* it is at least as fresh. A root DESIGN.md is never a design doc: it is the
* design system /design-consultation writes, and preferring it reviewed
* plans against a token file while printing "Design doc found" (#2839). office-hours dual-writes docs/designs/ alongside
* ~/.gstack, and the committed copy is what teammates see — but a stale old
* repo doc must never shadow a newer private session.
*
* Single source of truth for the freshness-preference logic that previously
* lived verbatim in plan-ceo-review, plan-eng-review, plan-devex-review, and
* the prerequisite-skill re-check in review.ts (GStack 2 fork-port wave,
* time-attack/gstack). Drift between copies meant plan reviews could
* disagree about which design doc wins.
* One invocation of bin/gstack-design-doc-find, which owns the precedence
* (newest branch-scoped doc in the state root's project directory, then the
* newest project-scoped doc, then the newest repo-local docs/designs/*.md when
* it is at least as fresh; a root DESIGN.md is the design system, never the doc) and resolves the state root through
* bin/gstack-paths. Plan reviews, autoplan and the prerequisite-skill re-check
* in spec-review.ts all render this one block, so they agree on which doc wins.
*
* The fragment carries no code fences — the {{DESIGN_DOC_DISCOVERY}} token
* sits inside each caller's ```bash block. Callers must set $SLUG and
* $BRANCH first (and `setopt +o nomatch` for zsh); the block sets $DESIGN
* and prints "Design doc found: ..." or "No design doc found".
* $BRANCH first; the block sets $DESIGN and prints "Design doc found: ..." or
* "No design doc found".
*/
import type { TemplateContext } from './types';
/**
* Raw canonical fragment, exported so TS resolvers (review.ts's prerequisite
* re-check) can interpolate it into their own template strings instead of
* embedding a drifting copy.
* Raw canonical fragment, exported so TS resolvers (spec-review.ts's
* prerequisite re-check) can interpolate it into their own template strings.
*/
export const DESIGN_DOC_DISCOVERY_BLOCK = `_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$_LOCALDOC" ] && _LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
# Repo-local docs win when at least as fresh (#703): office-hours dual-writes
# docs/designs/ alongside ~/.gstack, and the committed copy is what teammates
# see. A stale old repo doc never shadows a newer private session.
_REPOTOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
_REPODOC=""
# Only docs/designs/ holds feature design docs; a root DESIGN.md is the design system (#2839).
[ -n "$_REPOTOP" ] && _REPODOC=$(ls -t "$_REPOTOP"/docs/designs/*.md 2>/dev/null | head -1)
DESIGN="$_LOCALDOC"
if [ -n "$_REPODOC" ] && { [ -z "$_LOCALDOC" ] || [ "$_REPODOC" -nt "$_LOCALDOC" ]; }; then
DESIGN="$_REPODOC"
fi
export const DESIGN_DOC_DISCOVERY_BLOCK = `DESIGN=$(~/.claude/skills/gstack/bin/gstack-design-doc-find "$SLUG" "$BRANCH")
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"`;
export function generateDesignDocDiscovery(_ctx: TemplateContext): string {
+1
View File
@@ -35,6 +35,7 @@ Check if the diff touches frontend files using \`gstack-diff-scope\`:
\`\`\`bash
source <(${ctx.paths.binDir}/gstack-diff-scope <base> 2>/dev/null)
echo "SCOPE_FRONTEND=$SCOPE_FRONTEND"
\`\`\`
**If \`SCOPE_FRONTEND=false\`:** Skip design review silently. No output.
+1 -1
View File
@@ -17,7 +17,7 @@ import { CC_BACKGROUND_DEFAULT_SINCE } from './constants';
import { outsideVoiceFailurePolicy, outsideVoiceFor, outsideVoiceInvocation, outsideVoicePreflight, outsideVoiceProvenance } from './outside-voice';
import { runtimeRootPrelude } from './runtime-root';
const CODEX_BOUNDARY = 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are skill definitions, not repository review data. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\\n\\n';
const CODEX_BOUNDARY = 'Filesystem boundary: do not read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. They hold skill definitions, not repository code to review. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Do not modify agents/openai.yaml. Review only the repository code.\\n\\n';
export function generateCodexSecondOpinion(ctx: TemplateContext): string {
+18 -15
View File
@@ -76,30 +76,33 @@ For /qa and /qa-only:
- Browser Quick: SECONDS=30. Browser Full/Regression: SECONDS=900.
- Functional Full, Quick and Regression have no default total timer.
Set SECONDS to the shorter mode/caller limit; an unlimited mode uses the caller's bound.
Without a total time limit, do not start D; announce finite command timeouts.
Without a total time limit, do not create DEADLINE_FILE; announce finite command timeouts.
Stop when scoped contracts are tested or blocked.
Clocks/checkpoints use REPORT_DIR; mixed standalone runs use REPORT_DIR/browser and REPORT_DIR/functional, with one final report at REPORT_DIR. Caller paths win.
Names used below (quote every path):
- R: this surface's owned probe directory, chosen above. D: \`R/deadline.json\`, which exists only for a bounded run.
- PROBE_DIR: this surface's owned probe directory, chosen above. DEADLINE_FILE: \`PROBE_DIR/deadline.json\`, which exists only for a bounded run.
- SECONDS: the total probe budget set above. NNN: a fresh three-digit ID (001, 002, ...) for each capture or checkpoint; never reuse one.
- G = \`${quoteSafePath(ctx.paths.binDir)}/gstack-qa-deadline\`, the deadline guard: \`start\` creates D, \`status\` prints \`remainingMs\` and \`expired\`, and \`run\` executes one command, stopping it at the deadline.
- Q = \`${quoteSafePath(ctx.paths.binDir)}/gstack-qa-evidence\`, the functional evidence recorder: \`capture\` runs one command, stores its exit code, stdout and stderr under \`R/.qa-evidence/NNN/\`, and prints status \`complete\`, \`incomplete\` or \`sensitive\`.
- DEADLINE_TOOL = \`${quoteSafePath(ctx.paths.binDir)}/gstack-qa-deadline\`, the deadline guard: \`start\` creates DEADLINE_FILE, \`status\` prints \`remainingMs\` and \`expired\`, and \`run\` executes one command, stopping it at the deadline.
- EVIDENCE_TOOL = \`${quoteSafePath(ctx.paths.binDir)}/gstack-qa-evidence\`, the functional evidence recorder: \`capture\` runs one command, stores its exit code, stdout and stderr under \`PROBE_DIR/.qa-evidence/NNN/\`, and prints status \`complete\`, \`incomplete\` or \`sensitive\`.
Start once before baseline: \`bun G start D SECONDS [EARLIER_UTC]\` if bounded.
Start once before baseline: \`bun DEADLINE_TOOL start DEADLINE_FILE SECONDS [EARLIER_UTC]\` if bounded.
EARLIER_UTC = caller's absolute deadline, if set.
Functional: \`bun Q capture R NNN [--public] --deadline D -- COMMAND ARGS\`.
Unbounded: use \`--timeout-ms MS\` instead of \`--deadline D\`.
--public requires approved public/synthetic output; Q screens credentials. For complete private captures, await a safe Read of \`R/.qa-evidence/NNN/observation.json\` (the decoded stdout). Sensitive/incomplete captures cannot anchor checkpoints.
Bounded browsers: \`bun G run D -- COMMAND ARGS\`. No detached probes.
Never reset D/bypass G. Expiry or invalid/missing D stops probes; report unfinished coverage. QA_DEADLINE receipts are not observations.
Functional: \`bun EVIDENCE_TOOL capture PROBE_DIR NNN [--public] --deadline DEADLINE_FILE -- COMMAND ARGS\`.
Unbounded: use \`--timeout-ms MS\` instead of \`--deadline DEADLINE_FILE\`.
--public requires approved public/synthetic output; EVIDENCE_TOOL screens credentials. For complete private captures, await a safe Read of \`PROBE_DIR/.qa-evidence/NNN/observation.json\` (the decoded stdout). Sensitive/incomplete captures cannot anchor checkpoints.
Bounded browsers: \`bun DEADLINE_TOOL run DEADLINE_FILE -- COMMAND ARGS\`. No detached probes.
Never reset DEADLINE_FILE/bypass DEADLINE_TOOL. Expiry or an invalid/missing DEADLINE_FILE stops probes; report unfinished coverage. QA_DEADLINE receipts are not observations.
## 2. Probe loop
Each probe is one native command/interaction plus checks, excluding bookkeeping.
Never batch probes.
How one probe works: (1) if bounded, check the clock and choose the next command;
(2) publish a checkpoint naming it (functional: \`capture --after\` publishes it);
(3) run exactly that command via DEADLINE_TOOL or EVIDENCE_TOOL, read its result, repeat.
1. First demonstrate success: output AND durable effects. Guard if bounded; await completion.
2. **Decide whether another probe is needed.** If bounded, run \`bun G status D\`.
2. **Decide whether another probe is needed.** If bounded, run \`bun DEADLINE_TOOL status DEADLINE_FILE\`.
If expired or no safe next probe remains, STOP exploration; write the report, not a checkpoint.
${reportOnly ? ` **Classify the last result before copying it.** For public or synthetic observations,
retain the entire result unchanged, including owned fixture paths, IDs, hashes and
@@ -119,11 +122,11 @@ ${reportOnly ? ` For guarded text, copy the complete span between the guard's
Preserve every safe program-JSON key/value and identity hash unchanged.
Withhold unsafe values, disclose limits and stop that chain.
Check fields before publication. No drafts/placeholders or invented safe-path redactions; corrections cannot repair published notes.
Functional: do not write this file; the next capture publishes it: \`bun Q capture R NNN --deadline D --after PREV --hypothesis 'why' -- CMD\` (PREV: the last complete capture's ID). Q supplies observed; never transcribe it.
Functional: do not write this file; the next capture publishes it: \`bun EVIDENCE_TOOL capture PROBE_DIR NNN --deadline DEADLINE_FILE --after PREV --hypothesis 'why' -- CMD\` (PREV: the last complete capture's ID). EVIDENCE_TOOL supplies observed; never transcribe it.
Browser checkpoints use Write: create the four-field file yourself.
Wait for successful checkpoint publication before dispatch.
Never backfill or overwrite notes.
3. Run that exact probe; G enforces the deadline when bounded.
3. Run that exact probe; DEADLINE_TOOL enforces the deadline when bounded.
Report refusals as not-run; retain initial state/inputs/results. Repeat from step 2.
4. Replay the exact failing command/request from the same initial fixture state via steps 2–3 (same native command, fresh capture ID)
${reportOnly ? 'to confirm it' : 'before repair'}, then minimize via those gates. Expiry leaves confirmation/minimization incomplete.
@@ -147,7 +150,7 @@ Never freeze buggy output, weaken tests or delete valid red tests.`}
## 4. Final report
Use the surface report template; link each checkpoint. Separate browser scores, functional outcomes and proposed/executed tests.
Write R/annotations.json {evidence: [{capture, command, contract, expected, classification}], limits} (browser-only: evidence [], checkpoints in limits); before Markdown \`bun Q materialize R annotations.json\` (fills observed/metadata; prints reportLinks); you classify. Retain all safe probes, including failures/replays; disclose withheld/incomplete evidence.
Write PROBE_DIR/annotations.json {evidence: [{capture, command, contract, expected, classification}], limits} (browser-only: evidence [], checkpoints in limits); before Markdown \`bun EVIDENCE_TOOL materialize PROBE_DIR annotations.json\` (fills observed/metadata; prints reportLinks; runs once per report root); you classify. Annotate every safe capture, including failures/replays: an omitted capture is withheld and keeps the verdict inconclusive. Classify a capture taken before an input change \`superseded\`; it closes when the same command reran on current inputs. Disclose withheld/incomplete evidence.
Evidence is invocation-local${reportOnly ? '.' : '; /ship reruns once per invocation.'}
Missing prerequisites/expectations/observations, timeouts and refusal never pass.
Pass requires all required current-input contracts to pass with no required remainder.
@@ -260,7 +263,7 @@ Never install, import cookies or bootstrap tests. Functional-only skips browser
**3. Run smoke and plan checks.**
Follow the shared Probe loop for smoke checks and replays until the smoke limit.
Then run required plan checks and revalidation, even after smoke expires, using the same procedure but no smoke guard; never reset the clock. Their checkpoints sit beside D; they skip \`G status D\` and use \`--timeout-ms\`, not \`--deadline D\`. Post-expiry smoke rechecks are not-run.
Then run required plan checks and revalidation, even after smoke expires, using the same procedure but no smoke guard; never reset the clock. Their checkpoints sit beside DEADLINE_FILE; they skip \`DEADLINE_TOOL status DEADLINE_FILE\` and use \`--timeout-ms\`, not \`--deadline DEADLINE_FILE\`. Post-expiry smoke rechecks are not-run.
Use finite command timeouts, capped at the caller's remaining time if it has a deadline.${ship ? '' : ' /review sets none; only an invoker-supplied EARLIER_UTC counts.'}
Await clock/guard results before acting. When the caller's deadline expires, mark unfinished checks not-run.
+2 -1
View File
@@ -199,7 +199,8 @@ Core findings keep the core Confidence Calibration gates.
#### 5. Score and present specialists
Only specialist findings enter this header and \`quality_score\`; core findings do not.
Use the merged NON-advisory specialist findings for both counts and score:
Use the merged NON-advisory specialist findings for both counts and score;
the header's N is X + Y, so advisory findings never add to it:
\`quality_score = max(0, 10 - (critical_count * 2 + informational_count * 0.5))\`
Cap at 10 and retain for ${persistRef}. These are not final unresolved-defect totals.
Print only this block: the stage 6 activity object and \`test_stub\` bodies are log and Fix-First data.
+1 -1
View File
@@ -133,7 +133,7 @@ if command -v jq >/dev/null 2>&1; then
# .commit must be bound BEFORE piping to the split commit array: a
# pipe rebinds jq's context, so a bare .commit after it indexes the
# ARRAY with a string, every line errors into 2>/dev/null, and the
# aggregate is empty forever — the #2018 zero-tasks bug.
# aggregate is empty forever.
jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \\
'.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))' \\
"$f" 2>/dev/null >> "$ALL_JSONL" || true
+3 -3
View File
@@ -1432,7 +1432,7 @@ function trackShardBrowser(stateDir: string, env: NodeJS.ProcessEnv) {
let alive = true;
const check = (capture: Capture) => {
if (closed || capture.abort.signal.aborted || active !== capture) throw new CaptureStopped();
if (closed || capture.abort.signal.aborted || active !== capture || (!forced && performance.now() >= forceAt)) throw new CaptureStopped();
if (performance.now() >= Math.min(capture.deadline, deadline)) throw new BrowserCleanupError('browser ownership deadline exceeded');
};
const probe = async (capture: Capture, command: string, args: string[], timeout: number) => {
@@ -1462,7 +1462,7 @@ function trackShardBrowser(stateDir: string, env: NodeJS.ProcessEnv) {
killProcessGroup(child, 'SIGKILL');
reaper = setTimeout(() => finish(null), 100);
};
const timer = setTimeout(stop, Math.max(1, remaining));
const timer = setTimeout(() => { if (!closed) errors.add('browser identity probe did not complete'); stop(); }, Math.max(1, Math.min(remaining, forced ? Infinity : forceAt - performance.now())));
capture.abort.signal.addEventListener('abort', stop, { once: true });
child.once('error', () => { failed = true; finish(null); });
child.once('close', finish);
@@ -1693,7 +1693,7 @@ function trackShardBrowser(stateDir: string, env: NodeJS.ProcessEnv) {
};
const enqueue = () => {
if (closed || pending || process.platform === 'win32') return;
const operation: Capture = { abort: new AbortController(), deadline: Math.min(performance.now() + 10000, deadline, forced ? Infinity : forceAt), probes: new Set() };
const operation: Capture = { abort: new AbortController(), deadline: Math.min(performance.now() + 10000, deadline), probes: new Set() };
active = operation;
pending = (async () => {
try {
+2 -1
View File
@@ -26,12 +26,13 @@ export const PR_PROFILE_CASE_IDS = [
'gstack-upgrade-happy-path',
'investigate-owned-completion', 'investigate-owned-abort', 'investigate-owned-ending-error',
'ship-coverage-value', 'review-test-value', 'test-audit-report-only',
'office-hours-auto-mode',
'office-hours-auto-mode', 'plan-ceo-review-plan-mode',
] as const;
/** Audited ownership: unknown/direct-describe files remain broad coverage. */
export const PR_PROFILE_FILES: Record<string, readonly string[]> = {
'test/skill-e2e-office-hours-auto-mode.test.ts': ['office-hours-auto-mode'],
'test/skill-e2e-plan-ceo-plan-mode.test.ts': ['plan-ceo-review-plan-mode'],
'test/skill-e2e-investigate-owned-completion.test.ts': ['investigate-owned-completion'],
'test/skill-e2e-investigate-owned-termination.test.ts': ['investigate-owned-abort', 'investigate-owned-ending-error'],
'test/skill-e2e-hermetic-canary.test.ts': ['hermetic-canary', 'hermetic-sentinel'],