mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 22:45:02 +01:00
fix: pre-landing review fixes (journal reader strips the cwd-reset footer; an init with no text block no longer locks the session)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
98f31e924d
commit
bdac971bc6
3 files changed
+28
-9
No files matched your search
@@ -17,6 +17,10 @@ type Event = ClaudeParentPublicEvent;
|
||||
type Use = NativePublicToolEvent & { order: number };
|
||||
/** Claude Code's reply to a Read of an unchanged file it already returned in full. */
|
||||
export const DEDUP_REPLY = 'Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.';
|
||||
/** Claude Code appends exactly this one line to a Bash result when the command's `cd` left the session cwd (#3651). */
|
||||
const SHELL_CWD_RESET = /\nShell cwd was reset to [^\n]+$/;
|
||||
/** A Bash result's JSON, without that one host footer; anything else must still be strict JSON. */
|
||||
export function bashJson(text: string): any { return JSON.parse(text.replace(SHELL_CWD_RESET, '')); }
|
||||
/** The longest Read input or Bash command the index keeps; a literal init is far shorter. */
|
||||
const INDEX_INPUT_MAX = 4096;
|
||||
|
||||
@@ -69,7 +73,7 @@ function select(tests: GuardJournalTests, currentToolUseId: string) {
|
||||
const replies = (resultsOf.get(init.use.toolUseId) ?? []).flatMap(r => r.events)
|
||||
.filter(e => e.kind === 'result' && e.toolUseId === init.use.toolUseId && e.order > init.use.order);
|
||||
let reply: unknown;
|
||||
try { reply = replies.length === 1 ? JSON.parse(textResult(replies[0]!) ?? 'null') : undefined; } catch { reply = undefined; }
|
||||
try { reply = replies.length === 1 ? bashJson(textResult(replies[0]!) ?? 'null') : undefined; } catch { reply = undefined; }
|
||||
// A reused:true init keeps the earlier binding, as invocation() does.
|
||||
if (object(reply) && reply.reused === true && bound && bound.activePlan === reply.activePlan &&
|
||||
bound.restorePath === reply.restorePath) continue;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { extractImplementationPlan, checkPhaseImplementation, acceptedBlocks, ca
|
||||
import { autoplanPhaseCompletions } from '../../lib/autoplan-phase-publication';
|
||||
import { nativePathSpelling, ownedNativePath, sameNativePath, type ClaudeParentPublicEvent } from '../../lib/claude-journal-records';
|
||||
import { ownedRecordLimit, ownedRetainedLimit, type JournalPrefix, type OwnedRead, type OwnedReadMeasure } from '../../lib/claude-owned-journal';
|
||||
import { readGuardJournal, textResult, DEDUP_REPLY } from './guard-journal';
|
||||
import { bashJson, readGuardJournal, textResult, DEDUP_REPLY } from './guard-journal';
|
||||
import { REASONS, reasonCode, reasonText, type Detail, type ReasonCode } from './guard-reasons';
|
||||
import { logGuardDecision } from './guard-log';
|
||||
import { approveOwnedRead, renderSectionBase, userRenderRoot } from './owned-read';
|
||||
@@ -220,11 +220,6 @@ export function boundAutoplanPhaseConsumption(events: Event[], use: Use, cwd: st
|
||||
return consumption(use, cwd, root, invocation(events.filter(e => e.order < use.order), root));
|
||||
}
|
||||
|
||||
/** Claude Code appends exactly this one line to a Bash result when the command's `cd` left the session cwd (#3651). */
|
||||
const SHELL_CWD_RESET = /\nShell cwd was reset to [^\n]+$/;
|
||||
/** A Bash result's JSON, without that one host footer; anything else must still be strict JSON. */
|
||||
function bashJson(text: string): any { return JSON.parse(text.replace(SHELL_CWD_RESET, '')); }
|
||||
|
||||
/** Authenticate the existing direct-create result; this does not prove its shell command's origin. */
|
||||
function checkpointResult(result: Event, tools: Tools, init: Invocation): { phase: Phase; path: string } | undefined {
|
||||
if (result.kind !== 'result') return;
|
||||
@@ -286,7 +281,7 @@ function invocation(events: Event[], root: string): Invocation {
|
||||
// An errored init published nothing; a later correct init may still bind.
|
||||
if (results[0]!.isError === true) { lastFailure = INIT_FAILED_CAUSE; continue; }
|
||||
const text = textResult(results[0]!);
|
||||
if (text === undefined) fail('init_failed');
|
||||
if (text === undefined) { lastFailure = INIT_FAILED_CAUSE; continue; }
|
||||
let result: any;
|
||||
// Output that is not the init's JSON bound nothing; a later correct init may still bind.
|
||||
try { result = bashJson(text); } catch { lastFailure = INIT_UNPARSEABLE_CAUSE; continue; }
|
||||
|
||||
@@ -1628,8 +1628,28 @@ describe('#3651: Claude Code cwd-reset footer on a Bash result', () => {
|
||||
expect(decision.reason).toContain('re-run');
|
||||
});
|
||||
|
||||
test('a later clean init recovers the session after an init whose output did not parse', () => {
|
||||
test('#3651: the hook path keeps the first binding when a cwd-reset init is re-run after a typed user message', async () => {
|
||||
const footer = '\nShell cwd was reset to /home/user/project';
|
||||
const run = async (tail: string) => {
|
||||
const f = fixture(); (f.events[1] as any).content += tail; f.message();
|
||||
f.use('re-init', 'Bash', (f.events[0] as any).input); f.result('re-init', { content: JSON.stringify({ ...f.init, reused: true }) + tail });
|
||||
f.current();
|
||||
const { rows, record } = f.journal();
|
||||
const at = rows.findIndex(r => Array.isArray(r.message.content) && r.message.content.some((b: any) => b.type === 'tool_use' && b.id === 're-init'));
|
||||
rows[at - 1]!.message.stop_reason = 'end_turn';
|
||||
rows.splice(at, 0, record('user', 'Looks good, keep going.', { origin: { kind: 'human' }, promptSource: 'typed', promptId: randomUUID() }));
|
||||
rows.forEach((r, i) => { r.parentUuid = i ? rows[i - 1]!.uuid : null; });
|
||||
fs.writeFileSync(f.input.transcript_path, rows.map(x => JSON.stringify(x)).join('\n') + '\n');
|
||||
return withNativeProjectDirectory(f.cwd, () => runPublicationHook(f.input, ROOT));
|
||||
};
|
||||
expect(await run('')).toEqual(OWNED_READ_APPROVAL);
|
||||
expect(await run(footer)).toEqual(OWNED_READ_APPROVAL);
|
||||
});
|
||||
|
||||
for (const [name, content] of [['did not parse', undefined], ['had no single text block', [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }]]] as const)
|
||||
test(`a later clean init recovers the session after an init whose output ${name}`, () => {
|
||||
const f = withInitTail('\nunexpected trailing output');
|
||||
if (content) (f.events[1] as any).content = content;
|
||||
f.events.splice(2, 0, { ...(structuredClone(f.events[0]!) as any), toolUseId: 'again' },
|
||||
{ ...(structuredClone(f.events[1]!) as any), toolUseId: 'again', content: JSON.stringify({ ...f.init, reused: true }) });
|
||||
expect(ready(f)).toEqual({ allow: true });
|
||||
|
||||
Reference in new issue
Block a user