mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* 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>
180 lines
11 KiB
TypeScript
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/);
|
|
});
|
|
});
|