Files
gstack/test/helpers/shared-libs-review-start-evidence.ts
T
Garry Tan 7b534d3e90 v1.90.2.0 perf: halve local free-suite time and preserve coverage (#2972)
* perf: remove repeated test work and preserve AUQ execution budgets

* fix: validate native evaluation fixture evidence at its actual boundaries

* fix: clarify deployment approval and recovery state transitions

* chore: document coverage and release v1.90.2.0

* test: preserve Windows scheduling and native no-change consent

* test: recognize verified reads through fixture symlinks

* test: isolate alias-name installation from runtime assets
2026-09-25 13:27:07 -04:00

491 lines
26 KiB
TypeScript

import * as path from 'node:path';
import { createHash } from 'node:crypto';
interface StartContext {
repo: string;
state: string;
slug: string;
directory: string;
branch: string;
wtree: string;
startedAt: string;
}
/** Keep executable commands around heredocs; their data cannot introduce reads.
* This handles the literal delimiters used by the canonical Bun reuse command. */
function withoutHereDocBodies(source: string): string | undefined {
let output = '', quote = '';
const pending: { delimiter: string; tabs: boolean }[] = [];
for (let i = 0; i < source.length; i++) {
const char = source[i];
if (char === '\\' && quote !== "'") { output += char + (source[++i] ?? ''); continue; }
if (quote) { if (char === quote) quote = ''; output += char; continue; }
if (char === '"' || char === "'") { quote = char; output += char; continue; }
if (char === '#' && (i === 0 || /\s/.test(source[i - 1]))) {
while (i < source.length && source[i] !== '\n') output += source[i++];
i--; continue;
}
if (char === '<' && source[i + 1] === '<') {
const match = /^<<(-)?[ \t]*(?:'([^'\n]+)'|"([^"\n]+)"|([A-Za-z_][\w-]*))/.exec(source.slice(i));
if (!match) return undefined;
pending.push({ delimiter: match[2] ?? match[3] ?? match[4], tabs: !!match[1] });
output += ' '; i += match[0].length - 1; continue;
}
output += char;
if (char !== '\n' || !pending.length) continue;
for (const document of pending.splice(0)) {
let found = false;
while (i + 1 < source.length) {
const end = source.indexOf('\n', i + 1);
const line = source.slice(i + 1, end < 0 ? source.length : end).replace(/\r$/, '');
i = end < 0 ? source.length : end;
if ((document.tabs ? line.replace(/^\t*/, '') : line) === document.delimiter) { found = true; break; }
}
if (!found) return undefined;
}
}
return pending.length ? undefined : output;
}
function containsPath(text: string, file: string): boolean {
const escaped = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(?:^|[\\s"'\x60])${escaped}(?=$|[\\s"'\x60;])`).test(text);
}
function substitutionEnd(source: string, start: number): number {
let depth = 1, quote = '';
for (let i = start + 2; i < source.length; i++) {
const char = source[i];
if (char === '\\' && quote !== "'") { i++; continue; }
if (char === '$' && source[i + 1] === '(' && quote !== "'") {
i = substitutionEnd(source, i);
if (i < 0) return -1;
continue;
}
if (quote) { if (char === quote) quote = ''; continue; }
if (char === '"' || char === "'") quote = char;
else if (char === '(') depth++;
else if (char === ')' && --depth === 0) return i;
}
return -1;
}
/** Bounded inspection syntax, not a shell executor: quoted arguments and comments
* cannot introduce commands. Unknown inspection forms fail closed. */
function commands(source: string): { words: string[]; before: string; after: string; substitutions: number[]; quoted: number[] }[] {
const executable = withoutHereDocBodies(source);
if (executable === undefined) return [];
source = executable;
const result: ReturnType<typeof commands> = [];
let words: string[] = [], substitutions: number[] = [], quoted: number[] = [];
let word = '', quote = '', before = '', expanded = false, wasQuoted = false;
const flush = () => {
if (word) {
if (expanded) substitutions.push(words.length);
if (wasQuoted) quoted.push(words.length);
words.push(word);
}
word = ''; expanded = false; wasQuoted = false;
};
const end = (separator: string) => {
flush(); if (words.length) result.push({ words, before, after: separator, substitutions, quoted });
words = []; substitutions = []; quoted = []; before = separator;
};
for (let i = 0; i < source.length; i++) {
const char = source[i];
if (char === '\\' && quote !== "'") {
wasQuoted = true;
if (quote === '"' && !/[$`"\\\n]/.test(source[i + 1] ?? '')) word += char;
else { const escaped = source[++i] ?? ''; word += escaped === '$' ? '\0$' : escaped; }
continue;
}
if (char === '$' && source[i + 1] === '(' && quote !== "'") {
const end = substitutionEnd(source, i);
if (end < 0) return [];
expanded = true; word += source.slice(i, end + 1); i = end; continue;
}
if (quote) { if (char === quote) quote = ''; else word += quote === "'" && char === '$' ? '\0$' : char; continue; }
if (char === '"' || char === "'") { quote = char; wasQuoted = true; continue; }
if (char === '#' && !word) { while (i < source.length && source[i] !== '\n') i++; end(';'); }
else if (';|&()\n'.includes(char)) {
const separator = (char === '&' || char === '|') && source[i + 1] === char ? char + source[++i] : char;
end(separator === '\n' ? ';' : separator);
}
else if (/\s/.test(char)) flush();
else word += char;
}
if (quote) return [];
end('');
return result;
}
const basename = (word = '') => word.split(/[\\/]/).at(-1);
const sourcePaths = (file: string) => file.includes('\\') || /^[A-Za-z]:\//.test(file) ? path.win32 : path.posix;
type SourcePaths = ReturnType<typeof sourcePaths>;
function topLevelCommands(calls: ReturnType<typeof commands>): ReturnType<typeof commands> {
const scopes: string[] = [], result: ReturnType<typeof commands> = [];
for (const call of calls) {
if (call.before === '(') scopes.push(')');
if (!scopes.length && ['', ';'].includes(call.before)) {
if (['exit', 'return', 'exec'].includes(call.words[0])) break;
result.push(call);
}
let index = 0;
while (!call.quoted.includes(index) && ['then', 'else', 'do'].includes(call.words[index])) index++;
const head = call.quoted.includes(index) ? '' : call.words[index];
const close = ({ if: 'fi', for: 'done', while: 'done', until: 'done', case: 'esac', '{': '}' } as Record<string, string>)[head];
if (close) scopes.push(close);
else if (['fi', 'done', 'esac', '}'].includes(head) && scopes.pop() !== head) return [];
if (call.after === ')' && scopes.pop() !== ')') return [];
}
return result;
}
function reviewStart(words: string[]): boolean {
return words.length === 3 && basename(words[0]) === 'gstack-review-log'
&& words[1] === '--start' && words[2] === 'review';
}
function startTokenOutput(source: string, variables: Map<string, string | undefined>, token: string): string | undefined {
const calls = commands(source);
if (calls.length === 1 && reviewStart(calls[0].words) && !calls[0].before && !calls[0].after) return token;
const first = calls[0];
if (!first || first.before || first.words[0] !== 'echo' || first.words.length !== 2
|| expandVariables(first.words[1], variables) !== token) return undefined;
for (let i = 1; i < calls.length; i++) {
if (calls[i - 1].after !== '|') return undefined;
const words = calls[i].words;
if (['head', 'tail'].includes(words[0]) && (words.length === 2 && words[1] === '-1'
|| words.length === 3 && words[1] === '-n' && words[2] === '1')) continue;
if (words.length !== 3 || words[0] !== 'grep' || !['-oE', '-Eo'].includes(words[1])
|| !/^\[[A-Za-z0-9_.-]+\]\+$/.test(words[2])) return undefined;
try { if (new RegExp(words[2]).exec(token)?.[0] !== token) return undefined; } catch { return undefined; }
}
return calls.at(-1)?.after ? undefined : token;
}
function sameCallStartRead(source: string, file: string, expected: StartContext, token: string): boolean {
const paths = sourcePaths(file), variables = new Map<string, string | undefined>([
['GSTACK_HOME', expected.state], ['SLUG', expected.slug],
]);
if (paths.normalize(expected.directory) !== paths.join(expected.state, 'projects', expected.slug, '.review-starts')) return false;
let cwd: string | undefined = expected.repo, started = false;
for (const call of commands(source)) {
if (!['', ';'].includes(call.before) || !['', ';'].includes(call.after)
|| /^(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|function|exit|return|exec|eval|source|\.|[{}!])$/.test(call.words[0])) return false;
if (started && reader(call.words, operand => {
const value = expandVariables(operand, variables);
return value !== undefined && literalPath(value, cwd, paths) === file;
})) return true;
if (reviewStart(call.words)) {
if (cwd !== expected.repo || variables.get('GSTACK_HOME') !== expected.state) return false;
started = true;
} else if (call.words.every(word => /^[A-Za-z_]\w*=/.test(word))) {
for (const word of call.words) {
const equal = word.indexOf('='), expression = word.slice(equal + 1);
const substitution = /^\$\(([\s\S]*)\)$/.exec(expression);
const value = substitution ? startTokenOutput(substitution[1], variables, token) : expandVariables(expression, variables);
if (substitution && value === token && commands(substitution[1]).some(child => reviewStart(child.words))) {
if (cwd !== expected.repo || variables.get('GSTACK_HOME') !== expected.state) return false;
started = true;
}
variables.set(word.slice(0, equal), value);
}
} else if (call.words[0] === 'cd') {
const args = call.words.slice(call.words[1] === '--' ? 2 : 1);
const value = args.length === 1 ? expandVariables(args[0], variables) : undefined;
cwd = value ? literalPath(value, cwd, paths) : undefined;
} else if (['export', 'local', 'declare', 'readonly', 'unset', 'read', 'pushd', 'popd'].includes(call.words[0])) return false;
}
return false;
}
function successfulFinish(source: string, text: string, token: string, expected: StartContext): boolean {
const calls = commands(source), topLevel = topLevelCommands(calls);
const located = withDirectories(calls, expected.repo, sourcePaths(expected.repo), new Map([
['GSTACK_HOME', expected.state], ['SLUG', expected.slug],
]));
const finishes = (words: string[]) => basename(words[0]) === 'gstack-review-log'
&& words.some((word, index) => word === '--finish' && (words[index + 1] === token
|| /^\$(?:[A-Za-z_]\w*|\{[A-Za-z_]\w*\})$/.test(words[index + 1] ?? '')));
for (const call of topLevel) {
if (finishes(call.words)) return true;
if (call.quoted.includes(0) || call.words[0] !== 'if' || call.after !== ';' || !finishes(call.words.slice(1))) continue;
const index = calls.indexOf(call), success = calls[index + 1], failure = calls[index + 2], exit = calls[index + 3], end = calls[index + 4];
const context = located[index], argument = call.words[call.words.indexOf('--finish') + 1];
if (context.cwd !== expected.repo || context.variables.get('GSTACK_HOME') !== expected.state
|| expandVariables(argument, context.variables) !== token) continue;
if (success?.words[0] !== 'then' || success.quoted.includes(0) || success.words[1] !== 'echo' || success.words.length !== 3
|| /[$`\0]/.test(success.words[2]) || !text.split('\n').includes(success.words[2])
|| failure?.words[0] !== 'else' || failure.quoted.includes(0) || failure.words[1] !== 'echo'
|| exit?.words[0] !== 'exit' || !/^[1-9]\d*$/.test(exit.words[1] ?? '') || exit.words.length !== 2
|| end?.words[0] !== 'fi' || end.quoted.includes(0) || end.words.length !== 1
|| ![success, failure, exit, end].every(part => part.before === ';' && part.after === ';')) continue;
if (!topLevel.some(read => calls.indexOf(read) > index + 4 && basename(read.words[0]) === 'gstack-review-read')) continue;
for (const line of text.split('\n')) {
let row: any;
try { row = JSON.parse(line); } catch { continue; }
const binding = row?.review_binding;
if (row?.skill === 'review' && row.completed === true && row.wtree === expected.wtree
&& binding?.state === 'verified' && binding.start_wtree === expected.wtree && binding.end_wtree === expected.wtree
&& binding.started_at === expected.startedAt && binding.branch_id === createHash('sha256').update(expected.branch).digest('hex')) return true;
}
}
return false;
}
/** Resolve source-spelled paths, independent of the machine replaying the trace.
* Shell expansion is handled only by the discovery forms below, never guessed. */
function literalPath(value: string, cwd: string | undefined, paths: SourcePaths): string | undefined {
if (!value || /[\0$`*?\[\]~]/.test(value)) return undefined;
return paths.isAbsolute(value) ? paths.normalize(value) : cwd ? paths.resolve(cwd, value) : undefined;
}
function expandVariables(value: string, variables: Map<string, string | undefined>): string | undefined {
if (value.includes('\0') || value.includes('`')) return undefined;
let known = true;
const expanded = value.replace(/\$\{([A-Za-z_]\w*)(?::-[^}]*)?\}|\$([A-Za-z_]\w*)/g, (_match, braced, bare) => {
const bound = variables.get(braced ?? bare);
if (bound === undefined) known = false;
return bound ?? '';
});
return known && !expanded.includes('$') ? expanded : undefined;
}
/** Bash starts each tool call in the fixture repo. Literal cd changes the base
* for subsequent operands; pipelines/subshells cannot leak their cwd outward. */
function withDirectories(calls: ReturnType<typeof commands>, initialCwd: string | undefined, paths: SourcePaths,
trustedEnvironment: Map<string, string | undefined>) {
let cwd = initialCwd;
let variables = new Map(trustedEnvironment), previousCd = false;
const stack: { cwd: string | undefined; variables: Map<string, string | undefined> }[] = [];
return calls.map(call => {
if (call.before === '(') stack.push({ cwd, variables: new Map(variables) });
const located = { ...call, cwd, variables: new Map(variables) };
const conditional = ['&&', '||'].includes(call.before) && !(call.before === '&&' && previousCd);
previousCd = false;
if (call.words[0] === 'cd' && call.before !== '|' && !['|', '&'].includes(call.after)) {
const args = call.words.slice(call.words[1] === '--' ? 2 : 1);
const argument = args.length === 1 ? expandVariables(args[0], variables) : undefined;
cwd = conditional || call.after === '||' || !argument || argument.startsWith('-')
? undefined : literalPath(argument, cwd, paths);
previousCd = cwd !== undefined;
} else if (['pushd', 'popd'].includes(call.words[0])) cwd = undefined;
const assignments = ['export', 'local', 'declare', 'readonly'].includes(call.words[0]) ? call.words.slice(1) : call.words;
const isolated = call.before === '|' || ['|', '&'].includes(call.after);
if (!isolated && assignments.every(word => /^[A-Za-z_]\w*=/.test(word))) {
for (const word of assignments) {
const equal = word.indexOf('=');
variables.set(word.slice(0, equal), conditional ? undefined : expandVariables(word.slice(equal + 1), variables));
}
} else if (!isolated && call.words[0] === 'unset') for (const name of call.words.slice(1)) variables.delete(name);
if (call.after === ')') {
const restored = stack.pop();
cwd = restored?.cwd; variables = restored?.variables ?? new Map();
}
return located;
});
}
function reader(words: string[], target: string | ((operand: string) => boolean)): boolean {
const tool = basename(words[0]);
if (!['cat', 'head', 'tail', 'sed', 'jq'].includes(tool!)) return false;
let expression = tool === 'sed' || tool === 'jq', options = true;
for (let i = 1; i < words.length; i++) {
const word = words[i];
if (options && word === '--') { options = false; continue; }
if (options && word.startsWith('-')) {
if (tool === 'jq' && ['--arg', '--argjson', '--slurpfile', '--rawfile'].includes(word)) { i += 2; continue; }
if (tool === 'jq' && ['--args', '--jsonargs'].includes(word)) return false;
if ((tool === 'sed' && ['-e', '--expression', '-f', '--file'].includes(word))
|| (tool === 'jq' && ['-f', '--from-file'].includes(word))) { expression = false; i++; }
else if (['head', 'tail'].includes(tool!) && ['-n', '-c', '--lines', '--bytes'].includes(word)) i++;
continue;
}
if (expression) { expression = false; continue; }
if (typeof target === 'function' ? target(word) : word === target) return true;
}
return false;
}
const rebinds = (words: string[], variable: string) => words[0]?.startsWith(`${variable}=`)
|| ['export', 'local', 'declare', 'readonly', 'unset'].includes(words[0])
&& words.slice(1).some(word => word === variable || word.startsWith(`${variable}=`));
/** A path-producing find pipeline; filters may select lines, not supply another file. */
function findOutput(calls: ReturnType<typeof commands>): boolean {
if (!calls.length || basename(calls[0].words[0]) !== 'find'
|| calls[0].words.some(word => ['-exec', '-execdir', '-ok', '-okdir', '-printf', '-fprintf',
'-fprint', '-fprint0', '-ls', '-fls', '-delete', '-print0'].includes(word))) return false;
return calls.slice(1).every((call, offset) => {
if (calls[offset].after !== '|' || !['head', 'tail'].includes(basename(call.words[0])!)) return false;
for (let i = 1; i < call.words.length; i++) {
if (['-n', '--lines'].includes(call.words[i])) { if (!/^\d+$/.test(call.words[++i] ?? '')) return false; }
else if (!/^(?:-\d+|-n\d+|--lines=\d+)$/.test(call.words[i])) return false;
}
return true;
});
}
/** JSON-only cat output is sufficient when discovery itself pins this filename
* beneath a literal ancestor of its fixture-owned path. */
function findBindsFile(calls: ReturnType<typeof commands>, file: string, cwd: string | undefined,
variables: Map<string, string | undefined>): boolean {
if (!findOutput(calls)) return false;
const words = calls[0].words.map(word => expandVariables(word, variables) ?? word);
const paths = sourcePaths(file);
const scope = words[1];
const root = literalPath(scope, cwd, paths), actual = paths.normalize(file);
if (!root || (words[2] && !words[2].startsWith('-'))) return false;
if (root === actual) return true;
if (root === paths.parse(root).root || !actual.startsWith(root.replace(/[\\/]$/, '') + paths.sep)
|| words.some(word => ['-o', '-or', '!', '-not'].includes(word))) return false;
const filename = paths.basename(file);
return words.some((word, index) => {
const pattern = words[index + 1] ?? '';
if (word === '-name') return pattern === filename;
if (word !== '-path' || !pattern.includes(filename)) return false;
const glob = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
// find prints paths in the spelling of its root, including relative roots.
const discovered = scope.replace(/[\\/]$/, '') + paths.sep + paths.relative(root, actual);
return new RegExp(`^${glob}$`).test(discovered);
});
}
function inspectsFile(source: string, file: string, returnedPath: boolean, expected: StartContext): boolean {
const paths = sourcePaths(file);
const environment = new Map([['GSTACK_HOME', expected.state], ['SLUG', expected.slug]]);
const calls = withDirectories(commands(source), paths.normalize(expected.repo), paths, environment);
if (calls.some(call => reader(call.words, operand => {
const expanded = expandVariables(operand, call.variables);
return expanded !== undefined && literalPath(expanded, call.cwd, paths) === file;
}))) return true;
for (let i = 0; i < calls.length; i++) {
const call = calls[i];
for (const index of call.substitutions) {
const operand = call.words[index];
const assignment = /^([A-Za-z_]\w*)=\$\(([\s\S]*)\)$/.exec(operand);
if (index === 0 && call.words.length === 1 && assignment && findOutput(commands(assignment[2]))
&& (returnedPath || findBindsFile(commands(assignment[2]), file, call.cwd, call.variables))) {
for (const next of calls.slice(i + 1)) {
if (rebinds(next.words, assignment[1])) break;
if (reader(next.words, `$${assignment[1]}`) || reader(next.words, `\${${assignment[1]}}`)) return true;
}
}
if (!reader(call.words, operand)) continue;
const quoted = /^\$\(([\s\S]*)\)$/.exec(operand);
if (quoted && findOutput(commands(quoted[1]))
&& (returnedPath || findBindsFile(commands(quoted[1]), file, call.cwd, call.variables))) return true;
}
}
if (!returnedPath) return false;
for (let i = 0; i < calls.length; i++) {
const call = calls[i];
if (call.words[0] === 'for' && /^[A-Za-z_]\w*$/.test(call.words[1])
&& call.words[2] === 'in' && call.words.length === 4) {
const pattern = expandVariables(call.words[3], call.variables);
const match = pattern && /^(.*)[\\/]\*(?:\.json)?$/.exec(pattern);
if (match && literalPath(match[1], call.cwd, paths) === paths.dirname(file)) {
for (const next of calls.slice(i + 1)) {
if (next.words[0] === 'done') break;
const words = next.words[0] === 'do' ? next.words.slice(1) : next.words;
if (rebinds(words, call.words[1])) break;
if (reader(words, `$${call.words[1]}`) || reader(words, `\${${call.words[1]}}`)) return true;
}
}
}
if (basename(call.words[0]) !== 'find') continue;
const exec = call.words.indexOf('-exec');
const action = call.words.slice(exec + 1);
if (exec >= 0 && reader(action, '{}')) return true;
if (exec >= 0 && ['sh', 'bash'].includes(basename(action[0])!) && action[1] === '-c') {
// find -exec sh -c 'cat "$1"' _ {} \; passes each found path as $1.
const argument = action.indexOf('{}') - 3;
const script = commands((action[2] ?? '').replaceAll('\0$', '$'));
if (argument > 0 && !script.some(child => ['set', 'shift'].includes(child.words[0]))
&& script.some(child => reader(child.words, `$${argument}`))) return true;
}
if (call.after === '|' && calls[i + 1]?.words[0] === 'while') {
const header = calls[i + 1].words;
const variable = header.at(-1)!;
const executable = header.slice(1).find(word => !/^[A-Za-z_]\w*=/.test(word));
if (executable !== 'read' || !/^[A-Za-z_]\w*$/.test(variable)) continue;
for (const next of calls.slice(i + 2)) {
if (next.words[0] === 'done') break;
const words = next.words[0] === 'do' ? next.words.slice(1) : next.words;
if (rebinds(words, variable)) break;
if (reader(words, `$${variable}`) || reader(words, `\${${variable}}`)) return true;
}
}
}
return false;
}
/** Inspect native public tool blocks only; narration and instruction contents are not evidence. */
export function hasTrustedReviewStartRead(events: unknown[], expected: StartContext): boolean {
const pending = new Map<string, { tool: string; input: any; at: number }>();
const pairs: { tool: string; input: any; at: number; returnedAt: number; text: string }[] = [];
let position = 0;
for (const event of events as any[]) {
for (const block of Array.isArray(event?.message?.content) ? event.message.content : []) {
position++;
if (event.type === 'assistant' && block.type === 'tool_use' && typeof block.id === 'string') {
pending.set(block.id, { tool: block.name, input: block.input, at: position });
} else if (event.type === 'user' && block.type === 'tool_result') {
const call = pending.get(block.tool_use_id);
pending.delete(block.tool_use_id);
if (!call || block.is_error === true) continue;
const text = typeof block.content === 'string' ? block.content
: Array.isArray(block.content) ? block.content.filter((part: any) => part.type === 'text'
&& typeof part.text === 'string').map((part: any) => part.text).join('\n') : '';
pairs.push({ ...call, returnedAt: position, text });
}
}
}
for (const start of pairs) {
if (start.tool !== 'Bash' || typeof start.input?.command !== 'string'
|| !topLevelCommands(commands(start.input.command)).some(call => {
if (reviewStart(call.words)) return true;
if (call.words.length !== 1 || !call.substitutions.includes(0)) return false;
const assignment = /^[A-Za-z_]\w*=\$\(([\s\S]*)\)$/.exec(call.words[0]);
const children = assignment ? commands(assignment[1]) : [];
return children.length === 1 && !children[0].before && !children[0].after && reviewStart(children[0].words);
})) continue;
for (const token of start.text.match(/\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/g) ?? []) {
// Archived public Linux paths keep their spelling when free tests run on Windows.
const paths = sourcePaths(expected.directory);
const file = paths.join(expected.directory, `${token}.json`);
const finish = pairs.find(pair => pair.at > start.returnedAt && pair.tool === 'Bash'
&& typeof pair.input?.command === 'string'
// The documented shell variable is valid too: the trusted final row's
// started_at below binds its resolved value to this observed capture.
&& successfulFinish(pair.input.command, pair.text, token, expected));
if (!finish) continue;
for (const read of pairs) {
const sameCall = read === start;
if ((!sameCall && read.at <= start.returnedAt) || read.returnedAt >= finish.at) continue;
const command = typeof read.input?.command === 'string' ? read.input.command : '';
const directRead = read.tool === 'Read' && typeof read.input?.file_path === 'string'
&& literalPath(read.input.file_path, expected.repo, paths) === file;
// Discovery may return the path only in stdout; bind its cat invocation
// to that discovery instead of accepting unrelated reader/find words.
const shellRead = read.tool === 'Bash' && (sameCall ? sameCallStartRead(command, file, expected, token)
: inspectsFile(command, file, containsPath(read.text, file), expected));
if (!directRead && !shellRead) continue;
for (const line of read.text.split('\n')) {
// Native Read can prefix the single-line JSON file with a line number.
const json = line.replace(/^\s*\d+[\t →]+(?=\{)/, '').trim();
let record: any;
try { record = JSON.parse(json); } catch { continue; }
if (sameCall && start.text.indexOf(token) >= start.text.indexOf(line)) continue;
if (record?.skill === 'review' && record.repo === expected.repo && record.branch === expected.branch
&& record.wtree === expected.wtree && typeof expected.startedAt === 'string'
&& record.started_at === expected.startedAt) return true;
}
}
}
}
return false;
}