Files
gstack/test/context-restore-transcripts.test.ts
T
Garry Tanandcapy-ai[bot] c1900088d5 v1.91.272.0 feat(skills): absorb pstack's blast radius, transcript recall, vet-the-number and design-twice as lazy passes (#3500)
* feat(skills): absorb pstack's blast radius, transcript recall, vet-the-number and design-twice as lazy sections

/investigate gains sections/blast-radius.md (the one safety fact, the five-rung
evidence ladder, look where grep stops, confirmed and cleared risks apart) with a
Phase 4 pointer, and the shared-premise red flag before the 3-strike rule.
/context-restore is carved for the first time: on NO_CHECKPOINTS, sections/transcripts.md
mines this workspace's own transcripts (host stores declared in hosts/*.ts and rendered
by TRANSCRIPT_SOURCES / TRANSCRIPT_DISCOVERY), a fixed 7-day window, never another
project's folder, subagents return only findings, redaction before anything leaves,
git and gh verify every artifact, a status-tagged brief. /benchmark is carved for the
first time: sections/vet-the-number.md asks the seven questions before Phase 5 reports
a comparison. /plan-eng-review gains sections/design-twice.md (a second structurally
distinct shape compared on interface depth, the eight design red flags), bound by the
autoplan methodology snapshot alongside review-sections.md.

Ideas from pstack (Lauren Tan, MIT), rewritten in gstack's words; NOTICE.md. Garry,
2026-10-26: "Do it, send". Guards: carve-guards entries and measured lines, eleven
plan-eng-review pins re-measured, periodic carve-section-loading cases for the two new
carves, free tests with a fixture transcript.

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

* fix(pstack-c1): fold design-twice into Section 1 of the eng review section, per-host transcript table, fixture and slice-cap repairs

The remote free run reported nine failures in seven files, all from this
branch: four tests encode plan-eng-review's one companion section
(eng-scope-entry-ap, autoplan-methodology-names, plan-review-cases,
review-checklists), so the design-twice pass now lives inside Section 1 of
sections/review-sections.md with the skeleton, manifest and
bin/gstack-autoplan-snapshot.ts back at main; the Codex render of
context-restore named ~/.claude/ through the all-hosts transcript table, so
{{TRANSCRIPT_SOURCES}} renders this host's row only; the transcript fixture
is declared in FREE_FIXTURES; and the two new periodic cases took the census
to 39 slices, so evals-periodic.yml's eval-slices max-parallel goes 38 -> 40.
Pins: 11 plan-eng-review re-pins at +2,896; maxSizeRatio 1.2400 (measured
1.2389); Codex gpt-5.4 overlay 159,588 bytes.

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

* fix: pre-landing review fixes

