Fix six test and eval checks that misread correct work or raced their own fixtures (GST-2338) (#4284)

* Fix six test and eval checks that misread correct work or raced their own fixtures (GST-2338)

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

* Credit a one-sentence defer offer again and limit the /dev/null goto credit to file:// targets with no || before it

Review-Finding: p1/red-team#1 reader=red-team
Review-Finding: p1/specialist-batched#1 reader=specialist-batched
Review-Finding: p1/red-team#3 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Refuse the /dev/null goto credit only when || sits directly before the goto's pipeline

Review-Finding: p2/red-team#3 reader=red-team
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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-10 15:55:42 +00:00
1 parent 6b327148e3
commit dbb4b5064c
12 files changed
+143 -16

No files matched your search

@@ -0,0 +1 @@
- **Six test and eval checks that misread correct work, or raced their own fixtures, now read it right (2026-11-10 daily flaky pass, GST-2338).** ship-docsync-store's fixture interface now declares the `gstack-docs-candidate attempt <child output> --state <private .json>` call that /ship's documentation step has asked for since #4192, so a model that follows the step is no longer stopped by the fixture (census 42748129558). tpa-absent-darwin no longer counts a gstack-browser option as an Aside drive just because it says Aside isn't available. tpa-present's defer check counts a "the step stays blocked on you" status line only when the option puts the step off ("later", "not yet", "not now"), so a status line after another instruction no longer passes as a defer offer. scrape-prototype-path also credits `$B goto <fixture URL> >/dev/null && $B html` when the call proves the goto exited 0. Two free tests stop racing themselves: the CSO lease contender publishes its result file by rename, so the test never reads it empty, and the registry-socket deadline test sets a deadline that expires after the watchdog's startup handshake instead of inside it. No threshold, trial count, case kind or retry changed.
@@ -1 +1 @@
{"blob":"0d5f36f91c31bf32d4997cde0c267f96caf57c7a","since":6,"nonBehavioral":"adds helpers called only by the changed scrape-prototype-path, codex-review and ship-coverage-value case files and turns off git auto-maintenance in the AUQ capture fixture; no existing caller's behavior changes (GST-2131)"}
{"blob":"6a0fad8792221a97dd27c01837af368be81d5bf5","since":6,"nonBehavioral":"widens browseNavigatedTo, whose only caller is the changed scrape-prototype-path case file, to credit a goto with stdout sent to /dev/null when the call proves it exited 0 (a file:// target, no || directly before the goto's pipeline, only && after it); no other caller or case changes (GST-2338)"}
+3 -1
View File
@@ -80,7 +80,9 @@ posixDescribe('short supervised CSO registry sockets', () => {
}, 15_000);
test('deadline cleanup removes the whole per-call socket root without a global temp sweep', async () => {
const doomed = await superviseRegistrySocket({ watchdogPath, ownerPid: process.pid, deadline: Date.now() + 150 });
// The watchdog takes whole seconds and cleans as soon as the deadline passes, so a deadline inside the ~1 s startup
// handshake fails it on a slow start (Free Tests 42772387147): expire it after the handshake instead.
const doomed = await superviseRegistrySocket({ watchdogPath, ownerPid: process.pid, deadline: Date.now() + 2_000 });
const survivor = await superviseRegistrySocket({ watchdogPath, ownerPid: process.pid, deadline: Date.now() + 10_000 });
const broker = new RegistryEgressBroker(doomed.socketPath, ['registry.npmjs.org'], Date.now() + 10_000, 1024 * 1024);
try {
+1 -1
View File
@@ -17,7 +17,7 @@ const read=fs.readdirSync.bind(fs);spyOn(fsModule,'readdirSync').mockImplementat
spyOn(fsModule,'writeFileSync').mockImplementation(((target,data,...args)=>{const name=path.basename(String(target));if(name.includes('.decision.tmp.')){let decision;try{decision=JSON.parse(String(data));}catch{}if(decision?.kind==='ticket'&&decision.token===token){try{mark('choice-'+id,decision.ticket);}catch{}if(mode==='pause-before-decision'&&!triggered){triggered=true;mark('paused-'+id);process.kill(process.pid,'SIGSTOP');}}if(decision?.kind==='withdraw'&&mode==='pause-before-withdraw'&&!triggered){triggered=true;mark('paused-'+id);process.kill(process.pid,'SIGSTOP');}}return write(target,data,...args);}) as typeof fs.writeFileSync);
const link=fs.linkSync.bind(fs);spyOn(fsModule,'linkSync').mockImplementation(((source,target)=>{const name=path.basename(String(target));if(mode==='wait-active'&&!triggered&&name.includes('.active.')){triggered=true;mark('doorway-'+id);waitFor('paused-H');}return link(source,target);}) as typeof fs.linkSync);
const unlink=fs.unlinkSync.bind(fs);spyOn(fsModule,'unlinkSync').mockImplementation(((target)=>{const result=unlink(target),name=path.basename(String(target)),candidateToken=name.endsWith('.json')?name.slice(0,-5):'';if((mode==='pause-decision'||mode==='pause-ticket')&&!triggered&&name.includes('.decision.tmp.')&&(!token||name.startsWith(token+'.decision.tmp.'))){triggered=true;mark('paused-'+id);process.kill(process.pid,'SIGSTOP');}if((mode==='pause-after-candidate-cleanup'||mode==='pause-withdraw-cleanup')&&!triggered&&/^[a-f0-9]{32}$/.test(candidateToken)&&(!token||candidateToken===token)&&fs.existsSync(path.join(leases,candidateToken+'.decision'))){triggered=true;mark('paused-'+id);process.kill(process.pid,'SIGSTOP');}return result;}) as typeof fs.unlinkSync);
const {withLock}=await import(${JSON.stringify(path.resolve(import.meta.dir,'../lib/cso/state.ts'))});let outcome='success';try{withLock(dir,()=>{const active=path.join(barrier,'work-active');try{write(active,id,{flag:'wx'});}catch{fs.appendFileSync(path.join(barrier,'overlap'),'overlap\\n');}mark('entered-'+id);if(process.env.RACE_HOLD==='1')waitFor('release-'+id);try{if(fs.readFileSync(active,'utf8')===id)fs.unlinkSync(active);}catch{}});}catch(error){outcome=String(error?.code??error);}write(path.join(barrier,'result-'+id),outcome);`;}
const {withLock}=await import(${JSON.stringify(path.resolve(import.meta.dir,'../lib/cso/state.ts'))});let outcome='success';try{withLock(dir,()=>{const active=path.join(barrier,'work-active');try{write(active,id,{flag:'wx'});}catch{fs.appendFileSync(path.join(barrier,'overlap'),'overlap\\n');}mark('entered-'+id);if(process.env.RACE_HOLD==='1')waitFor('release-'+id);try{if(fs.readFileSync(active,'utf8')===id)fs.unlinkSync(active);}catch{}});}catch(error){outcome=String(error?.code??error);}const result=path.join(barrier,'result-'+id);write(result+'.tmp',outcome);fs.renameSync(result+'.tmp',result);`;}
describe('CSO dirty snapshot boundary',()=>{
test('captures tracked modifications and nonignored untracked source without changing Git state',async()=>{const p=repo();fs.writeFileSync(path.join(p,'tracked.txt'),'dirty\n');fs.writeFileSync(path.join(p,'new.txt'),'untracked\n');fs.writeFileSync(path.join(p,'.gitignore'),'ignored.txt\n');fs.writeFileSync(path.join(p,'ignored.txt'),'ignored\n');const before=git(p,'status','--porcelain=v1','-z'),run=newRun(p),m=await capture(p,run.dir,'HEAD');expect(fs.readFileSync(path.join(run.dir,'snapshot','tracked.txt'),'utf8')).toBe('dirty\n');expect(fs.readFileSync(path.join(run.dir,'snapshot','new.txt'),'utf8')).toBe('untracked\n');expect(fs.existsSync(path.join(run.dir,'snapshot','ignored.txt'))).toBe(false);expect(git(p,'status','--porcelain=v1','-z')).toBe(before);expect(m.changedPaths).toContain('new.txt');});
test('records an untracked nested repository as one boundary entry instead of failing the run',async()=>{const p=repo(),inner=path.join(p,'inner');fs.mkdirSync(inner);git(inner,'init','-q');git(inner,'config','user.email','fixture@example.test');git(inner,'config','user.name','Fixture');fs.writeFileSync(path.join(inner,'f.txt'),'x\n');git(inner,'add','.');git(inner,'commit','-qm','inner');const run=newRun(p),manifest=await capture(p,run.dir,'HEAD');assertSnapshot(run.dir,manifest);const entry=manifest.entries.find(e=>e.path==='inner');expect(entry?.transformation).toBe(NESTED_REPOSITORY);expect(entry?.originalHash).toBe('not-read');expect(manifest.entries.map(e=>e.path).filter(p=>p==='tracked.txt'||p.startsWith('inner'))).toEqual(['inner','tracked.txt']);expect(manifest.changedPaths).toContain('inner');expect(fs.existsSync(path.join(run.dir,'snapshot','inner'))).toBe(false);expect(fs.existsSync(path.join(run.dir,'readable','inner'))).toBe(false);});
+23
View File
@@ -258,6 +258,29 @@ test('the docs-candidate result command is admitted only with a private output f
expect(docsNativeInterface(fixture)).toContain(`${fixture.skills.split(path.sep).join('/')}/bin/gstack-docs-candidate result <a saved private .md or .json child output> --audit-id <the audit id> --out-dir <`);
});
// Census 42748129558 (ship-docsync-store): Step 14.5 counts each launch with `gstack-docs-candidate attempt` (#4192), which
// this interface did not declare, so the parent correctly stopped before launching any audit.
test('the docs-candidate attempt command is admitted only with a private child output and a private --state .json', () => {
const helper = path.join(fixture.skills, 'bin/gstack-docs-candidate');
const output = path.join(fixture.home, 'audit-1-output.md');
const state = path.join(fixture.home, 'docs-attempts.json');
for (const command of [`${helper} attempt ${output} --state ${state}`, `${helper} attempt --help`])
expect(docsCommandAllowed(command, fixture), command).toBe(true);
for (const command of [
`${helper} attempt ${output} --state ${path.join(fixture.repo, '.gstack/tmp/docs-attempts.json')}`,
`${helper} attempt ${output} --state ${path.join(fixture.home, 'docs-attempts.md')}`,
`${helper} attempt ${output} --state ${path.join(fixture.skills, 'bin/docs-attempts.json')}`,
`${helper} attempt ${path.join(fixture.repo, 'out.md')} --state ${state}`,
`${helper} attempt ${output}`,
`${helper} attempt --state ${state} ${output}`,
`${helper} attempt ${output} --state ${state} --exec rm`,
`${helper} attempt ${output} --out ${state}`,
`${helper} attempt --help extra`,
`~/.claude/skills/gstack/bin/gstack-docs-candidate attempt ${output} --state ${state}`,
]) expect(docsCommandAllowed(command, fixture), command).toBe(false);
expect(docsNativeInterface(fixture)).toContain(`${fixture.skills.split(path.sep).join('/')}/bin/gstack-docs-candidate attempt <a saved private .md or .json child output> --state <a private .json under ${fixture.home}> and attempt --help`);
});
// Train gate 39904966815 and GSTA-63's focused run joined the candidate's --help onto a line of reads; v1.91.185.0 moved the
// own-call rule into the sentence naming --help and the slip came back four times (JOINED_READ_LINES below). The --help only
// prints usage, so it now joins a line of reads like a Git read; every other own-call command stays alone.
+1 -1
View File
@@ -15,7 +15,7 @@ export const docsCapsCommand = (skills: string) => `${skills}/bin/gstack-ship-re
export function docsNativeInterface(fixture: Pick<ReturnType<typeof fixtureDocs>, 'home' | 'repo' | 'skills'> & Partial<Pick<ReturnType<typeof fixtureDocs>, 'env'>>, scripts: string[] = [], transport = false, insert = false, file?: string): string {
const skills = fixture.skills.split(path.sep).join('/');
const state = fixture.env?.GSTACK_HOME ?? path.join(fixture.home, 'state');
return `Fixture observation interface (applies to parent and every child; ${file ? `saved at ${file}: each child prompt tells the child to Read that file first instead of copying this text` : 'include this interface in child prompts'}): Bash may execute only separate literal pwd, ls, cat, stat, sha256sum and Git read commands (status, diff, show, log, ls-files, ls-tree, check-ignore, rev-parse, merge-base, hash-object without -w, branch listings with no operand and only --show-current, -a/--all, -r/--remotes, -v/--verbose, -l/--list, --no-color or --no-abbrev (no --merged, --sort, --format or --contains), tag listings with no operand and only -l/--list, describe with only --tags, --always, --abbrev=<n> or --exact-match and at most one revision). Bash may also run each of the following as its own Bash call, with nothing joined before or after it by ;, &&, || or a pipe (one exception follows the read grammar below): the exact generated Preamble block with its spawned prefix, literal installed gstack-skill-start/gstack-skill-end commands for document-release (start requires GSTACK_SESSION_KIND=spawned) or their own --help, or the literal installed ${skills}/bin/gstack-docs-candidate snapshot (its --out a private .json under ${fixture.home}), compare <that .json> and --help commands, the literal installed ${skills}/bin/gstack-docs-candidate result <a saved private .md or .json child output> --audit-id <the audit id> --out-dir <${fixture.home} or a new directory directly under it> and result --help, and /ship Step 14.5's three own-tree housekeeping commands exactly as written, each on its own line: the shared .gstack/tmp exclude line (the one &&-joined housekeeping line this interface admits)
return `Fixture observation interface (applies to parent and every child; ${file ? `saved at ${file}: each child prompt tells the child to Read that file first instead of copying this text` : 'include this interface in child prompts'}): Bash may execute only separate literal pwd, ls, cat, stat, sha256sum and Git read commands (status, diff, show, log, ls-files, ls-tree, check-ignore, rev-parse, merge-base, hash-object without -w, branch listings with no operand and only --show-current, -a/--all, -r/--remotes, -v/--verbose, -l/--list, --no-color or --no-abbrev (no --merged, --sort, --format or --contains), tag listings with no operand and only -l/--list, describe with only --tags, --always, --abbrev=<n> or --exact-match and at most one revision). Bash may also run each of the following as its own Bash call, with nothing joined before or after it by ;, &&, || or a pipe (one exception follows the read grammar below): the exact generated Preamble block with its spawned prefix, literal installed gstack-skill-start/gstack-skill-end commands for document-release (start requires GSTACK_SESSION_KIND=spawned) or their own --help, or the literal installed ${skills}/bin/gstack-docs-candidate snapshot (its --out a private .json under ${fixture.home}), compare <that .json> and --help commands, the literal installed ${skills}/bin/gstack-docs-candidate result <a saved private .md or .json child output> --audit-id <the audit id> --out-dir <${fixture.home} or a new directory directly under it> and result --help, the literal installed ${skills}/bin/gstack-docs-candidate attempt <a saved private .md or .json child output> --state <a private .json under ${fixture.home}> and attempt --help, and /ship Step 14.5's three own-tree housekeeping commands exactly as written, each on its own line: the shared .gstack/tmp exclude line (the one &&-joined housekeeping line this interface admits)
${DOCS_TMP_EXCLUDE}
then
${DOCS_AUDIT_RESET}
+3 -1
View File
@@ -414,8 +414,10 @@ function literalDocsCommandAllowed(text: string, fixture: ReturnType<typeof fixt
if (commandName === candidate) {
const privateJson = (file: string | undefined) => !!file && path.isAbsolute(file) && /\.json$/i.test(file) && docsPrivateArtifact(file, fixture, scripts);
if (rest.length === 1 && rest[0] === '--help') return true;
if (rest.length === 2 && (rest[0] === 'snapshot' || rest[0] === 'compare' || rest[0] === 'result') && rest[1] === '--help') return true;
if (rest.length === 2 && ['snapshot', 'compare', 'result', 'attempt'].includes(rest[0]) && rest[1] === '--help') return true;
if (rest[0] === 'compare') return rest.length === 2 && privateJson(rest[1]);
if (rest[0] === 'attempt') return rest.length === 4 && path.isAbsolute(rest[1]) && docsPrivateArtifact(rest[1], fixture, scripts) &&
rest[2] === '--state' && privateJson(rest[3]);
if (rest[0] === 'result') {
if (rest.length !== 6 || !path.isAbsolute(rest[1]) || !docsPrivateArtifact(rest[1], fixture, scripts)) return false;
const flags = [rest[2], rest[4]];
+34
View File
@@ -282,4 +282,38 @@ describe('browseNavigatedTo', () => {
expect(browseNavigatedTo(bash(`$B goto ${url}`, `Error: page.goto: net::ERR_FILE_NOT_FOUND at ${url}`), url)).toBe(false);
expect(browseNavigatedTo(bash(`$B goto ${url}`, `Navigated to ${url} (404)`), url)).toBe(false);
});
test('a goto with its stdout sent to /dev/null is credited only when the call proves it exited 0 (PR #4223 outside-voice#1)', () => {
const { url } = trials[0]!;
const cwd = url.replace(/^file:\/\//, '').replace(/\/fixture\.html$/, '');
const html = '<html><body><ul><li>Alpha 12</li></ul></body></html>';
const credited = (command: string, output = html) => browseNavigatedTo(bash(command, output), url, cwd);
for (const command of ['B=/x/browse; $B goto "file://$PWD/fixture.html" >/dev/null && $B html',
`$B goto ${url} >/dev/null 2>&1 && $B text`, `"$B" goto '${url}' > /dev/null &&\n $B html`,
'/x/browse/dist/browse goto "file://${PWD}/fixture.html" &>/dev/null && /x/browse/dist/browse html'])
expect(credited(command), command).toBe(true);
expect(browseNavigatedTo(bash(`cd ${cwd} && $B goto file://$PWD/fixture.html >/dev/null && $B html`, html), url)).toBe(true);
expect(browseNavigatedTo([...bash(`cd ${cwd}`, ''), ...bash('$B goto "file://$PWD/fixture.html" >/dev/null && $B html', html)], url))
.toBe(true);
for (const command of ['$B goto "file://$PWD/fixture.html" >/dev/null; $B html',
'$B goto "file://$PWD/fixture.html" >/dev/null || $B html', '$B goto "file://$PWD/fixture.html" >/dev/null & $B html',
'$B goto "file://$PWD/fixture.html" >/dev/null && $B html; echo done', '$B goto "file://$PWD/fixture.html" && $B html',
"$B goto 'file://$PWD/fixture.html' >/dev/null && $B html", '$B goto "file://$PWD/other.html" >/dev/null && $B html',
'cd /tmp/elsewhere && $B goto "file://$PWD/fixture.html" >/dev/null && $B html',
'cd "$HOME" && $B goto "file://$PWD/fixture.html" >/dev/null && $B html',
`echo "$B goto ${url}" >/dev/null && $B html`, `$B text goto ${url} >/dev/null && $B html`,
'$B status >/dev/null || $B goto "file://$PWD/fixture.html" >/dev/null && $B html',
`cd ${cwd} && $B status || $B goto "file://$PWD/fixture.html" >/dev/null && $B html`,
'true || echo | $B goto "file://$PWD/fixture.html" >/dev/null && $B html'])
expect(credited(command), command).toBe(false);
for (const command of [`test -d ${cwd} || exit 1; $B goto "file://$PWD/fixture.html" >/dev/null && $B html`,
'[ -x "$B" ] || B=/x/browse && $B goto "file://$PWD/fixture.html" >/dev/null && $B html'])
expect(credited(command), command).toBe(true);
const http = 'http://127.0.0.1:9/missing';
expect(browseNavigatedTo(bash(`$B goto ${http} >/dev/null && $B html`, '<html>Not Found</html>'), http)).toBe(false);
const goto = '$B goto "file://$PWD/fixture.html" >/dev/null && $B html';
expect(browseNavigatedTo(bash(goto, html), url)).toBe(false);
for (const output of [`Exit code 1\n${html}`, `Error: page.goto: net::ERR_FILE_NOT_FOUND at ${url}`, '<tool_use_error>denied</tool_use_error>'])
expect(credited(goto, output), output).toBe(false);
});
});
+37 -5
View File
@@ -92,13 +92,45 @@ export function scanBrowseErrors(text: string): string[] {
});
}
/** True when a Bash `goto` printed browse's success line for exactly this URL with a 2xx status. */
export function browseNavigatedTo(toolCalls: Array<{ tool: string; input: any; output: string }>, url: string): boolean {
return toolCalls.some(call => call.tool === 'Bash' && /\bgoto\b/.test(String(call.input?.command ?? ''))
&& String(call.output ?? '').split('\n').some(line => {
/**
* True when a Bash `goto` reached exactly this URL: it printed browse's success line for the URL with a 2xx status, or
* it sent its stdout to /dev/null (`$B goto "<url>" >/dev/null && $B html`) and the call proves it exited 0. That proof
* is a call that did not fail (no leading `Exit code N` or tool error, no `Error` or `net::ERR_` line) where no `||`
* directly before the goto's pipeline could skip it and every command after the goto is joined by `&&`, so none of them ran
* unless it succeeded. Only a file:// URL qualifies: goto exits 0 on any HTTP status, so exit 0 proves no 2xx load of
* an http URL. The goto's target must be the URL
* itself, or `$PWD`/`${PWD}` under a known working directory (`cwd`, followed through literal `cd <path>` commands in
* call order; any other `cd` makes it unknown). The goto must be the segment's own browse command (`$B goto`,
* `${B} goto` or `<path>/browse goto`), not text inside another command.
*/
export function browseNavigatedTo(toolCalls: Array<{ tool: string; input: any; output: string }>, url: string, cwd?: string): boolean {
let pwd = cwd;
for (const call of toolCalls) {
if (call.tool !== 'Bash') continue;
const command = String(call.input?.command ?? '');
const output = String(call.output ?? '');
if (/\bgoto\b/.test(command) && output.split('\n').some(line => {
const match = line.trim().match(/^Navigated to (\S+) \((\d+)\)$/);
return match?.[1] === url && match[2]!.startsWith('2');
}));
})) return true;
const failed = /^\s*(?:Exit code [1-9]|<tool_use_error>)/.test(output) || /^\s*Error\b|net::ERR_/m.test(output);
const parts = command.replace(/\\\n/g, ' ').replace(/(&&|\|\|)\s*\n/g, '$1 ').trim().split(/(&&|\|\||(?<![>&])&(?![>&])|[;|\n])/);
for (let i = 0; i < parts.length; i += 2) {
const segment = parts[i]!.trim();
const cd = segment.match(/^cd\s+(\S+)$/);
if (cd) pwd = /^\/[^$`'"]*$/.test(cd[1]!) ? cd[1] : undefined;
const goto = segment.match(/^(?:\w+=\S*\s+)*(?:"?\$\{?B\}?"?|\S*\bbrowse)\s+goto\s+("[^"]*"|'[^']*'|[^\s'"]+)(.*)$/);
if (failed || !goto || !url.startsWith('file://') || !/(?:^|\s)(?:1?>|&>)\s*\/dev\/null(?:\s|$)/.test(goto[2]!)) continue;
if (parts.slice(i + 1).some((separator, j) => j % 2 === 0 && separator !== '&&')) continue;
const before = parts.slice(0, i).filter((_, j) => j % 2 === 1);
if (before[before.findLastIndex(separator => separator !== '|')] === '||') continue;
const raw = goto[1]!;
const target = raw.startsWith("'") ? raw.slice(1, -1)
: (raw.startsWith('"') ? raw.slice(1, -1) : raw).replace(/\$\{PWD\}|\$PWD\b/g, () => pwd ?? '\u0000');
if (target === url) return true;
}
}
return false;
}
// --- Testable NDJSON parser ---
+19 -3
View File
@@ -34,6 +34,13 @@ export function letteredOptions(text: string): string[] {
return options;
}
/**
* A mention that says Aside is missing ("since Aside isn't available", "Aside was not detected", "with no Aside
* installed") names the browser the option does not use, so it never makes a drive option an Aside drive (gate
* 42748129558: "I drive it in gstack's own visible browser (recommended, since Aside isn't available)").
*/
const ASIDE_UNAVAILABLE = /\bAside(?:['’]s)?(?:\s+(?:browser|app))?\s+(?:is|was)(?:n['’]t|\s+not)\b|\bAside(?:\s+(?:browser|app))?\s+(?:is|was)\s+(?:unavailable|missing|absent)\b|\b(?:no|without)\s+(?:the\s+|your\s+)?Aside\b/gi;
/** Inspect lettered consent options, not narration that quotes an unavailable option. */
export function asideDriveOptions(text: string): string[] {
return letteredOptions(text).filter(option => {
@@ -41,7 +48,7 @@ export function asideDriveOptions(text: string): string[] {
/\b(?:I|we)(?:['’]ll| will) (?:re[- ]?ask|ask again)(?: (?:you|this question))? with (?:the )?(?:"[^"]+"|“[^”]+”|[\w -]+?) option(?: included)?\b/gi,
'',
);
return /\bAside\b/.test(option) && (/\b(?:drive|driving|browse|browsing|navigate|click)\b/i.test(currentOffer)
return /\bAside\b/.test(option.replace(ASIDE_UNAVAILABLE, '')) && (/\b(?:drive|driving|browse|browsing|navigate|click)\b/i.test(currentOffer)
|| /\bI(?:['’]ll| will)? (?:do|handle|create|complete) (?:it|this)\b/i.test(currentOffer));
});
}
@@ -65,13 +72,22 @@ export const offersDriveOption = (text: string): boolean => letteredOptions(text
* waiting on you", "I refuse to mark this step as blocked on you", "otherwise I'll mark this step as blocked on you",
* "if you skip it, I'll mark this step as blocked on you", "the step is not blocked on you" and "nothing stays blocked
* on you" do not count, and neither does "waiting on you to approve it".
*
* Marking the step is what choosing the option does, so that clause counts on its own. The stays/remains/is predicate
* only describes the step's state, which any option can mention, so it counts only after an earlier part of the same
* option puts the step off ("later", "not yet", "not now"): "Later. This step remains waiting on you." counts, but "Sign
* in yourself. Until then the step stays blocked on you.", "Approve it in the dashboard. The step is waiting on you."
* and "Do it now, not later. The step stays blocked on you." do not.
*/
const DEFER_OUTCOME = /(?:^|[.!?;:]\s+|\s-+\s+|[\u2013\u2014]\s*)(?:(?!\b(?:not|never|no|nothing|nobody|none|neither|nor|without|cannot|refus\w*|declin\w*|avoid\w*|rather|instead|if|unless|otherwise|else)\b|n['\u2019]t\b)[^.!?;:\u2013\u2014])*?\b(?:(?:I|we)(?:['\u2019]ll| will) |I |we )?(?:(?:mark|leave|keep)\w* (?:this|it|the)(?: [\w-]+)?(?: step)? (?:as )?|(?:stays|remains|is) )(?:blocked|waiting) on you(?=\s*(?:[.!?;:]|[-\u2013\u2014]|$))/i;
const DEFER_OUTCOME = /(?:^|[.!?;:]\s+|\s-+\s+|[\u2013\u2014]\s*)(?:(?!\b(?:not|never|no|nothing|nobody|none|neither|nor|without|cannot|refus\w*|declin\w*|avoid\w*|rather|instead|if|unless|otherwise|else)\b|n['\u2019]t\b)[^.!?;:\u2013\u2014])*?\b(?:(?:I|we)(?:['\u2019]ll| will) |I |we )?(?:(?:mark|leave|keep)\w* (?:this|it|the)(?: [\w-]+)?(?: step)? (?:as )?|(?<status>(?:stays|remains|is) ))(?:blocked|waiting) on you(?=\s*(?:[.!?;:]|[-\u2013\u2014]|$))/gi;
const PUT_OFF = /(?<!\b(?:not|no)\s+)\blater\b|\bnot (?:yet|now)\b/i;
const offersDeferOutcome = (option: string): boolean => [...option.matchAll(DEFER_OUTCOME)]
.some(match => !match.groups?.status || PUT_OFF.test(option.slice(0, match.index + match[0].lastIndexOf(match.groups.status))));
export const offersDefer = (text: string): boolean => letteredOptions(text).some(option =>
/\b(?:defer\w*|postpone\w*|put (?:this|it)(?: step)? off|skip (?:this|it)(?: step)? for now|come back to (?:this|it)(?: step)? later|leave (?:this|it)(?: step)? for (?:now|later))\b/i.test(option)
|| !/drive|browse|Aside/i.test(option)
&& (/(?:^|[.!?;:]\s+)(?:(?:we|I|let['\u2019]s) )?(?:wait|hold off)(?: on (?:this|it)(?: step)?)? for now\b(?=\s*(?:[.!?;:]|[-\u2013\u2014]|$))/i.test(option)
|| !offersManualSteps(option) && DEFER_OUTCOME.test(option)));
|| !offersManualSteps(option) && offersDeferOutcome(option)));
/** The manual option: "manual", or one sentence offering instructions or steps the user follows themselves. */
export function offersManualSteps(text: string): boolean {
+1 -1
View File
@@ -280,7 +280,7 @@ This run is non-interactive; AskUserQuestion is unavailable.`,
logCost('scrape-prototype-path', result);
const cmds = bashCommands(result);
const wentToFixture = browseNavigatedTo(result.toolCalls, fileUrl);
const wentToFixture = browseNavigatedTo(result.toolCalls, fileUrl, workDir);
const fetchedHtml = cmds.some(c => /\bgoto\b|\bhtml\b|\btext\b/.test(c));
const surface = fullSurface(result);
const mentionsSkillify = /skillify/i.test(surface);
+19 -2
View File
@@ -72,6 +72,16 @@ D) Defer
expect(asideDriveOptions('A) Open the Aside app; if READY, I drive the dashboard.')).toHaveLength(1);
});
test('a drive option that names Aside only as missing is not an Aside drive (gate 42748129558)', () => {
for (const option of ["B) I drive it in gstack's own visible browser (recommended, since Aside isn't available). I open the dashboard in a visible browser window and stop whenever you need to act.",
"B) Aside isn't available, so I drive it in gstack's own browser.", "B) Aside was not detected; I drive it in gstack's own browser.",
"B) Since the Aside browser is unavailable, I drive it in gstack's own browser.", "B) With no Aside installed, I drive it in gstack's browser."])
expect(asideDriveOptions(option), option).toEqual([]);
for (const option of ["A) Aside isn't open yet, so I open Aside and drive the dashboard.",
'A) I drive it in your Aside browser; Aside is not the only option.', "A) Aside wasn't running, but now I drive it in your Aside browser."])
expect(asideDriveOptions(option), option).toHaveLength(1);
});
test.each(recoveryFixture.responses)('native recovery response $attempt defers drive consent to a new question', ({ text }) => {
expect(asideDriveOptions(text)).toEqual([]);
expect(asideDriveOptions(text.replace('option included.', 'option included; then I drive in your Aside browser.'))).toHaveLength(1);
@@ -577,8 +587,9 @@ describe('Opus consent renders (gate captures)', () => {
test('the defer outcome counts in any words, and a refusal of it never does', () => {
// Gate 42431616220 slice 15: the red tpa-present render's D) option, missed by the spelling list.
for (const option of ["D) Do it later. I'll mark this step as waiting on you.", 'D) Not yet: the step stays blocked on you.',
'D) Later. This step remains waiting on you.', "D) Do it later \u2014 I'll leave this step as blocked on you.",
"D) Do it later - I'll leave this step as blocked on you."])
'D) Later. This step remains waiting on you.', 'D) Later, so this step remains waiting on you.', "D) Do it later \u2014 I'll leave this step as blocked on you.",
"D) Do it later - I'll leave this step as blocked on you.", 'D) Not now. The step stays blocked on you.',
'D) Sign in yourself later. The step is waiting on you.'])
expect(offersDefer(option), option).toBe(true);
for (const text of ['D) Continue now. I will not mark this step as blocked on you.',
"D) Continue now. I won't mark this step as waiting on you.", 'D) Continue now. I never mark this step as blocked on you.',
@@ -598,6 +609,12 @@ describe('Opus consent renders (gate captures)', () => {
"B) I drive it in gstack's own browser and mark this step as waiting on you.",
"C) Manual instructions; I'll mark this step as blocked on you.",
'C) I give you the steps to do it yourself. The step stays blocked on you.',
// /ship note at d98f9bb5 (PR #4223): a state line after a non-defer instruction is not a defer offer.
'D) Sign in yourself. Until then the step stays blocked on you.',
'D) Approve the token in the dashboard. The step is waiting on you.',
'D) Continue now. The step is waiting on you.',
'D) Do it now, not later. The step stays blocked on you.',
'D) The step is waiting on you. Do it later.',
"I'll mark this step as waiting on you.\nA) I drive it in your Aside browser\nB) Manual instructions"])
expect(offersDefer(text), text).toBe(false);
});