mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
Replaying run 36385945043's two Design attempts showed the existing routes
rejected correct endings: attempt 1 at the typed-completion path field
('- Reviewed plan written to …' is not a 'Plan written to' line), attempt 2
at the leading-fence veto (its final message opens with the dashboard).
nativePlanTerminalPreconditions is the structural prefix of
hasNativePlanTerminal (behavior unchanged). structuredPlanCompletion adds,
inside the existing nativeSummary branch: a complete report (Design
binding for Design), a completed review-log row for the expected skill
appended during this attempt under the child's GSTACK_HOME/project slug
(resolved with bin/gstack-slug) and stamped with the fixture commit, timed
between the report/last answer (second resolution) and the final native
message, a final message with stop_reason end_turn (now carried on public
transcript messages), and no visible question or permission prompt.
Timeout summaries add idleFor and lastTerminalCandidate. Terminal and throw
captures copy the plan file and review-log rows into the artifact
directory; copies are best-effort and recorded in evidence-copy.json.
Free regressions: both captured Design endings (trimmed fixture with
provenance; report, row and end_turn reconstructed and labelled), the
negative controls, and real-PTY completion/timeout runs through the real
review logger.
58 lines
2.5 KiB
TypeScript
58 lines
2.5 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { getProjectEvalDir } from './eval-store';
|
|
|
|
interface PlanCountSnapshot {
|
|
skillName: string;
|
|
observation: object;
|
|
raw: string;
|
|
visible: string;
|
|
viewport?: string;
|
|
cwd: string;
|
|
claudeConfigDir: string | null;
|
|
}
|
|
|
|
/** One owned directory per count attempt; periodic captures replace files atomically. */
|
|
export function createPlanCountSnapshotWriter(env: NodeJS.ProcessEnv = process.env):
|
|
(input: PlanCountSnapshot) => { artifactDir?: string; artifactError?: string } {
|
|
let artifactDir: string | undefined;
|
|
// An explicit output directory requests retention even outside CI's named
|
|
// runs. Keep its fallback stable across checkpoints and unique per writer.
|
|
const runId = env.EVALS_RUN_ID || (env.GSTACK_EVAL_DIR ? `local-${randomUUID()}` : undefined);
|
|
return (input) => {
|
|
if (!runId) return {};
|
|
try {
|
|
if (!artifactDir) {
|
|
const segment = (text: string) => text.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 120) || 'run';
|
|
const root = path.resolve(env.GSTACK_EVAL_DIR || getProjectEvalDir(), 'pty-count', segment(runId));
|
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
artifactDir = fs.mkdtempSync(path.join(root, `${segment(input.skillName)}-${Date.now()}-`));
|
|
}
|
|
const write = (name: string, content: string) => {
|
|
const target = path.join(artifactDir!, name);
|
|
fs.writeFileSync(`${target}.tmp`, content, { mode: 0o600 });
|
|
fs.renameSync(`${target}.tmp`, target);
|
|
};
|
|
write('terminal.raw.log', input.raw);
|
|
write('terminal.visible.log', input.visible);
|
|
if (input.viewport !== undefined) write('terminal.screen.log', input.viewport);
|
|
write('observation.json', JSON.stringify({
|
|
...input.observation, artifactDir,
|
|
capture: { skill: input.skillName, runId, cwd: input.cwd,
|
|
claudeConfigDir: input.claudeConfigDir, at: new Date().toISOString() },
|
|
}, null, 2) + '\n');
|
|
return { artifactDir };
|
|
} catch (error) {
|
|
// Preserve any partial evidence and the original test outcome; make the
|
|
// write failure visible instead of claiming diagnostics were retained.
|
|
return { artifactDir, artifactError: String(error) };
|
|
}
|
|
};
|
|
}
|
|
|
|
/** Keep a single snapshot outside the temporary fixture that setup later removes. */
|
|
export function persistPlanCountSnapshot(input: PlanCountSnapshot, env: NodeJS.ProcessEnv = process.env) {
|
|
return createPlanCountSnapshotWriter(env)(input);
|
|
}
|