fix(gsd): unlock depth verification outside guided flow (#4058)

This commit is contained in:
mastertyko 2026-04-13 14:07:07 +02:00 committed by GitHub
parent 65ba0fc30b
commit 1d8e7c95ff
2 changed files with 105 additions and 10 deletions

View file

@ -181,14 +181,10 @@ export function registerHooks(pi: ExtensionAPI): void {
// Only gate-shaped ask_user_questions calls should block execution.
// The gate stays pending until the user selects the approval option.
if (event.toolName === "ask_user_questions") {
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
const inDiscussion = milestoneId !== null || isQueuePhaseActive();
if (inDiscussion) {
const questions: any[] = (event.input as any)?.questions ?? [];
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
if (typeof questionId === "string") {
setPendingGate(questionId);
}
const questions: any[] = (event.input as any)?.questions ?? [];
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
if (typeof questionId === "string") {
setPendingGate(questionId);
}
}
@ -286,7 +282,6 @@ export function registerHooks(pi: ExtensionAPI): void {
if (event.toolName !== "ask_user_questions") return;
const milestoneId = getDiscussionMilestoneId(process.cwd());
const queueActive = isQueuePhaseActive();
if (!milestoneId && !queueActive) return;
const details = event.details as any;
@ -319,13 +314,16 @@ export function registerHooks(pi: ExtensionAPI): void {
// Only unlock the gate if the user selected the first option (confirmation).
// Cross-references against the question's defined options to reject free-form "Other" text.
const answer = details.response?.answers?.[question.id];
const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
markDepthVerified(inferredMilestoneId);
clearPendingGate();
}
break;
}
}
if (!milestoneId && !queueActive) return;
if (!milestoneId) return;
const basePath = process.cwd();

View file

@ -0,0 +1,97 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { registerHooks } from "../bootstrap/register-hooks.ts";
import {
getPendingGate,
resetWriteGateState,
shouldBlockContextArtifactSave,
} from "../bootstrap/write-gate.ts";
function makeTempDir(prefix: string): string {
const dir = join(
tmpdir(),
`gsd-depth-gate-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
);
mkdirSync(dir, { recursive: true });
return dir;
}
test("register-hooks unlocks milestone depth verification from question id without guided-flow state (#4047)", async (t) => {
const dir = makeTempDir("manual");
const originalCwd = process.cwd();
process.chdir(dir);
resetWriteGateState();
t.after(() => {
resetWriteGateState();
process.chdir(originalCwd);
rmSync(dir, { recursive: true, force: true });
});
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<void> | void>>();
const pi = {
on(event: string, handler: (event: any, ctx?: any) => Promise<void> | void) {
const existing = handlers.get(event) ?? [];
existing.push(handler);
handlers.set(event, existing);
},
} as any;
registerHooks(pi);
const questionId = "depth_verification_M001_confirm";
const questions = [
{
id: questionId,
question: "Do you agree?",
options: [
{ label: "Yes, you got it (Recommended)" },
{ label: "Needs adjustment" },
],
},
];
const toolCallHandlers = handlers.get("tool_call");
const toolResultHandlers = handlers.get("tool_result");
assert.ok(toolCallHandlers?.length, "tool_call handler should be registered");
assert.ok(toolResultHandlers?.length, "tool_result handler should be registered");
for (const handler of toolCallHandlers ?? []) {
await handler({
toolName: "ask_user_questions",
input: { questions },
});
}
assert.equal(getPendingGate(), questionId, "gate should be set even without guided-flow state");
assert.equal(
shouldBlockContextArtifactSave("CONTEXT", "M001").block,
true,
"milestone context should still be blocked before confirmation",
);
for (const handler of toolResultHandlers ?? []) {
await handler({
toolName: "ask_user_questions",
input: { questions },
details: {
response: {
answers: {
[questionId]: { selected: "Yes, you got it (Recommended)" },
},
},
},
});
}
assert.equal(getPendingGate(), null, "confirming the depth question should clear the pending gate");
assert.equal(
shouldBlockContextArtifactSave("CONTEXT", "M001").block,
false,
"question-id milestone inference should unlock the matching milestone context write",
);
});