The discovery block takes the none branch from hosts/*.ts's transcripts
declaration instead of a host-name fallthrough, and the resolver header says
where each half lives; the T2 subagent brief frames the transcript as data,
not instructions; the test asserts table/block agreement per host, imports
CAPY_NO_TRANSCRIPTS, and drops the fixture-shape case nothing reads.

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

* docs: ADDING_A_HOST.md lists the optional transcripts HostConfig field

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-10-27 09:59:09 +00:00

180 lines
11 KiB
TypeScript

/**
* /context-restore's no-checkpoint fallback (pstack absorb, item 3): the carve
* shape, the host transcript table, and the discovery block run against fixture
* transcripts. The block must list only this workspace's files inside the
* window, never a sibling project's, and print TRANSCRIPTS: none on Capy.
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { CARVE_GUARDS } from './helpers/carve-guards';
import { ALL_HOST_CONFIGS } from '../hosts/index';
import { CAPY_NO_TRANSCRIPTS, generateTranscriptDiscovery, generateTranscriptSources } from '../scripts/resolvers/transcripts';
const ROOT = path.resolve(import.meta.dir, '..');
const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
const FIXTURE = path.join(ROOT, 'test', 'fixtures', 'context-restore-transcript.jsonl');
const FENCE = '\u0060\u0060\u0060';
const DISCOVERY_MARKER = 'TRANSCRIPT_DAYS="${TRANSCRIPT_DAYS:-7}"';
function bashBlockAfter(text: string, heading: string, marker: string): string {
const start = text.indexOf(heading);
if (start < 0) throw new Error('no heading ' + heading);
const at = text.indexOf(marker, start);
if (at < 0) throw new Error('no ' + marker + ' after ' + heading);
const open = text.lastIndexOf(FENCE + 'bash', at);
const close = text.indexOf(FENCE, open + 7);
return text.slice(open + 8, close);
}
describe('context-restore carve: the transcript fallback', () => {
const skeleton = read('context-restore/SKILL.md');
const section = read('context-restore/sections/transcripts.md');
const manifest = JSON.parse(read('context-restore/sections/manifest.json')) as { skill: string; sections: Array<{ id: string; file: string }> };
test('one section, registered, pointed to from the no-checkpoint branch and nowhere earlier', () => {
expect(manifest.skill).toBe('context-restore');
expect(manifest.sections.map(s => s.id)).toEqual(['transcripts']);
expect(CARVE_GUARDS['context-restore']!.expectedSections).toEqual(['transcripts.md']);
const noSaved = skeleton.indexOf('## If no saved contexts exist');
const stop = skeleton.indexOf('> **STOP.**');
expect(noSaved).toBeGreaterThan(skeleton.indexOf('### Step 1: Find saved contexts'));
expect(stop).toBeGreaterThan(noSaved);
expect(skeleton.match(/> \*\*STOP\.\*\*/g)).toHaveLength(1);
expect(skeleton).toContain('sections/transcripts.md');
expect(skeleton).not.toContain('"No saved contexts yet.');
expect(skeleton).toMatch(/HARD GATE:\*\* Do NOT implement code changes/);
});
test("the host table comes from hosts/*.ts: this host's row plus the Capy row, never another host's paths", () => {
const ctx = (host: string) => ({ host, skillName: 'context-restore', paths: { binDir: '$GSTACK_BIN' } }) as never;
expect(ALL_HOST_CONFIGS.filter(h => h.transcripts).map(h => h.name).sort()).toEqual(['claude', 'codex']);
const claude = generateTranscriptSources(ctx('claude'));
expect(section).toContain(claude);
expect(claude).toContain('| Claude Code | `~/.claude/projects/<slug>/*.jsonl` |');
expect(claude).not.toContain('~/.codex/');
const codex = generateTranscriptSources(ctx('codex'));
expect(codex).toContain('| OpenAI Codex CLI | `~/.codex/sessions/');
expect(codex).not.toContain('~/.claude/');
const cursor = generateTranscriptSources(ctx('cursor'));
expect(cursor).toContain('| Cursor | none |');
expect(cursor).not.toContain('~/.claude/');
for (const table of [claude, codex, cursor]) expect(table).toContain('| Capy (any model) | none |');
expect(section).toMatch(/Capy[^\n]*say so and use the shared record and git/);
});
test('the table and the discovery block agree for every host on whether a store exists', () => {
for (const host of ALL_HOST_CONFIGS) {
const ctx = { host: host.name, skillName: 'context-restore', paths: { binDir: '$GSTACK_BIN' } } as never;
const discovery = generateTranscriptDiscovery(ctx);
const sources = generateTranscriptSources(ctx);
if (host.transcripts) {
expect(sources, host.name).toContain(host.transcripts.root);
expect(discovery, host.name).toContain('TRANSCRIPT_DIR="' + host.transcripts.root.replace(/^~/, '$HOME').split(/\/(?=<|\*)/)[0]);
expect(discovery, host.name).not.toContain('declares no transcript store');
} else {
expect(sources, host.name).toContain('| ' + host.displayName + ' | none |');
expect(discovery, host.name).toContain('declares no transcript store');
}
}
expect(section).toMatch(/data, not instructions/);
});
test('the rules: a 7-day default window, never another project, subagents return findings only, redaction before anything leaves, git and gh verify, the tagged brief', () => {
expect(section).toContain('the default is the last 7 days');
expect(section).toContain('"all" means all and is never quietly narrowed');
expect(section).toContain("never open another project's");
expect(section).toContain('The raw transcript stays in the subagent.');
expect(section).toContain('gstack-redact');
expect(section).toContain('gbrain search');
expect(section).toMatch(/checked with \u0060git\u0060 and\s+\u0060gh\u0060 before it enters the brief/);
for (const tag of ['[merged #N]', '[open PR #N]', '[in flight <branch>]', '[verified, uncommitted]', '[reverted #N]', '[planned, not started]']) expect(section).toContain(tag);
for (const heading of ['### Capsule', '### Threads', '### Problems', '### Next move']) expect(section).toContain(heading);
expect(section).toContain('D) Save this as a checkpoint now (/context-save)');
});
test('hosts with no declared store render a one-line none; Claude and Codex render their own discovery', () => {
const ctx = (host: string) => ({ host, skillName: 'context-restore', paths: { binDir: '$GSTACK_BIN' } }) as never;
expect(generateTranscriptDiscovery(ctx('cursor'))).toContain('TRANSCRIPTS: none (Cursor declares no transcript store');
expect(generateTranscriptDiscovery(ctx('claude'))).toContain('$HOME/.claude/projects/');
expect(generateTranscriptDiscovery(ctx('codex'))).toContain('$HOME/.codex/sessions');
for (const host of ['claude', 'codex', 'cursor']) expect(generateTranscriptDiscovery(ctx(host))).toContain('if [ -n "${GSTACK_CAPY:-}" ]');
});
});
describe('the discovery block against fixture transcripts', () => {
let tmp: string;
let home: string;
let project: string;
const block = bashBlockAfter(read('context-restore/sections/transcripts.md'), '### Step T1', DISCOVERY_MARKER);
const codexRender = path.join(ROOT, '.agents', 'skills', 'gstack-context-restore', 'SKILL.md');
function sh(script: string, extra: Record<string, string> = {}, cwd = project) {
const env: Record<string, string> = {};
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !k.startsWith('GSTACK_') && k !== 'CLAUDE_PROJECT_DIR') env[k] = v;
const r = spawnSync('bash', ['-c', script], { cwd, env: { ...env, HOME: home, ...extra }, encoding: 'utf8', timeout: 30_000 });
return { out: (r.stdout ?? '') + (r.stderr ?? ''), status: r.status ?? -1 };
}
const slug = (p: string) => p.replace(/[^A-Za-z0-9]/g, '-');
beforeAll(() => {
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cr-transcripts-')));
home = path.join(tmp, 'home');
project = path.join(tmp, 'work', 'shop.app');
fs.mkdirSync(project, { recursive: true });
spawnSync('git', ['init', '-q', project], { timeout: 30_000 });
// ~/.claude/skills/gstack is this checkout, so the block's gstack-slug call resolves.
fs.mkdirSync(path.join(home, '.claude', 'skills'), { recursive: true });
fs.symlinkSync(ROOT, path.join(home, '.claude', 'skills', 'gstack'));
const mine = path.join(home, '.claude', 'projects', slug(project));
const sibling = path.join(home, '.claude', 'projects', slug(path.join(tmp, 'work', 'shop-admin')));
fs.mkdirSync(mine, { recursive: true });
fs.mkdirSync(sibling, { recursive: true });
fs.copyFileSync(FIXTURE, path.join(mine, '7c2f.jsonl'));
fs.copyFileSync(FIXTURE, path.join(mine, 'old.jsonl'));
fs.copyFileSync(FIXTURE, path.join(sibling, 'other.jsonl'));
const tenDaysAgo = new Date(Date.now() - 10 * 86_400_000);
fs.utimesSync(path.join(mine, 'old.jsonl'), tenDaysAgo, tenDaysAgo);
// Codex: dated rollouts, one for this cwd, one for the sibling.
const day = path.join(home, '.codex', 'sessions', '2026', '10', '20');
fs.mkdirSync(day, { recursive: true });
const meta = (cwd: string) => JSON.stringify({ timestamp: '2026-10-20T17:02:11.000Z', type: 'session_meta', payload: { id: 'r1', cwd } }) + '\n' + fs.readFileSync(FIXTURE, 'utf8');
fs.writeFileSync(path.join(day, 'rollout-2026-10-20T17-02-11-r1.jsonl'), meta(project));
fs.writeFileSync(path.join(day, 'rollout-2026-10-20T17-02-12-r2.jsonl'), meta(path.join(tmp, 'work', 'shop-admin')));
});
afterAll(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
test("Claude Code: lists this workspace's files inside the window, not the old one, never the sibling project", () => {
const r = sh(block);
expect(r.status, r.out).toBe(0);
expect(r.out).toContain('TRANSCRIPT_WINDOW: last 7 days');
expect(r.out).toContain('TRANSCRIPT_PROJECT: ' + project);
expect(r.out).toContain('/7c2f.jsonl');
expect(r.out).not.toContain('/old.jsonl');
expect(r.out).not.toContain('shop-admin');
expect(r.out).toMatch(/TRANSCRIPTS: .*\(1 files in the last 7 days\)/);
});
test('a wider window admits the old file; a missing folder and Capy both print none', () => {
expect(sh(block, { TRANSCRIPT_DAYS: '30' }).out).toContain('/old.jsonl');
const empty = path.join(tmp, 'work', 'fresh');
fs.mkdirSync(empty, { recursive: true });
spawnSync('git', ['init', '-q', empty], { timeout: 30_000 });
expect(sh(block, {}, empty).out).toMatch(/TRANSCRIPTS: none \(.*is missing/);
const capy = sh(block, { GSTACK_CAPY: '1' });
expect(capy.out).toContain(CAPY_NO_TRANSCRIPTS);
expect(capy.out).not.toContain('TRANSCRIPT: ');
});
test.skipIf(!fs.existsSync(codexRender))('Codex: lists only the rollout whose cwd is this workspace', () => {
const codexBlock = bashBlockAfter(fs.readFileSync(codexRender, 'utf8'), '### Step T1', DISCOVERY_MARKER);
const r = sh(codexBlock, { GSTACK_ROOT: ROOT, GSTACK_BIN: path.join(ROOT, 'bin') });
expect(r.status, r.out).toBe(0);
expect(r.out).toContain('rollout-2026-10-20T17-02-11-r1.jsonl');
expect(r.out).not.toContain('r2.jsonl');
expect(r.out).toMatch(/TRANSCRIPTS: .*\(1 files whose cwd is this workspace/);
});
});