fix(autoplan): name a shell-built snapshot init as the cause of the missing-evidence denial (#3045)

The guard binds a phase entry only to a literal snapshot-init Bash call.
When the model resolved the tool path into a variable, chained or piped the
call, init ran and succeeded but the denial said 'Complete the existing
snapshot init step', which reads as already done, so the run gave up. When
no init binds but a Bash call looks like one, the denial now says the shape
cannot be bound and prints the literal re-run (answers reused:true). The
binding rule is unchanged, and the skill says to run init on its own.

Co-authored-by: CharlesBerg85 <CharlesBerg85@users.noreply.github.com>
This commit is contained in:
garrytanandCharlesBerg85 committed 2026-10-06 18:20:37 +00:00
1 parent e77c5320c5
commit 2328cd3bbc
4 files changed
+36 -1

No files matched your search

+2
View File
@@ -685,6 +685,8 @@ Before scope/review:
```bash
bun "<SNAPSHOT_TOOL>" init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"
```
Run init as its own Bash call with the literal absolute paths: no variables,
substitutions, chaining, pipes or redirects, which the guard cannot bind.
Use returned paths/`scope`; never hand-wrap. init backs up SOURCE_PLAN exactly,
then initializes ACTIVE_PLAN atomically without losing requirements.
Reviewers get only `## Implementation plan`; analysis stays in `## Review record`,
+2
View File
@@ -215,6 +215,8 @@ Before scope/review:
```bash
bun "<SNAPSHOT_TOOL>" init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"
```
Run init as its own Bash call with the literal absolute paths: no variables,
substitutions, chaining, pipes or redirects, which the guard cannot bind.
Use returned paths/`scope`; never hand-wrap. init backs up SOURCE_PLAN exactly,
then initializes ACTIVE_PLAN atomically without losing requirements.
Reviewers get only `## Implementation plan`; analysis stays in `## Review record`,
+9 -1
View File
@@ -224,13 +224,17 @@ function initArguments(command: unknown, root: string): string[] | undefined {
return args.slice(1);
}
/** Diagnostic only, never binds: a Bash call that ran snapshot init in a shape the guard cannot bind (#3045). */
const UNBINDABLE_INIT = /(?:gstack-autoplan-snapshot|SNAPSHOT_TOOL|\$\{?[A-Z_]+\}?"?)\S*\s+init\s/;
function invocation(events: Event[], root: string): Invocation {
let bound: Invocation | undefined;
let chosen: Record<string, any> | undefined;
let unbindable = false;
for (const use of events) {
if (use.kind !== 'use' || use.name !== 'Bash') continue;
const args = initArguments(use.input?.command, root);
if (!args) continue;
if (!args) { unbindable ||= typeof use.input?.command === 'string' && UNBINDABLE_INIT.test(use.input.command) && /\.md\b/.test(use.input.command); continue; }
const results = events.filter(x => x.kind === 'result' && x.toolUseId === use.toolUseId && x.order > use.order);
if (results.length !== 1) fail('Autoplan initialization acknowledgment is unavailable or ambiguous.');
const text = textResult(results[0]!);
@@ -244,6 +248,10 @@ function invocation(events: Event[], root: string): Invocation {
bound = { activePlan: result.activePlan, restorePath: result.restorePath,
originalSha256: result.originalSha256, start: results[0]!.order };
}
if ((!chosen || !bound) && unbindable)
fail('Autoplan invocation evidence is unavailable: snapshot init ran through a shell variable, substitution, chaining, ' +
'a pipe or a redirect, which this guard cannot bind. Re-run it as one Bash call with the literal absolute paths: ' +
'`bun "<SNAPSHOT_TOOL>" init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"` (it answers reused:true), then retry this tool.');
if (!chosen || !bound) fail('Autoplan invocation evidence is unavailable. Complete the existing snapshot init step before phase entry.');
const restore = read(bound.restorePath, true), active = read(bound.activePlan);
const reference = JSON.stringify(bound.restorePath).replace(/--/g, '\\u002d\\u002d');
+23
View File
@@ -483,6 +483,29 @@ describe('Autoplan parent publication guard', () => {
});
}
test('#3045: no init at all keeps the generic missing-evidence denial', () => {
const f = fixture(); f.message(); f.current(); f.events.splice(0, 2); f.reorder();
const reason = (f.evaluate() as any).reason;
expect(reason).toContain('Complete the existing snapshot init step');
expect(reason).not.toContain('cannot bind');
});
for (const [name, command] of [
['a resolved-variable init piped to tail', (f: any) => `ST=$(bun -e 'console.log(require("fs").realpathSync(process.argv[1]))' "${ROOT}/bin/gstack-autoplan-snapshot.ts"); bun "$ST" init "${f.source}" "${f.active}" "${f.restore}" 2>&1 | tail -5`],
['an init through a variable from an earlier call', (f: any) => `bun "$SNAPSHOT_TOOL" init "${f.source}" "${f.active}" "${f.restore}"`],
['a chained literal init', (f: any) => `bun "${ROOT}/bin/gstack-autoplan-snapshot.ts" init "${f.source}" "${f.active}" "${f.restore}" && echo done`],
] as const)
test(`#3045: ${name} is named as the cause with the literal re-run; only the literal re-run binds`, () => {
const f = fixture(); f.message(); f.current();
(f.events[0] as any).input.command = command(f);
const reason = (f.evaluate() as any).reason;
expect(reason).toContain('which this guard cannot bind');
expect(reason).toContain('`bun "<SNAPSHOT_TOOL>" init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"`');
f.events.splice(2, 0, { ...structuredClone(f.events[0]!), toolUseId: 're-init',
input: { command: `bun "${ROOT}/bin/gstack-autoplan-snapshot.ts" init "${f.source}" "${f.active}" "${f.restore}"` } },
{ ...structuredClone(f.events[1]!), toolUseId: 're-init', content: JSON.stringify({ ...f.init, reused: true }) });
f.reorder(); expect(f.evaluate()).toEqual({ allow: true });
});
test('split successful close ranges through EOF are sufficient', () => {
const f = fixture(); f.events.splice(4, 2); f.read('part1', f.packet.closePacketPath, 1, 10);
f.read('part2', f.packet.closePacketPath, 11); f.message(); f.current(); expect(f.evaluate()).toEqual({ allow: true });