mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 03:30:28 +01:00
Merge oct7-small: context-restore, browse cookbook, credits (B1, B6, B7)
This commit is contained in:
commit
94eae93a3a
20 files changed
+488
-66
No files matched your search
@@ -71,6 +71,18 @@ describe('Aside driver contract ({{ASIDE_SETUP}})', () => {
|
||||
expect(section).toContain('listBrowserTabs()` output is private user data');
|
||||
});
|
||||
|
||||
test('signed-in tabs (#3063): offer only target-origin tabs, attach after one confirmation, no second sign-in loop', () => {
|
||||
const rules = setup.slice(setup.indexOf('### Rules for driving a real browser'));
|
||||
const rule = (n: number) => rules.match(new RegExp(`\\n${n}\\. \\*\\*[\\s\\S]*?(?=\\n\\d+\\. \\*\\*|\\n\\n)`))![0];
|
||||
expect(rule(1)).toContain("Before the first `openTab`, offer that list's tabs on the target origin (title and origin only)");
|
||||
expect(rule(1)).toContain('attach only after the user confirms one');
|
||||
expect(rule(4)).toContain('a second wall means the session is tab- or URL-bound: offer their tab (rule 1), never another sign-in');
|
||||
expect(rule(4)).not.toContain('the browser\'s cookies now apply');
|
||||
// Rule 6 no longer suggests an early `return`; rule 7 names the fast-[ok abort.
|
||||
expect(rule(6)).not.toContain('`return`');
|
||||
expect(rule(7)).toContain('a fast `[ok` without it is an abort');
|
||||
});
|
||||
|
||||
test('consent boundary: look freely, act on non-local targets only after one AskUserQuestion', () => {
|
||||
expect(section).toContain('Invocation is consent to LOOK, not to ACT');
|
||||
expect(section).toContain(ASIDE_LOCAL_HOST_RULE);
|
||||
@@ -100,6 +112,20 @@ describe('Aside driver contract ({{ASIDE_SETUP}})', () => {
|
||||
expect(section).toContain('use the Read tool on the copied file');
|
||||
});
|
||||
|
||||
test('cookbook names the silent failure modes of aside repl scripts (#3064)', () => {
|
||||
expect(cookbook).toContain('`evaluate` returns only JSON-serializable values');
|
||||
expect(cookbook).toContain('End such calls with `; return true` or return `JSON.stringify(...)`');
|
||||
expect(cookbook).toContain('No top-level `return`');
|
||||
expect(cookbook).toContain('Write abort paths as `if`/`else`');
|
||||
expect(cookbook).toContain('`pg.locator(sel).press("Enter")`');
|
||||
expect(cookbook).toContain('An empty DOM read after an action says something about the selector, not the app');
|
||||
expect(cookbook).toContain('read the state before clicking');
|
||||
// The attach recipe keeps the tab list private and never closes the user's tab.
|
||||
expect(cookbook).toContain('print only those tabs\' title and origin, never the rest');
|
||||
expect(cookbook).toContain('never `closeTab` it');
|
||||
expect(setup).not.toContain('Silent failures');
|
||||
});
|
||||
|
||||
test('cookbook uses only the verified Aside APIs', () => {
|
||||
expect(section).toContain('Page.addScriptToEvaluateOnNewDocument');
|
||||
expect(section).toContain('Emulation.setDeviceMetricsOverride');
|
||||
|
||||
@@ -47,6 +47,7 @@ function env(extra: Record<string, string> = {}): Record<string, string> {
|
||||
delete out.GSTACK_STATE_ROOT;
|
||||
delete out.GSTACK_STATE_DIR;
|
||||
delete out.CURRENT_BRANCH;
|
||||
delete out.CLAUDE_PROJECT_DIR;
|
||||
return { ...out, HOME: tmp, GSTACK_HOME: home, ...extra };
|
||||
}
|
||||
|
||||
@@ -66,12 +67,12 @@ function repo(name: string, origin?: string): string {
|
||||
}
|
||||
|
||||
/** The context-save flow: path block, the agent's Write, then the stamp block. */
|
||||
function save(cwd: string, title: string, timestamp: string, branch = 'main'): string {
|
||||
function save(cwd: string, title: string, timestamp: string, branch = 'main', extra: Record<string, string> = {}): string {
|
||||
const paths = sh(SAVE_PATH_BLOCK, cwd, { TITLE_RAW: title, TEST_TIMESTAMP: timestamp });
|
||||
expect(paths.status).toBe(0);
|
||||
const file = paths.stdout.match(/^FILE=(.+)$/m)![1];
|
||||
fs.writeFileSync(file, `---\nstatus: in-progress\nbranch: ${branch}\ntimestamp: ${timestamp}\n---\n\n## Working on: ${title}\n`);
|
||||
const stamp = sh(STAMP_BLOCK.replace('FILE="<the FILE path printed above>"', `FILE=${JSON.stringify(file)}`), cwd);
|
||||
const stamp = sh(STAMP_BLOCK.replace('FILE="<the FILE path printed above>"', `FILE=${JSON.stringify(file)}`), cwd, extra);
|
||||
expect(stamp.stdout).toContain('STAMPED');
|
||||
return file;
|
||||
}
|
||||
@@ -279,3 +280,159 @@ describe('gstack-slug --adopt-legacy', () => {
|
||||
expect(r.stdout).toContain("Nothing to adopt: this project's slug (acme-app) did not change.");
|
||||
});
|
||||
});
|
||||
|
||||
describe('context-restore offers newer checkpoints it used to skip (#3065)', () => {
|
||||
function git(cwd: string, ...args: string[]) {
|
||||
const r = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 30_000 });
|
||||
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
|
||||
}
|
||||
/** A main tree with one commit (fixture-local identity) so worktrees can be added. */
|
||||
function mainTree(name: string, origin: string): string {
|
||||
const dir = repo(name, origin);
|
||||
git(dir, 'config', 'user.name', 'Fixture');
|
||||
git(dir, 'config', 'user.email', 'fixture@example.com');
|
||||
git(dir, 'commit', '-q', '--allow-empty', '-m', 'init');
|
||||
return dir;
|
||||
}
|
||||
function worktree(main: string, at: string, branch: string): string {
|
||||
git(main, 'worktree', 'add', '-q', '-b', branch, at);
|
||||
return fs.realpathSync(at);
|
||||
}
|
||||
const pointers = (bucket: string) => {
|
||||
const dir = path.join(home, 'projects', bucket, 'checkpoint-pointers');
|
||||
return fs.existsSync(dir) ? fs.readdirSync(dir).map((f) => path.join(dir, f)) : [];
|
||||
};
|
||||
|
||||
describe('case 1: saved in a nested repo, restored in its non-git parent', () => {
|
||||
function layout() {
|
||||
const mono = path.join(tmp, 'mono');
|
||||
fs.mkdirSync(mono);
|
||||
const pkg = repo('mono/pkg', 'git@gitlab.example.com:grp/sub/repo.git');
|
||||
const stale = save(mono, 'old mono notes', '20260920-100000');
|
||||
return { mono, pkg, stale };
|
||||
}
|
||||
|
||||
test('with $CLAUDE_PROJECT_DIR set, the save leaves a pointer and restore offers the newer checkpoint', () => {
|
||||
const { mono, pkg, stale } = layout();
|
||||
const fresh = save(pkg, 'pkg feature', '20261001-100000', 'main', { CLAUDE_PROJECT_DIR: mono });
|
||||
expect(path.basename(path.dirname(path.dirname(fresh)))).toMatch(/^sub-repo-[0-9a-f]{16}$/);
|
||||
|
||||
const [ptr] = pointers('mono');
|
||||
expect(ptr).toBeDefined();
|
||||
expect(JSON.parse(fs.readFileSync(ptr, 'utf-8'))).toEqual({
|
||||
checkpoint: fresh,
|
||||
bucket: path.basename(path.dirname(path.dirname(fresh))),
|
||||
project_root: pkg,
|
||||
});
|
||||
|
||||
const r = restore(mono);
|
||||
expect(r.latest).toBe(stale);
|
||||
expect(r.lines).toContain(`POINTER ${fresh}`);
|
||||
});
|
||||
|
||||
test('without $CLAUDE_PROJECT_DIR no pointer is written, and the below-cwd scan still finds it', () => {
|
||||
const { mono, pkg, stale } = layout();
|
||||
const fresh = save(pkg, 'pkg feature', '20261001-100000');
|
||||
expect(pointers('mono')).toEqual([]);
|
||||
|
||||
const r = restore(mono);
|
||||
expect(r.latest).toBe(stale);
|
||||
expect(r.lines).toContain(`BELOW_CWD ${fresh}`);
|
||||
expect(r.out).not.toContain('POINTER');
|
||||
});
|
||||
|
||||
test('a save from the starting directory itself writes no pointer', () => {
|
||||
const { pkg } = layout();
|
||||
save(pkg, 'pkg feature', '20261001-100000', 'main', { CLAUDE_PROJECT_DIR: pkg });
|
||||
expect(fs.existsSync(path.join(home, 'projects'))).toBe(true);
|
||||
for (const bucket of fs.readdirSync(path.join(home, 'projects'))) expect(pointers(bucket)).toEqual([]);
|
||||
});
|
||||
|
||||
test('checkpoints not below the cwd are not offered, and the scan reads only the 200 newest', () => {
|
||||
const { mono, pkg } = layout();
|
||||
const sibling = repo('elsewhere', 'git@github.com:acme/elsewhere.git');
|
||||
const outside = save(sibling, 'sibling work', '20261002-100000');
|
||||
const below = save(pkg, 'pkg feature', '20261001-100000');
|
||||
expect(restore(mono).out).not.toContain(outside);
|
||||
expect(restore(mono).lines).toContain(`BELOW_CWD ${below}`);
|
||||
|
||||
// 200 newer checkpoints of another project push the below-cwd one out of the read window.
|
||||
const dir = path.dirname(outside);
|
||||
for (let i = 0; i < 200; i++) {
|
||||
fs.writeFileSync(path.join(dir, `20261003-${String(100000 + i)}-n${i}.md`), `---\nbranch: main\nproject_root: ${sibling}\n---\n`);
|
||||
}
|
||||
expect(restore(mono).out).not.toContain('BELOW_CWD');
|
||||
});
|
||||
|
||||
test('from $HOME, each project below shows only its newest checkpoint', () => {
|
||||
const { pkg } = layout();
|
||||
const older = save(pkg, 'first pass', '20261001-100000');
|
||||
const newer = save(pkg, 'second pass', '20261002-100000');
|
||||
const r = sh(RESTORE_BLOCK, tmp, { CURRENT_BRANCH: 'main' });
|
||||
expect(r.stdout).toContain(`BELOW_CWD ${newer}`);
|
||||
expect(r.stdout).not.toContain(older);
|
||||
});
|
||||
|
||||
test('inside a git repo the below-cwd scan does not run', () => {
|
||||
const outer = repo('outer');
|
||||
const inner = repo('outer/inner', 'git@github.com:acme/inner.git');
|
||||
const file = save(inner, 'inner work', '20261001-100000');
|
||||
expect(restore(outer).out).not.toContain(file);
|
||||
// A pointer from a session that started in the outer repo still surfaces it.
|
||||
save(inner, 'inner work 2', '20261002-100000', 'main', { CLAUDE_PROJECT_DIR: outer });
|
||||
expect(restore(outer).out).toMatch(/^POINTER .*20261002-100000-inner-work-2\.md$/m);
|
||||
});
|
||||
});
|
||||
|
||||
describe('case 2: another branch of the same repository holds a newer checkpoint for the task', () => {
|
||||
test('a ticket worktree below the main tree: restore shows the newer continuation, branch order unchanged', () => {
|
||||
const main = mainTree('app', 'git@github.com:acme/app.git');
|
||||
const plan = save(main, 'Login plan', '20261001-100000', 'master');
|
||||
const wt = worktree(main, path.join(main, '.worktrees', 't12'), 'feature/12-login');
|
||||
const impl = save(wt, 'Implement login', '20261003-100000', 'feature/12-login');
|
||||
expect(fs.readFileSync(impl, 'utf-8')).toContain(`\nworktree: ${wt}\n`);
|
||||
expect(fs.readFileSync(plan, 'utf-8')).not.toContain('worktree:');
|
||||
|
||||
const r = restore(main, 'master');
|
||||
expect(r.latest).toBe(plan);
|
||||
expect(r.lines).toContain(`NEWER_TASK ${impl}`);
|
||||
});
|
||||
|
||||
test('same normalized title, or the same ticket token, on a worktree outside the main tree', () => {
|
||||
const main = mainTree('app', 'git@github.com:acme/app.git');
|
||||
const plan = save(main, 'Auth refactor', '20261001-100000', 'master');
|
||||
const wt = worktree(main, path.join(tmp, 'app-auth'), 'auth-work');
|
||||
const cont = save(wt, '2026-10-03 auth-refactor!', '20261003-100000', 'auth-work');
|
||||
expect(restore(main, 'master').lines).toContain(`NEWER_TASK ${cont}`);
|
||||
|
||||
fs.rmSync(cont);
|
||||
fs.rmSync(plan);
|
||||
save(main, 'PROJ-42 wip', '20261001-110000', 'master');
|
||||
const ticket = save(wt, 'wip', '20261003-110000', 'feature/PROJ-42-x');
|
||||
expect(restore(main, 'master').lines).toContain(`NEWER_TASK ${ticket}`);
|
||||
});
|
||||
|
||||
test('negative controls: generic titles alone, older saves, unrelated sibling worktrees', () => {
|
||||
const main = mainTree('app', 'git@github.com:acme/app.git');
|
||||
const wtA = worktree(main, path.join(main, '.worktrees', 'a'), 'feat-a');
|
||||
const wtB = worktree(main, path.join(main, '.worktrees', 'b'), 'feat-b');
|
||||
const outside = worktree(main, path.join(tmp, 'app-x'), 'feat-x');
|
||||
|
||||
save(main, 'wip', '20261001-100000', 'master');
|
||||
save(outside, 'WIP', '20261003-100000', 'feat-x');
|
||||
expect(restore(main, 'master').out).not.toContain('NEWER_TASK');
|
||||
|
||||
// An older matching checkpoint is not a continuation.
|
||||
save(outside, 'Payments', '20260901-100000', 'feat-x');
|
||||
save(main, 'payments', '20261002-100000', 'master');
|
||||
expect(restore(main, 'master').out).not.toContain('NEWER_TASK');
|
||||
|
||||
// A sibling worktree's own save is never displaced by another worktree's newer one.
|
||||
const own = save(wtA, 'Search box', '20261004-100000', 'feat-a');
|
||||
save(wtB, 'Billing export', '20261005-100000', 'feat-b');
|
||||
const r = restore(wtA, 'feat-a');
|
||||
expect(r.latest).toBe(own);
|
||||
expect(r.out).not.toContain('NEWER_TASK');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -411,7 +411,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
// eureka log, the office-hours lookup and the taste-profile read; measured
|
||||
// 1.0834 (2026-09-30). + named design-direction defaults to avoid in the native
|
||||
// subagent prompt (s02 H4a); measured 1.0911 (2026-10-02).
|
||||
maxSizeRatio: 1.144, // + v1.91.19.0 wave: design --version probe (B4), taste-profile load (F3), verdict-form outside voice (INV-1), plus v1.91.17.0's round accounting and board/approval blocks (#3022) merged in; measured 100,446 / 90,375 = 1.1114 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 103,384 / 90,375 = 1.1439 (2026-10-06).
|
||||
maxSizeRatio: 1.147, // + v1.91.19.0 wave: design --version probe (B4), taste-profile load (F3), verdict-form outside voice (INV-1), plus v1.91.17.0's round accounting and board/approval blocks (#3022) merged in; measured 100,446 / 90,375 = 1.1114 (2026-10-03). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 103,384 / 90,375 = 1.1439 (2026-10-06). + Oct 7 wave B6: Aside rules 1, 4, 6 and 7 (offer a target-origin tab before openTab, a second sign-in wall means a tab- or URL-bound session, no early-return hint, fast [ok without the sentinel is an abort); measured 103,576 / 90,375 = 1.1461 (2026-10-07).
|
||||
},
|
||||
cso: {
|
||||
skill: 'cso',
|
||||
@@ -537,7 +537,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
},
|
||||
behavioral: 'prompt',
|
||||
maxSkeletonBytes: 74_500, // + Aside browser contract for Step 7 canary ({{ASIDE_SETUP}}); measured 73_523
|
||||
maxSizeRatio: 1.100, // + v1.81 Aside contract + gstack-browser fallback block; measured 1.077. + both untrusted-content marker formats in the browser fallback (W7f) + the readiness gate's project test command and live eval-store paths (s06 H6/H7); measured 1.1076 (2026-10-02). Re-measured 1.0943 (2026-10-03); cap = measured + 0.005 headroom.
|
||||
maxSizeRatio: 1.102, // + v1.81 Aside contract + gstack-browser fallback block; measured 1.077. + both untrusted-content marker formats in the browser fallback (W7f) + the readiness gate's project test command and live eval-store paths (s06 H6/H7); measured 1.1076 (2026-10-02). Re-measured 1.0943 (2026-10-03); cap = measured + 0.005 headroom. + Oct 7 wave B6: Aside rules 1, 4, 6 and 7 (offer a target-origin tab before openTab, a second sign-in wall means a tab- or URL-bound session, no early-return hint, fast [ok without the sentinel is an abort); measured 115,226 / 104,578 = 1.1018 (2026-10-07).
|
||||
minUnionBytes: 91_000, // Phase 4 wave 1; estimated union ~94.9KB
|
||||
mustContain: ['readiness', 'merge', 'canary', 'revert', 'staging'],
|
||||
},
|
||||
@@ -668,7 +668,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
},
|
||||
behavioral: 'prompt',
|
||||
maxSkeletonBytes: 63_500, // + v2.0 {{ASIDE_SETUP}}/{{BROWSE_FALLBACK}} (replaces the browse setup block); measured 61_253
|
||||
maxSizeRatio: 1.125, // + v1.81 Aside contract + gstack-browser fallback block (1.080 on v1.91.7.0) + the shared test value bar at 8a.5 ({{TEST_VALUE_BAR:qa}}); measured 1.094 + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.101 (2026-09-30) + v1.91.12.0 merge of #2999 (await scope/method Reads, capture --after checkpoints, browser-only empty evidence list) with #3002; measured 1.1028 (2026-10-01). + both untrusted-content marker formats in the browser fallback (W7f); measured 1.1035 (2026-10-02). Re-measured 1.1038 after the W8 probe-loop renames (2026-10-03); the cap already matches. + the annotate-every-capture and superseded rule before materialize (a ship-exploratory-late-input run published evidence that omitted older-snapshot captures); measured 1.1064 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 93,282 / 84,049 = 1.1099 (2026-10-04). + reliability follow-ups C2 (judge-named gaps after trimming the added wording: feasible 180 s Browser Quick, PROBE_DIR's source line, stop goes to the §4 report, written JSON keeps full card values; lane S's {user}/timing fields); measured 93,311 / 84,049 = 1.11020 (2026-10-04). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 94,486 / 84,049 = 1.1242 (2026-10-06).
|
||||
maxSizeRatio: 1.127, // + v1.81 Aside contract + gstack-browser fallback block (1.080 on v1.91.7.0) + the shared test value bar at 8a.5 ({{TEST_VALUE_BAR:qa}}); measured 1.094 + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.101 (2026-09-30) + v1.91.12.0 merge of #2999 (await scope/method Reads, capture --after checkpoints, browser-only empty evidence list) with #3002; measured 1.1028 (2026-10-01). + both untrusted-content marker formats in the browser fallback (W7f); measured 1.1035 (2026-10-02). Re-measured 1.1038 after the W8 probe-loop renames (2026-10-03); the cap already matches. + the annotate-every-capture and superseded rule before materialize (a ship-exploratory-late-input run published evidence that omitted older-snapshot captures); measured 1.1064 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 93,282 / 84,049 = 1.1099 (2026-10-04). + reliability follow-ups C2 (judge-named gaps after trimming the added wording: feasible 180 s Browser Quick, PROBE_DIR's source line, stop goes to the §4 report, written JSON keeps full card values; lane S's {user}/timing fields); measured 93,311 / 84,049 = 1.11020 (2026-10-04). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 94,486 / 84,049 = 1.1242 (2026-10-06). + Oct 7 wave B6: Aside rules 1, 4, 6 and 7 (offer a target-origin tab before openTab, a second sign-in wall means a tab- or URL-bound session, no early-return hint, fast [ok without the sentinel is an abort); measured 94,681 / 84,049 = 1.1265 (2026-10-07).
|
||||
minUnionBytes: 69_500, // measured union 70,385
|
||||
// 'aside repl' pins the Aside contract; '$B goto' pins the fallback block in the always-loaded skeleton.
|
||||
mustContain: ['bug', 'aside repl', '$B goto', 'fix', 'Health Score Rubric', 'regression'],
|
||||
@@ -705,7 +705,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
gateAfterStop: undefined,
|
||||
},
|
||||
behavioral: 'prompt',
|
||||
maxSkeletonBytes: 32_600, // + v2.0 {{ASIDE_SETUP}}/{{BROWSE_FALLBACK}} skeleton + the STOP-Read pointer; measured 29_986 + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); skeleton measured 32,532 (2026-10-06).
|
||||
maxSkeletonBytes: 34_100, // + v2.0 {{ASIDE_SETUP}}/{{BROWSE_FALLBACK}} skeleton + the STOP-Read pointer; measured 29_986 + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); skeleton measured 32,532 (2026-10-06). + Oct 7 wave B6: Aside rules 1, 4, 6 and 7 (offer a target-origin tab before openTab, a second sign-in wall means a tab- or URL-bound session, no early-return hint, fast [ok without the sentinel is an abort), and the cookbook's silent-failure list and signed-in-tab recipe (the rule details live here, not in every browsing skill); skeleton measured 34,047 (2026-10-07).
|
||||
minUnionBytes: 36_000, // skeleton + command-list section (14,240); measured union ~38.5 KB
|
||||
// 'BEGIN/END UNTRUSTED EXTERNAL' pins the untrusted-content warning in the
|
||||
// command reference; 'aside repl' pins the Aside contract in the skeleton.
|
||||
|
||||
Reference in new issue
Block a user