mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* fix(land-and-deploy): never merge over red, pending or missing CI (#2995) /land-and-deploy gated only on required checks, so a repo with none (every GitHub Free private repo, any repo without branch protection) skipped the CI wait and could merge over red CI. The verdict now comes from bin/gstack-ci-gate, a tested helper bound to the head SHA: all checks by default, cancel/unknown buckets fail, NO_CHECKS needs explicit per-head approval, and both merge commands (including --auto) run only after a PASS for the same SHA they pin with --match-head-commit. Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> * fix(slug): give nested-group projects their own state bucket and verify checkpoint identity (#3003, #2901) gstack-slug kept only the last two remote path segments, so customer-a/product/repo and customer-b/product/repo shared one bucket and /context-restore could present another project's checkpoint. - One canonical-remote rule (bash + TS, golden vectors) used by all six resolvers: lowercased host, no userinfo/port/.git, Azure DevOps and Bitbucket Server normalized. 3+ segment remotes get <last-two>-<16 hex>; two-segment remotes (all of GitHub) are unchanged. - Versioned sticky cache (v2:) keeps cache hits git-free. - context-save stamps the credential-free remote and root; context-restore only treats this project's checkpoints as latest, reports foreign/unverified ones first, and treats a different root with the same remote (Conductor handoff) as information. - gstack-slug --adopt-legacy copies only files the user confirms. - question-preference-hook reads the same versioned cache, so never-ask preferences fire; unknown identity asks instead of auto-deciding. * fix(gbrain): brain context loads, managed sync works, memory sync stops duplicating (#2883, #2985, #2922) - brain-context-load called 'gbrain list_pages' (an MCP tool name), so every skill's brain context was 0 bytes. It now uses 'gbrain list' with gbrain's real filter flags, and prints one line when queries fail. - The code-stage sync always passes --no-pull (managed gbrain >= 0.51 refuses without it); a gbrain that rejects the flag fails with an upgrade message and is never retried without it. stderr is streamed with a bounded tail and quoted in failure summaries. - /sync-gbrain gains --sources (and GSTACK_MEMORY_INGEST_SOURCES); by default it skips types already synced through a federated curated source, so curated pages stop being re-duplicated. Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> * fix(codex,cso): clean Codex reviews stop failing; semgrep and trivy SARIF results import (#2769, #3011) - Codex review gate is PASS / FAIL / UNVERIFIED. Untagged output (what a clean 'codex review --base' prints) is UNVERIFIED, not FAIL, and never a false PASS; P2/P3-only output passes. - import-sarif resolves semgrep's undeclared %SRCROOT% and trivy's ROOTPATH (including git-URL targets) through explicit, captured mappings; declared bases must sit inside the source root or the run's checkout, URIs are percent-decoded, and rejected results are counted with rule, base and path instead of silently dropped. * fix: version strings are not IPs, failed update checks are not 'up to date', private security reporting (#2856, #2786, #2984) - The pre-push redact hook exempts a root VERSION file that is only a version string and the version token in CHANGELOG release headings; the engine allowlist is untouched. - gstack-update-check caches an unreadable remote as CHECK_FAILED with a short TTL instead of UP_TO_DATE; /gstack-upgrade never reports 'latest' after a failed check. - SECURITY.md points reporters at GitHub private vulnerability reporting. - pr-title-sync (pull_request_target, write token) pins actions/checkout to a reviewed SHA. * fix(autoplan): publication guard accepts real Claude Code journals (#3007, #3009, #2968, #2977, #2986, #3006) The guard required a session journal to start with a user message, but Claude Code starts it with SessionStart hook attachments (and a forked compacted session starts with a compact_boundary), so every phase was denied for anyone with a SessionStart hook. Windows path spellings (C:/, /c/, drive case) were rejected too. - Reader accepts the captured startup, compact, clear and fork shapes (claude 2.1.284 fixture), computes root uniqueness over every candidate, and returns typed reason codes instead of one string. - Hard denials (competing root, foreign cwd, sidechain, agent, cycle, malformed, identity) say what happened and how to run the reviews by hand; unflushed journals still retry. - A well-formed, stable journal whose root shape is unrecognized now warns ('phase publication was NOT verified') and continues, without overriding Claude Code's own permission check; no bypass switch. - Windows paths fold before validation, so '..' is still rejected. - Periodic drift canary runs the reader on fresh journals from the pinned claude. Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com> Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com> Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com> Co-authored-by: elmesery-dev <elmesery.dev@gmail.com> * v1.91.13.0: severe bug fix wave release notes, stale gbrain verbs in docs * test: seed the versioned slug cache in the memory-injection deny case (#2901) The preference hook now resolves the project bucket from gstack-slug's v2 slug-cache entry instead of basename(cwd); this fixture still wrote preferences under the basename bucket without the entry the skill preamble always writes first. * fix: full-suite follow-ups for the fix wave - gstack-slug's --help heredoc is over 512B, so it gets the BASH_COMPAT=50 pipe-deadlock guard like the other bins. - Refresh the codex/factory ship goldens for the plan-gates slug change. - The periodic lane gains the autoplan journal drift canary (107 paid files); raise the eval-slices cap to 25 so every slice still starts at once until its recorded duration lets the planner pack it. * fix(plan-eng-review): clearer Step 0 sequencing, write routes and section outputs The workflow judge scored plan-eng-review's instructions at actionability 3.67 (bar 4) on text unchanged from main. Resolve the ambiguities it named together, without changing any gate or output: Step 0 names the Read's order and keeps one STOP; the pending-call, read-only, forbidden-write and failed-save rules become one Write routes block; header/label limits and grid checks are concrete; Performance gets a checklist; each section has a stated output shape; Lake Score is defined. Same total length (parity cap). --------- Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com> Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com> Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com> Co-authored-by: elmesery-dev <elmesery.dev@gmail.com>
407 lines
18 KiB
TypeScript
407 lines
18 KiB
TypeScript
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { JUDGE_MS, CAPTURE_MS } from './helpers/eval-budgets';
|
|
import { runSkillTest } from './helpers/session-runner';
|
|
import {
|
|
ROOT, runId, evalsEnabled,
|
|
describeIfSelected, testConcurrentIfSelected,
|
|
copyDirSync, logCost, recordE2E,
|
|
createEvalCollector, finalizeEvalCollector,
|
|
} from './helpers/e2e-helpers';
|
|
import { spawnSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
const evalCollector = createEvalCollector('e2e-session-intelligence');
|
|
|
|
// --- Session Intelligence E2E ---
|
|
// Tests the core contract: timeline events flow in, context recovery flows out,
|
|
// /context-save + /context-restore round-trip.
|
|
|
|
describeIfSelected('Session Intelligence E2E', [
|
|
'timeline-event-flow', 'context-recovery-artifacts',
|
|
'context-save-writes-file', 'context-restore-loads-latest',
|
|
], () => {
|
|
let workDir: string;
|
|
let gstackHome: string;
|
|
let slug: string;
|
|
|
|
beforeAll(() => {
|
|
workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-session-intel-'));
|
|
gstackHome = path.join(workDir, '.gstack-home');
|
|
|
|
// Init git repo
|
|
const run = (cmd: string, args: string[]) =>
|
|
spawnSync(cmd, args, { cwd: workDir, stdio: 'pipe', timeout: 5000 });
|
|
run('git', ['init', '-b', 'main']);
|
|
run('git', ['config', 'user.email', 'test@test.com']);
|
|
run('git', ['config', 'user.name', 'Test']);
|
|
fs.writeFileSync(path.join(workDir, 'app.ts'), 'console.log("hello");\n');
|
|
run('git', ['add', '.']);
|
|
run('git', ['commit', '-m', 'initial']);
|
|
|
|
// Copy bin scripts needed by timeline and checkpoint
|
|
const binDir = path.join(workDir, 'bin');
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
for (const script of [
|
|
'gstack-timeline-log', 'gstack-timeline-read', 'gstack-slug', 'gstack-state-root.sh', 'gstack-remote-identity.sh',
|
|
'gstack-learnings-log', 'gstack-learnings-search',
|
|
]) {
|
|
const src = path.join(ROOT, 'bin', script);
|
|
if (fs.existsSync(src)) {
|
|
fs.copyFileSync(src, path.join(binDir, script));
|
|
fs.chmodSync(path.join(binDir, script), 0o755);
|
|
}
|
|
}
|
|
|
|
// Compute slug (same logic as gstack-slug without git remote)
|
|
slug = path.basename(workDir).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
});
|
|
|
|
afterAll(() => {
|
|
try { fs.rmSync(workDir, { recursive: true, force: true }); } catch {}
|
|
finalizeEvalCollector(evalCollector);
|
|
});
|
|
|
|
// --- Test 1: Timeline event flow ---
|
|
// Write a timeline event via gstack-timeline-log, read it back via gstack-timeline-read.
|
|
// This is the foundational data flow test: events go in, they come back out.
|
|
testConcurrentIfSelected('timeline-event-flow', async () => {
|
|
const projectDir = path.join(gstackHome, 'projects', slug);
|
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
|
|
// Write two events via the binary
|
|
const logBin = path.join(workDir, 'bin', 'gstack-timeline-log');
|
|
const readBin = path.join(workDir, 'bin', 'gstack-timeline-read');
|
|
const env = { ...process.env, GSTACK_HOME: gstackHome };
|
|
const opts = { cwd: workDir, env, stdio: 'pipe' as const, timeout: 10000 };
|
|
|
|
spawnSync(logBin, [JSON.stringify({
|
|
skill: 'review', event: 'started', branch: 'main', session: 'test-1',
|
|
})], opts); // timeout via opts
|
|
spawnSync(logBin, [JSON.stringify({
|
|
skill: 'review', event: 'completed', branch: 'main',
|
|
outcome: 'success', duration_s: 120, session: 'test-1',
|
|
})], opts); // timeout via opts
|
|
|
|
// Read via gstack-timeline-read
|
|
const readResult = spawnSync(readBin, ['--branch', 'main'], opts); // timeout via opts
|
|
const readOutput = readResult.stdout?.toString() || '';
|
|
|
|
// Verify timeline.jsonl exists and has content
|
|
const timelinePath = path.join(projectDir, 'timeline.jsonl');
|
|
expect(fs.existsSync(timelinePath)).toBe(true);
|
|
|
|
const lines = fs.readFileSync(timelinePath, 'utf-8').trim().split('\n');
|
|
expect(lines.length).toBe(2);
|
|
|
|
// Verify the events are valid JSON with expected fields
|
|
const event1 = JSON.parse(lines[0]);
|
|
expect(event1.skill).toBe('review');
|
|
expect(event1.event).toBe('started');
|
|
expect(event1.ts).toBeDefined();
|
|
|
|
const event2 = JSON.parse(lines[1]);
|
|
expect(event2.event).toBe('completed');
|
|
expect(event2.outcome).toBe('success');
|
|
|
|
// Verify gstack-timeline-read output includes the events
|
|
expect(readOutput).toContain('review');
|
|
|
|
recordE2E(evalCollector, 'timeline event flow', 'Session Intelligence E2E', {
|
|
output: readOutput,
|
|
exitReason: 'success',
|
|
duration: 0,
|
|
toolCalls: [],
|
|
browseErrors: [],
|
|
costEstimate: { inputChars: 0, outputChars: 0, estimatedTokens: 0, estimatedCost: 0, turnsUsed: 0 },
|
|
transcript: [],
|
|
model: 'direct',
|
|
firstResponseMs: 0,
|
|
maxInterTurnMs: 0,
|
|
}, { passed: true });
|
|
|
|
console.log(`Timeline flow: ${lines.length} events written, read output ${readOutput.length} chars`);
|
|
}, 30_000);
|
|
|
|
// --- Test 2: Context recovery with seeded artifacts ---
|
|
// Seed CEO plans and timeline events, then run a skill and verify the preamble
|
|
// outputs "RECENT ARTIFACTS" and "LAST_SESSION".
|
|
testConcurrentIfSelected('context-recovery-artifacts', async () => {
|
|
const projectDir = path.join(gstackHome, 'projects', slug);
|
|
fs.mkdirSync(path.join(projectDir, 'ceo-plans'), { recursive: true });
|
|
|
|
// Seed a CEO plan
|
|
fs.writeFileSync(
|
|
path.join(projectDir, 'ceo-plans', '2026-03-31-test-feature.md'),
|
|
'---\nstatus: ACTIVE\n---\n# CEO Plan: Test Feature\nThis is a test plan.\n',
|
|
);
|
|
|
|
// Seed timeline with a completed event on main branch
|
|
const timelineEntry = JSON.stringify({
|
|
ts: new Date().toISOString(),
|
|
skill: 'ship',
|
|
event: 'completed',
|
|
branch: 'main',
|
|
outcome: 'success',
|
|
duration_s: 60,
|
|
session: 'prior-session',
|
|
});
|
|
fs.writeFileSync(path.join(projectDir, 'timeline.jsonl'), timelineEntry + '\n');
|
|
|
|
// Copy the /learn skill (lightweight, tier-2 skill that runs context recovery)
|
|
copyDirSync(path.join(ROOT, 'learn'), path.join(workDir, 'learn'));
|
|
|
|
const result = await runSkillTest({
|
|
prompt: `Read the file learn/SKILL.md for instructions.
|
|
|
|
Run the context recovery check — the preamble should show recent artifacts.
|
|
|
|
IMPORTANT:
|
|
- Use GSTACK_HOME="${gstackHome}" as an environment variable when running bin scripts.
|
|
- The bin scripts are at ./bin/ (relative to this directory), not at ~/.claude/skills/gstack/bin/.
|
|
Replace any references to ~/.claude/skills/gstack/bin/ with ./bin/ when running commands.
|
|
- Do NOT use AskUserQuestion.
|
|
- Just run the preamble bash block and report what you see.
|
|
- Look for "RECENT ARTIFACTS" and "LAST_SESSION" in the output.
|
|
- In your final message, quote VERBATIM (copy exactly, do not paraphrase) any output lines containing "RECENT ARTIFACTS" or "LAST_SESSION".`,
|
|
workingDirectory: workDir,
|
|
maxTurns: 10,
|
|
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob'],
|
|
timeout: JUDGE_MS,
|
|
testName: 'context-recovery-artifacts',
|
|
runId,
|
|
});
|
|
|
|
logCost('context recovery', result);
|
|
|
|
const output = result.output.toLowerCase();
|
|
|
|
// The preamble should have found the seeded artifacts
|
|
const foundArtifacts = output.includes('recent artifacts') || output.includes('ceo-plans');
|
|
const foundLastSession = output.includes('last_session') || output.includes('ship');
|
|
const foundTimeline = output.includes('timeline') || output.includes('completed');
|
|
|
|
// At least the CEO plan or timeline should be visible
|
|
const foundCount = [foundArtifacts, foundLastSession, foundTimeline].filter(Boolean).length;
|
|
|
|
const exitOk = ['success', 'error_max_turns'].includes(result.exitReason);
|
|
|
|
recordE2E(evalCollector, 'context recovery', 'Session Intelligence E2E', result, {
|
|
passed: exitOk && foundCount >= 1,
|
|
});
|
|
|
|
expect(exitOk).toBe(true);
|
|
expect(foundCount).toBeGreaterThanOrEqual(1);
|
|
|
|
console.log(`Context recovery: artifacts=${foundArtifacts}, lastSession=${foundLastSession}, timeline=${foundTimeline}`);
|
|
}, CAPTURE_MS);
|
|
|
|
// --- Test 3: /context-save writes a file ---
|
|
// Hand-feed the save section of context-save/SKILL.md to claude -p and verify
|
|
// a file gets written to the project's checkpoints dir with valid frontmatter.
|
|
testConcurrentIfSelected('context-save-writes-file', async () => {
|
|
const projectDir = path.join(gstackHome, 'projects', slug);
|
|
fs.mkdirSync(path.join(projectDir, 'checkpoints'), { recursive: true });
|
|
|
|
// Copy the /context-save skill
|
|
copyDirSync(path.join(ROOT, 'context-save'), path.join(workDir, 'context-save'));
|
|
|
|
// Add a staged change so /context-save has something to capture
|
|
fs.writeFileSync(path.join(workDir, 'feature.ts'), 'export function newFeature() { return true; }\n');
|
|
spawnSync('git', ['add', 'feature.ts'], { cwd: workDir, stdio: 'pipe', timeout: 5000 });
|
|
|
|
// Extract the save section from the skill template (before the List section)
|
|
const full = fs.readFileSync(path.join(ROOT, 'context-save', 'SKILL.md'), 'utf-8');
|
|
const saveStart = full.indexOf('## Save flow');
|
|
const listStart = full.indexOf('## List flow');
|
|
const saveSection = full.slice(saveStart, listStart > saveStart ? listStart : undefined);
|
|
|
|
const result = await runSkillTest({
|
|
prompt: `You are testing the /context-save skill. Follow these instructions to save a context file.
|
|
|
|
${saveSection.slice(0, 2000)}
|
|
|
|
IMPORTANT:
|
|
- Use GSTACK_HOME="${gstackHome}" as an environment variable when running bin scripts.
|
|
- The bin scripts are at ./bin/ (relative to this directory), not at ~/.claude/skills/gstack/bin/.
|
|
Replace any references to ~/.claude/skills/gstack/bin/ with ./bin/ when running commands.
|
|
- Save the file to ${projectDir}/checkpoints/ with a filename like "20260401-test-context.md".
|
|
- Include YAML frontmatter with status, branch, and timestamp.
|
|
- Include a summary of what's being worked on (you can see from git status).
|
|
- Do NOT use AskUserQuestion.`,
|
|
workingDirectory: workDir,
|
|
maxTurns: 10,
|
|
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob'],
|
|
timeout: JUDGE_MS,
|
|
testName: 'context-save-writes-file',
|
|
runId,
|
|
});
|
|
|
|
logCost('context-save', result);
|
|
|
|
// Check that a context file was created
|
|
const checkpointDir = path.join(projectDir, 'checkpoints');
|
|
const files = fs.existsSync(checkpointDir)
|
|
? fs.readdirSync(checkpointDir).filter(f => f.endsWith('.md'))
|
|
: [];
|
|
|
|
const exitOk = ['success', 'error_max_turns'].includes(result.exitReason);
|
|
const fileCreated = files.length > 0;
|
|
|
|
let fileContent = '';
|
|
if (fileCreated) {
|
|
fileContent = fs.readFileSync(path.join(checkpointDir, files[0]), 'utf-8');
|
|
}
|
|
|
|
const hasYamlFrontmatter = fileContent.includes('---') && fileContent.includes('status:');
|
|
const hasBranch = fileContent.includes('branch:') || fileContent.includes('main');
|
|
|
|
recordE2E(evalCollector, 'context-save writes file', 'Session Intelligence E2E', result, {
|
|
passed: exitOk && fileCreated && hasYamlFrontmatter,
|
|
});
|
|
|
|
expect(exitOk).toBe(true);
|
|
expect(fileCreated).toBe(true);
|
|
expect(hasYamlFrontmatter).toBe(true);
|
|
|
|
console.log(`context-save: ${files.length} files created, YAML frontmatter: ${hasYamlFrontmatter}, branch: ${hasBranch}`);
|
|
}, CAPTURE_MS);
|
|
|
|
// --- Test 4: /context-restore loads the newest file across branches ---
|
|
// Seed two saved-context files with different YYYYMMDD-HHMMSS prefixes and
|
|
// different branches in their frontmatter. Hand-feed the restore section to
|
|
// claude -p. Verify the agent identifies the newer file (by filename prefix)
|
|
// and presents its content, regardless of the current branch.
|
|
testConcurrentIfSelected('context-restore-loads-latest', async () => {
|
|
// PRIVATE home for this test: the suite runs concurrently, and the shared
|
|
// gstackHome's checkpoints dir also receives the context-save test's
|
|
// freshly-written checkpoint (a 2026-08-29 filename prefix — always the
|
|
// "newest"). In CI, save completed before this test's agent listed the
|
|
// dir, so the agent CORRECTLY restored the sibling's checkpoint and the
|
|
// assertions failed; locally the ordering happened to run restore first.
|
|
// An isolated home makes the fixture set closed regardless of ordering
|
|
// (the agent may derive the dir from GSTACK_HOME/projects/<slug>, so the
|
|
// whole home moves, not just the checkpoint path we hand it).
|
|
const restoreHome = path.join(workDir, '.gstack-restore-home');
|
|
const projectDir = path.join(restoreHome, 'projects', slug);
|
|
const checkpointDir = path.join(projectDir, 'checkpoints');
|
|
fs.mkdirSync(checkpointDir, { recursive: true });
|
|
|
|
// Copy the /context-restore skill
|
|
copyDirSync(path.join(ROOT, 'context-restore'), path.join(workDir, 'context-restore'));
|
|
|
|
// Seed two files: older on branch-a (title "old-work"), newer on branch-b
|
|
// (title "newer-wintermute-work"). Current branch (main) matches neither.
|
|
const olderFile = path.join(checkpointDir, '20260101-120000-old-work.md');
|
|
const newerFile = path.join(checkpointDir, '20260202-130000-newer-wintermute-work.md');
|
|
fs.writeFileSync(olderFile, `---
|
|
status: in-progress
|
|
branch: branch-a
|
|
timestamp: 2026-01-01T12:00:00-07:00
|
|
---
|
|
|
|
## Working on: old work
|
|
|
|
### Summary
|
|
This is older work on branch-a.
|
|
|
|
### Remaining Work
|
|
1. Should NOT be loaded by default restore.
|
|
`);
|
|
fs.writeFileSync(newerFile, `---
|
|
status: in-progress
|
|
branch: branch-b
|
|
timestamp: 2026-02-02T13:00:00-07:00
|
|
---
|
|
|
|
## Working on: newer wintermute work
|
|
|
|
### Summary
|
|
This is the newest saved context. Cross-branch restore should load THIS file.
|
|
|
|
### Remaining Work
|
|
1. Finish the wintermute integration.
|
|
`);
|
|
|
|
// Deliberately scramble mtimes so filesystem mtime DISAGREES with filename
|
|
// prefix — this proves we're using filename ordering, not ls -1t.
|
|
const pastOlderMtime = Math.floor(Date.now() / 1000); // now (newest mtime)
|
|
const pastNewerMtime = pastOlderMtime - 60 * 60 * 24 * 30; // 30 days ago
|
|
fs.utimesSync(olderFile, pastOlderMtime, pastOlderMtime);
|
|
fs.utimesSync(newerFile, pastNewerMtime, pastNewerMtime);
|
|
|
|
// Extract the restore-flow section from the skill template
|
|
const full = fs.readFileSync(path.join(ROOT, 'context-restore', 'SKILL.md'), 'utf-8');
|
|
const restoreStart = full.indexOf('## Restore flow');
|
|
const importantStart = full.indexOf('## Important Rules', restoreStart);
|
|
const restoreSection = full.slice(restoreStart, importantStart > restoreStart ? importantStart : undefined);
|
|
|
|
const result = await runSkillTest({
|
|
prompt: `You are testing the /context-restore skill. Follow these instructions to restore the most recent saved context.
|
|
|
|
${restoreSection.slice(0, 2500)}
|
|
|
|
IMPORTANT:
|
|
- Use GSTACK_HOME="${restoreHome}" as an environment variable when running bin scripts.
|
|
- The bin scripts are at ./bin/ (relative to this directory), not at ~/.claude/skills/gstack/bin/.
|
|
- Look in ${checkpointDir} for saved context files.
|
|
- Current branch is "main" — do NOT filter by current branch. Load across all branches.
|
|
- The newest file by YYYYMMDD-HHMMSS prefix is the canonical "most recent". Filesystem mtime has been scrambled — do not use it.
|
|
- Do NOT use AskUserQuestion. Just present the content of the newest file.
|
|
- Your final message MUST end with these two lines (they are machine-checked — copy them exactly, do not paraphrase):
|
|
1. The newest file's "## Working on:" heading line, VERBATIM as it appears in that file.
|
|
2. A literal marker line: RESTORED: <filename of the newest file>`,
|
|
workingDirectory: workDir,
|
|
maxTurns: 8,
|
|
allowedTools: ['Bash', 'Read', 'Grep', 'Glob'],
|
|
timeout: JUDGE_MS,
|
|
testName: 'context-restore-loads-latest',
|
|
runId,
|
|
});
|
|
|
|
logCost('context-restore', result);
|
|
|
|
const output = result.output ?? '';
|
|
|
|
// Evidence-based checks. CI receipts showed the agent finding + reading the
|
|
// RIGHT file, then paraphrasing the final message ("the most recent context
|
|
// is from branch-b...") — exact-substring checks over stochastic prose
|
|
// flaked. Three signal classes, strongest first:
|
|
// 1. Machine-checkable output contract (prompt demands the verbatim
|
|
// "## Working on:" heading + a "RESTORED: <filename>" marker line).
|
|
// 2. Lenient content echo (legacy): distinctive newer-file phrases.
|
|
// 3. Tool-call corroboration: a tool call whose INPUT names the newer
|
|
// file. Corroboration only — if the agent also read the OLDER file,
|
|
// tool evidence is void and the final output must present the newer.
|
|
const newerFileName = '20260202-130000-newer-wintermute-work';
|
|
const olderFileName = '20260101-120000-old-work';
|
|
const newerMarker = new RegExp(`RESTORED:.*${newerFileName}`, 'i').test(output);
|
|
const olderMarker = new RegExp(`RESTORED:.*${olderFileName}`, 'i').test(output);
|
|
const newerContent = output.includes('newer wintermute work') || output.includes('wintermute integration');
|
|
const outputPresentsNewer = newerMarker || newerContent;
|
|
|
|
const toolInputs = result.toolCalls.map(tc => JSON.stringify(tc.input ?? {}));
|
|
const toolReadNewer = toolInputs.some(input => input.includes(newerFileName));
|
|
const toolReadOlder = toolInputs.some(input => input.includes(olderFileName));
|
|
|
|
// Presenting the OLDER file fails: an explicit RESTORED marker naming it,
|
|
// or older-file content with no newer-file presentation alongside.
|
|
const loadedOlder = olderMarker || (output.includes('old work') && !outputPresentsNewer);
|
|
// Tool evidence counts only when the older file was never read: a run that
|
|
// reads BOTH files must present the NEWER one in the final output to pass.
|
|
const loadedNewer = outputPresentsNewer || (toolReadNewer && !toolReadOlder);
|
|
const exitOk = ['success', 'error_max_turns'].includes(result.exitReason);
|
|
|
|
recordE2E(evalCollector, 'context-restore loads latest', 'Session Intelligence E2E', result, {
|
|
passed: exitOk && loadedNewer && !loadedOlder,
|
|
});
|
|
|
|
expect(exitOk).toBe(true);
|
|
expect(loadedNewer).toBe(true);
|
|
expect(loadedOlder).toBe(false);
|
|
|
|
console.log(`context-restore: loadedNewer=${loadedNewer} (marker=${newerMarker}, content=${newerContent}, toolNewer=${toolReadNewer}, toolOlder=${toolReadOlder}), loadedOlder=${loadedOlder}`);
|
|
}, CAPTURE_MS);
|
|
});
|