From 69eb369675e098c75ba6975c976516a75b541e86 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 19:06:59 -0700 Subject: [PATCH 1/2] fix(gsd): restore full tool set after discuss flow scoping dispatchWorkflow scoped tools down for discuss-* flows but never restored them. The narrowed set persisted into subsequent dispatches, making GSD execution tools permanently unavailable for the session. Now saves the full tool list before scoping and restores it immediately after sendMessage queues the turn. The LLM turn has already captured the scoped set so it's unaffected. Closes #3628 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/guided-flow.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index d0f400448..c0bd063d6 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -295,8 +295,10 @@ async function dispatchWorkflow( // "Grammar is too complex" when the combined tool schema is too large. // Discuss flows only need a small subset of GSD tools — strip the heavy // planning/execution/completion tools to keep the grammar within limits. + let savedTools: string[] | null = null; if (unitType?.startsWith("discuss-")) { const currentTools = pi.getActiveTools(); + savedTools = currentTools; // Keep all non-GSD tools (builtins, other extensions) and only the // GSD tools on the discuss allowlist. const scopedTools = currentTools.filter( @@ -322,6 +324,13 @@ async function dispatchWorkflow( }, { triggerTurn: true }, ); + + // Restore full tool set after the message is queued. The LLM turn has + // already captured the scoped set — restoring prevents the narrowed + // tools from leaking into subsequent dispatches (#3628). + if (savedTools) { + pi.setActiveTools(savedTools); + } } /** From 3c83aa5539eaf3a5c6d88e11fddc4469a2c9688b Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:29:28 -0700 Subject: [PATCH 2/2] test: add regression test for restoring tools after discuss flow scoping Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/restore-tools-after-discuss.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts diff --git a/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts b/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts new file mode 100644 index 000000000..a820125e9 --- /dev/null +++ b/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts @@ -0,0 +1,63 @@ +/** + * Regression test for #3628 — restore tool set after discuss flow scoping + * + * The discuss flow narrows the active tool set to avoid "grammar too complex" + * errors. Without restoring after sendMessage, the narrowed tools leaked into + * subsequent dispatches, breaking plan/execute flows. + * + * The fix saves the full tool set before scoping and restores it after + * sendMessage returns. + */ + +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const src = readFileSync( + resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'guided-flow.ts'), + 'utf-8', +) + +describe('restore tools after discuss flow scoping (#3628)', () => { + it('savedTools is declared before the discuss scoping block', () => { + // savedTools must be declared before the discuss-* check + const savedToolsDecl = src.indexOf('let savedTools') + const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-"))') + assert.ok(savedToolsDecl !== -1, 'savedTools variable must be declared') + assert.ok(discussCheck !== -1, 'discuss-* type check must exist') + assert.ok( + savedToolsDecl < discussCheck, + 'savedTools must be declared before the discuss scoping block', + ) + }) + + it('savedTools captures current tools inside the discuss block', () => { + const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-"))') + assert.ok(discussCheck !== -1) + + // Look for savedTools assignment within the discuss block + const blockAfter = src.slice(discussCheck, discussCheck + 500) + assert.ok( + blockAfter.includes('savedTools = currentTools'), + 'savedTools must be assigned from currentTools inside the discuss block', + ) + }) + + it('savedTools is restored after sendMessage', () => { + // Find the sendMessage call + const sendMsg = src.indexOf('triggerTurn: true') + assert.ok(sendMsg !== -1, 'sendMessage with triggerTurn must exist') + + // After sendMessage, savedTools should be restored via setActiveTools + const afterSend = src.slice(sendMsg, sendMsg + 500) + assert.ok( + afterSend.includes('if (savedTools)'), + 'savedTools restoration guard must exist after sendMessage', + ) + assert.ok( + afterSend.includes('setActiveTools(savedTools)'), + 'setActiveTools(savedTools) must be called to restore the full tool set', + ) + }) +})