mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
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:
1 parent
e77c5320c5
commit
2328cd3bbc
4 files changed
+36
-1
No files matched your search
@@ -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`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user