fix: call cleanupQuickBranch on turn_end to squash-merge quick branch back (#3054)
cleanupQuickBranch() was exported from quick.ts but never called anywhere. After a /gsd quick task completed, the user was left on the quick branch with orphaned state in quick-return.json. Register a turn_end hook in register-hooks.ts that calls cleanupQuickBranch() after each agent turn. The function is already idempotent (no-op when no quick-return state is pending), so it is safe to call on every turn. Closes #2668 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dfb18c6e62
commit
fb10141e9b
2 changed files with 102 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ import { buildBeforeAgentStartResult } from "./system-context.js";
|
|||
import { handleAgentEnd } from "./agent-end-recovery.js";
|
||||
import { clearDiscussionFlowState, isDepthVerified, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution } from "./write-gate.js";
|
||||
import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
|
||||
import { cleanupQuickBranch } from "../quick.js";
|
||||
import { getDiscussionMilestoneId } from "../guided-flow.js";
|
||||
import { loadToolApiKeys } from "../commands-config.js";
|
||||
import { loadFile, saveFile, formatContinue } from "../files.js";
|
||||
|
|
@ -86,6 +87,17 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|||
await handleAgentEnd(pi, event, ctx);
|
||||
});
|
||||
|
||||
// Squash-merge quick-task branch back to the original branch after the
|
||||
// agent turn completes (#2668). cleanupQuickBranch is a no-op when no
|
||||
// quick-return state is pending, so this is safe to call on every turn.
|
||||
pi.on("turn_end", async () => {
|
||||
try {
|
||||
cleanupQuickBranch();
|
||||
} catch {
|
||||
// Best-effort: don't break the turn lifecycle if cleanup fails.
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_before_compact", async () => {
|
||||
if (isAutoActive() || isAutoPaused()) {
|
||||
return { cancel: true };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Tests that cleanupQuickBranch is called on turn_end to squash-merge the
|
||||
* quick branch back to the original branch after the agent completes.
|
||||
*
|
||||
* Relates to #2668: /gsd quick does not squash-merge branch back after agent
|
||||
* completes task. cleanupQuickBranch() exists but is never invoked.
|
||||
*
|
||||
* The fix registers a turn_end hook in register-hooks.ts that calls
|
||||
* cleanupQuickBranch() after each turn, which is a no-op when no quick-task
|
||||
* state is pending.
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
// ─── Structural test: verify turn_end hook exists in register-hooks.ts ──────
|
||||
|
||||
describe("quick task turn_end cleanup (#2668)", () => {
|
||||
const hooksSource = readFileSync(
|
||||
join(import.meta.dirname, "..", "bootstrap", "register-hooks.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
it("register-hooks.ts imports cleanupQuickBranch from quick.ts", () => {
|
||||
assert.ok(
|
||||
hooksSource.includes("cleanupQuickBranch"),
|
||||
"register-hooks.ts must reference cleanupQuickBranch",
|
||||
);
|
||||
|
||||
// Verify it's imported (not just mentioned in a comment)
|
||||
const importMatch = hooksSource.match(
|
||||
/import\s*\{[^}]*cleanupQuickBranch[^}]*\}\s*from\s*["'][^"']*quick/,
|
||||
);
|
||||
assert.ok(
|
||||
importMatch,
|
||||
"cleanupQuickBranch must be imported from quick module",
|
||||
);
|
||||
});
|
||||
|
||||
it("registers a turn_end handler that calls cleanupQuickBranch", () => {
|
||||
// Find the turn_end registration
|
||||
const turnEndMatch = hooksSource.match(
|
||||
/pi\.on\(\s*["']turn_end["']/,
|
||||
);
|
||||
assert.ok(
|
||||
turnEndMatch,
|
||||
"register-hooks.ts must register a turn_end handler",
|
||||
);
|
||||
|
||||
// Extract the turn_end handler body — find everything from the pi.on("turn_end"
|
||||
// to the matching closing });
|
||||
const turnEndIdx = hooksSource.indexOf(turnEndMatch[0]);
|
||||
assert.ok(turnEndIdx !== -1);
|
||||
|
||||
// Get the rest of the source from that point
|
||||
const rest = hooksSource.slice(turnEndIdx);
|
||||
|
||||
// The handler must call cleanupQuickBranch
|
||||
// Look for cleanupQuickBranch within the first handler body (up to first `});`)
|
||||
const handlerEnd = rest.indexOf("});");
|
||||
assert.ok(handlerEnd !== -1, "turn_end handler has a closing });");
|
||||
|
||||
const handlerBody = rest.slice(0, handlerEnd);
|
||||
assert.ok(
|
||||
handlerBody.includes("cleanupQuickBranch"),
|
||||
"turn_end handler must call cleanupQuickBranch",
|
||||
);
|
||||
});
|
||||
|
||||
it("turn_end handler calls cleanupQuickBranch without arguments (uses cwd default)", () => {
|
||||
// cleanupQuickBranch(basePath = process.cwd()) — calling without args is correct
|
||||
// because the handler runs in the same process where handleQuick set up cwd
|
||||
const turnEndIdx = hooksSource.indexOf('pi.on("turn_end"') !== -1
|
||||
? hooksSource.indexOf('pi.on("turn_end"')
|
||||
: hooksSource.indexOf("pi.on('turn_end'");
|
||||
assert.ok(turnEndIdx !== -1);
|
||||
|
||||
const rest = hooksSource.slice(turnEndIdx);
|
||||
const handlerEnd = rest.indexOf("});");
|
||||
const handlerBody = rest.slice(0, handlerEnd);
|
||||
|
||||
// Should call cleanupQuickBranch() — either bare or with no-arg form
|
||||
assert.ok(
|
||||
handlerBody.includes("cleanupQuickBranch("),
|
||||
"turn_end handler invokes cleanupQuickBranch()",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue