gsd-pi 2.66.1-dev.9a14d3d → 2.66.1-dev.e700a1b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +79 -11
  2. package/dist/resources/extensions/gsd/auto-model-selection.js +11 -2
  3. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +49 -2
  4. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +111 -0
  5. package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
  6. package/dist/resources/extensions/gsd/detection.js +6 -0
  7. package/dist/resources/extensions/gsd/index.js +1 -1
  8. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  9. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  10. package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
  11. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -2
  12. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  13. package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
  14. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  16. package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  17. package/dist/resources/extensions/remote-questions/manager.js +8 -0
  18. package/dist/web/standalone/.next/BUILD_ID +1 -1
  19. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  20. package/dist/web/standalone/.next/build-manifest.json +2 -2
  21. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  22. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  23. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.html +1 -1
  39. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/package.json +1 -1
  50. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +4 -3
  51. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/retry-handler.js +6 -5
  53. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +19 -0
  55. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  56. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts +2 -0
  57. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +15 -0
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  64. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +22 -0
  65. package/packages/pi-coding-agent/src/core/retry-handler.ts +6 -5
  66. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +16 -0
  67. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
  68. package/src/resources/extensions/ask-user-questions.ts +103 -11
  69. package/src/resources/extensions/gsd/auto-model-selection.ts +11 -2
  70. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +57 -2
  71. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +128 -0
  72. package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
  73. package/src/resources/extensions/gsd/detection.ts +6 -0
  74. package/src/resources/extensions/gsd/index.ts +6 -0
  75. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  76. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  77. package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
  78. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -2
  79. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  80. package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
  81. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  82. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  83. package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  84. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +21 -7
  85. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  86. package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
  87. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
  88. package/src/resources/extensions/gsd/tests/write-gate.test.ts +156 -0
  89. package/src/resources/extensions/remote-questions/manager.ts +9 -0
  90. /package/dist/web/standalone/.next/static/{UR2XjRqocvGBpbjt8dHCS → TCMOrUQ1suscla6CiwNya}/_buildManifest.js +0 -0
  91. /package/dist/web/standalone/.next/static/{UR2XjRqocvGBpbjt8dHCS → TCMOrUQ1suscla6CiwNya}/_ssgManifest.js +0 -0
@@ -58,6 +58,51 @@ export function questionSignature(questions) {
58
58
  export function resetAskUserQuestionsCache() {
59
59
  turnCache.clear();
60
60
  }
61
+ /**
62
+ * Race a remote channel dispatch against the local TUI. The first to produce
63
+ * a valid (non-error, non-timeout) result wins. The loser is cancelled via
64
+ * the shared AbortController.
65
+ *
66
+ * If the local TUI responds first, the remote poll is aborted (the message
67
+ * stays in Discord/Slack but polling stops). If remote responds first, the
68
+ * local TUI prompt is cancelled.
69
+ *
70
+ * Returns null only when both sides fail or are cancelled.
71
+ */
72
+ async function raceRemoteAndLocal(startRemote, startLocal, controller, questions) {
73
+ // Wrap local TUI result into the same shape as remote results
74
+ const localPromise = startLocal().then((result) => {
75
+ if (!result || Object.keys(result.answers).length === 0)
76
+ return null;
77
+ return {
78
+ content: [{ type: "text", text: formatForLLM(result) }],
79
+ details: { questions, response: result, cancelled: false },
80
+ };
81
+ }).catch(() => null);
82
+ const remotePromise = startRemote().then((result) => {
83
+ if (!result)
84
+ return null;
85
+ const details = result.details;
86
+ // Treat timeouts and errors as non-wins — let the local TUI win instead
87
+ if (details?.timed_out || details?.error)
88
+ return null;
89
+ return result;
90
+ }).catch(() => null);
91
+ // Race: first non-null result wins
92
+ const winner = await Promise.race([
93
+ localPromise.then((r) => r ? { source: "local", result: r } : null),
94
+ remotePromise.then((r) => r ? { source: "remote", result: r } : null),
95
+ ]);
96
+ if (winner) {
97
+ // Cancel the loser
98
+ controller.abort();
99
+ return winner.result;
100
+ }
101
+ // First to resolve was null — wait for the other
102
+ const [localResult, remoteResult] = await Promise.all([localPromise, remotePromise]);
103
+ controller.abort();
104
+ return localResult ?? remoteResult;
105
+ }
61
106
  // ─── Helpers ──────────────────────────────────────────────────────────────────
62
107
  const OTHER_OPTION_LABEL = "None of the above";
63
108
  function errorResult(message, questions = []) {
@@ -116,19 +161,42 @@ export default function AskUserQuestions(pi) {
116
161
  return errorResult(`Error: ask_user_questions requires non-empty options for every question (question "${q.id}" has none)`, params.questions);
117
162
  }
118
163
  }
119
- // Try remote first if configured (works in both interactive and headless modes).
120
- // tryRemoteQuestions returns null when no remote channel is configured, so
121
- // this is a no-op when the user has not set up Slack/Discord/Telegram.
122
- const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
123
- const remoteResult = await tryRemoteQuestions(params.questions, signal);
124
- if (remoteResult) {
125
- // Cache successful remote results to prevent duplicate Discord dispatches
126
- const remoteDetails = remoteResult.details;
127
- if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
128
- turnCache.set(sig, remoteResult);
164
+ // ── Routing: race remote + local, remote-only, or local-only ────────
165
+ const { tryRemoteQuestions, isRemoteConfigured } = await import("./remote-questions/manager.js");
166
+ const hasRemote = isRemoteConfigured();
167
+ // Case 1: Both remote and local UI available — race them.
168
+ // The first response wins; the loser is cancelled via AbortController.
169
+ if (hasRemote && ctx.hasUI) {
170
+ const raceController = new AbortController();
171
+ // Merge the parent signal so external cancellation propagates.
172
+ const onParentAbort = () => raceController.abort();
173
+ signal?.addEventListener("abort", onParentAbort, { once: true });
174
+ const raceSignal = raceController.signal;
175
+ const raceResult = await raceRemoteAndLocal(() => tryRemoteQuestions(params.questions, raceSignal), () => showInterviewRound(params.questions, {}, ctx), raceController, params.questions);
176
+ signal?.removeEventListener("abort", onParentAbort);
177
+ if (raceResult) {
178
+ const details = raceResult.details;
179
+ if (details && !details.timed_out && !details.error && !details.cancelled) {
180
+ turnCache.set(sig, raceResult);
181
+ }
182
+ return { ...raceResult, details: raceResult.details };
183
+ }
184
+ // Both sides failed/cancelled — fall through to error
185
+ return errorResult("ask_user_questions: no response received from local UI or remote channel", params.questions);
186
+ }
187
+ // Case 2: Remote configured but no local UI (headless) — remote only.
188
+ if (hasRemote && !ctx.hasUI) {
189
+ const remoteResult = await tryRemoteQuestions(params.questions, signal);
190
+ if (remoteResult) {
191
+ const remoteDetails = remoteResult.details;
192
+ if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
193
+ turnCache.set(sig, remoteResult);
194
+ }
195
+ return { ...remoteResult, details: remoteResult.details };
129
196
  }
130
- return { ...remoteResult, details: remoteResult.details };
197
+ return errorResult("Error: remote channel configured but returned no result", params.questions);
131
198
  }
199
+ // Case 3: No remote — local UI only.
132
200
  if (!ctx.hasUI) {
133
201
  return errorResult("Error: UI not available (non-interactive mode)", params.questions);
134
202
  }
@@ -252,8 +252,17 @@ export function resolveModelId(modelId, availableModels, currentProvider) {
252
252
  return undefined;
253
253
  if (candidates.length === 1)
254
254
  return candidates[0];
255
- // Extension / CLI-wrapper providers that should never win bare-ID resolution
256
- // when a first-class API provider also offers the same model.
255
+ // When the user's current provider is claude-code (set by startup migration
256
+ // or explicit selection), honour it for bare IDs. Routing back to anthropic
257
+ // would undo the migration and hit the third-party subscription block (#3772).
258
+ if (currentProvider === "claude-code") {
259
+ const ccMatch = candidates.find(m => m.provider === "claude-code");
260
+ if (ccMatch)
261
+ return ccMatch;
262
+ }
263
+ // Extension / CLI-wrapper providers that should not win bare-ID resolution
264
+ // when a first-class API provider also offers the same model AND the user
265
+ // has not explicitly chosen the extension provider.
257
266
  const EXTENSION_PROVIDERS = new Set(["claude-code"]);
258
267
  // Prefer currentProvider only when it is a first-class API provider
259
268
  if (currentProvider && !EXTENSION_PROVIDERS.has(currentProvider)) {
@@ -3,7 +3,7 @@ import { isToolCallEventType } from "@gsd/pi-coding-agent";
3
3
  import { buildMilestoneFileName, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
4
4
  import { buildBeforeAgentStartResult } from "./system-context.js";
5
5
  import { handleAgentEnd } from "./agent-end-recovery.js";
6
- import { clearDiscussionFlowState, isDepthVerified, isDepthConfirmationAnswer, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution } from "./write-gate.js";
6
+ import { clearDiscussionFlowState, isDepthVerified, isDepthConfirmationAnswer, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution, setPendingGate, clearPendingGate, getPendingGate, shouldBlockPendingGate, shouldBlockPendingGateBash } from "./write-gate.js";
7
7
  import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
8
8
  import { cleanupQuickBranch } from "../quick.js";
9
9
  import { getDiscussionMilestoneId } from "../guided-flow.js";
@@ -159,6 +159,36 @@ export function registerHooks(pi) {
159
159
  if (loopCheck.block) {
160
160
  return { block: true, reason: loopCheck.reason };
161
161
  }
162
+ // ── Discussion gate enforcement: track pending questions ─────────
163
+ // During a discussion flow, EVERY ask_user_questions call matters.
164
+ // When ask_user_questions is called, mark it as pending. It stays
165
+ // pending until the user responds. This prevents the model from
166
+ // continuing if ask_user_questions fails, errors, or is cancelled.
167
+ if (event.toolName === "ask_user_questions") {
168
+ const milestoneId = getDiscussionMilestoneId();
169
+ const inDiscussion = milestoneId !== null || isQueuePhaseActive();
170
+ if (inDiscussion) {
171
+ const questions = event.input?.questions ?? [];
172
+ const questionId = questions[0]?.id ?? "ask_user_questions";
173
+ setPendingGate(typeof questionId === "string" ? questionId : "ask_user_questions");
174
+ }
175
+ }
176
+ // ── Discussion gate enforcement: block tool calls while gate is pending ──
177
+ // If ask_user_questions was called with a gate ID but hasn't been confirmed,
178
+ // block all non-read-only tool calls to prevent the model from skipping gates.
179
+ if (getPendingGate()) {
180
+ const milestoneId = getDiscussionMilestoneId();
181
+ if (isToolCallEventType("bash", event)) {
182
+ const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive());
183
+ if (bashGuard.block)
184
+ return bashGuard;
185
+ }
186
+ else {
187
+ const gateGuard = shouldBlockPendingGate(event.toolName, milestoneId, isQueuePhaseActive());
188
+ if (gateGuard.block)
189
+ return gateGuard;
190
+ }
191
+ }
162
192
  // ── Queue-mode execution guard (#2545): block source-code mutations ──
163
193
  // When /gsd queue is active, the agent should only create milestones,
164
194
  // not execute work. Block write/edit to non-.gsd/ paths and bash commands
@@ -225,9 +255,26 @@ export function registerHooks(pi) {
225
255
  if (!milestoneId && !queueActive)
226
256
  return;
227
257
  const details = event.details;
258
+ // ── Discussion gate enforcement: handle gate question responses ──
259
+ // If the result is cancelled or has no response, the pending gate stays active
260
+ // so the model is blocked from non-read-only tools until it re-asks.
261
+ // If the user responded at all (even "needs adjustment"), clear the pending gate
262
+ // because the user engaged — the prompt handles the re-ask-after-adjustment flow.
263
+ const questions = event.input?.questions ?? [];
264
+ const currentPendingGate = getPendingGate();
265
+ if (currentPendingGate) {
266
+ if (details?.cancelled || !details?.response) {
267
+ // Gate stays pending — model will be blocked from non-read-only tools
268
+ // until it re-asks and gets a valid response
269
+ }
270
+ else {
271
+ // User responded (confirmed or requested adjustment) — clear the pending gate.
272
+ // The prompt-level instructions handle the "needs adjustment" re-ask flow.
273
+ clearPendingGate();
274
+ }
275
+ }
228
276
  if (details?.cancelled || !details?.response)
229
277
  return;
230
- const questions = event.input?.questions ?? [];
231
278
  for (const question of questions) {
232
279
  if (typeof question.id === "string" && question.id.includes("depth_verification")) {
233
280
  // Only unlock the gate if the user selected the first option (confirmation).
@@ -24,6 +24,37 @@ const QUEUE_SAFE_TOOLS = new Set([
24
24
  const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s)/;
25
25
  let depthVerificationDone = false;
26
26
  let activeQueuePhase = false;
27
+ /**
28
+ * Discussion gate enforcement state.
29
+ *
30
+ * When ask_user_questions is called with a recognized gate question ID,
31
+ * we track the pending gate. Until the gate is confirmed (user selects the
32
+ * first/recommended option), all non-read-only tool calls are blocked.
33
+ * This mechanically prevents the model from rationalizing past failed or
34
+ * cancelled gate questions.
35
+ */
36
+ let pendingGateId = null;
37
+ /**
38
+ * Recognized gate question ID patterns.
39
+ * These appear in both discuss-prepared.md (4-layer) and discuss.md (depth/requirements/roadmap).
40
+ */
41
+ const GATE_QUESTION_PATTERNS = [
42
+ "layer1_scope_gate",
43
+ "layer2_architecture_gate",
44
+ "layer3_error_gate",
45
+ "layer4_quality_gate",
46
+ "depth_verification",
47
+ ];
48
+ /**
49
+ * Tools that are safe to call while a gate is pending.
50
+ * Includes read-only tools and ask_user_questions itself (so the model can re-ask).
51
+ */
52
+ const GATE_SAFE_TOOLS = new Set([
53
+ "ask_user_questions",
54
+ "read", "grep", "find", "ls", "glob",
55
+ "search-the-web", "resolve_library", "get_library_docs", "fetch_page",
56
+ "search_and_read",
57
+ ]);
27
58
  export function isDepthVerified() {
28
59
  return depthVerificationDone;
29
60
  }
@@ -35,14 +66,94 @@ export function setQueuePhaseActive(active) {
35
66
  }
36
67
  export function resetWriteGateState() {
37
68
  depthVerificationDone = false;
69
+ pendingGateId = null;
38
70
  }
39
71
  export function clearDiscussionFlowState() {
40
72
  depthVerificationDone = false;
41
73
  activeQueuePhase = false;
74
+ pendingGateId = null;
42
75
  }
43
76
  export function markDepthVerified() {
44
77
  depthVerificationDone = true;
45
78
  }
79
+ /**
80
+ * Check whether a question ID matches a recognized gate pattern.
81
+ */
82
+ export function isGateQuestionId(questionId) {
83
+ return GATE_QUESTION_PATTERNS.some(pattern => questionId.includes(pattern));
84
+ }
85
+ /**
86
+ * Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
87
+ */
88
+ export function setPendingGate(gateId) {
89
+ pendingGateId = gateId;
90
+ }
91
+ /**
92
+ * Clear the pending gate (called when the user confirms).
93
+ */
94
+ export function clearPendingGate() {
95
+ pendingGateId = null;
96
+ }
97
+ /**
98
+ * Get the currently pending gate, if any.
99
+ */
100
+ export function getPendingGate() {
101
+ return pendingGateId;
102
+ }
103
+ /**
104
+ * Check whether a tool call should be blocked because a discussion gate
105
+ * is pending (ask_user_questions was called but not confirmed).
106
+ *
107
+ * Returns { block: true, reason } if the tool should be blocked.
108
+ * Read-only tools and ask_user_questions itself are always allowed.
109
+ */
110
+ export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive) {
111
+ if (!pendingGateId)
112
+ return { block: false };
113
+ const inDiscussion = milestoneId !== null;
114
+ const inQueue = queuePhaseActive ?? false;
115
+ if (!inDiscussion && !inQueue)
116
+ return { block: false };
117
+ if (GATE_SAFE_TOOLS.has(toolName))
118
+ return { block: false };
119
+ // Bash read-only commands are also safe
120
+ if (toolName === "bash")
121
+ return { block: false }; // bash is checked separately below
122
+ return {
123
+ block: true,
124
+ reason: [
125
+ `HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
126
+ `You MUST re-call ask_user_questions with the gate question before making any other tool calls.`,
127
+ `If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
128
+ `did not match a provided option, you MUST re-ask — never rationalize past the block.`,
129
+ `Do NOT proceed, do NOT use alternative approaches, do NOT skip the gate.`,
130
+ ].join(" "),
131
+ };
132
+ }
133
+ /**
134
+ * Check whether a bash command should be blocked because a discussion gate is pending.
135
+ * Read-only bash commands are allowed; mutating commands are blocked.
136
+ */
137
+ export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive) {
138
+ if (!pendingGateId)
139
+ return { block: false };
140
+ const inDiscussion = milestoneId !== null;
141
+ const inQueue = queuePhaseActive ?? false;
142
+ if (!inDiscussion && !inQueue)
143
+ return { block: false };
144
+ // Allow read-only bash commands
145
+ if (BASH_READ_ONLY_RE.test(command))
146
+ return { block: false };
147
+ return {
148
+ block: true,
149
+ reason: [
150
+ `HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
151
+ `You MUST re-call ask_user_questions with the gate question before running mutating commands.`,
152
+ `If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
153
+ `did not match a provided option, you MUST re-ask — never rationalize past the block.`,
154
+ ].join(" "),
155
+ };
156
+ }
46
157
  /**
47
158
  * Check whether a depth_verification answer confirms the discussion is complete.
48
159
  * Uses structural validation: the selected answer must exactly match the first
@@ -15,6 +15,10 @@ import { gsdRoot } from "./paths.js";
15
15
  const DEFAULT_EXCLUDES = [
16
16
  ".gsd/",
17
17
  ".planning/",
18
+ ".plans/",
19
+ ".claude/",
20
+ ".cursor/",
21
+ ".vscode/",
18
22
  ".git/",
19
23
  "node_modules/",
20
24
  "dist/",
@@ -170,6 +170,12 @@ const TEST_MARKERS = [
170
170
  /** Directories skipped during bounded recursive project scans. */
171
171
  const RECURSIVE_SCAN_IGNORED_DIRS = new Set([
172
172
  ".git",
173
+ ".gsd",
174
+ ".planning",
175
+ ".plans",
176
+ ".claude",
177
+ ".cursor",
178
+ ".vscode",
173
179
  "node_modules",
174
180
  ".venv",
175
181
  "venv",
@@ -1,4 +1,4 @@
1
- export { isDepthConfirmationAnswer, isDepthVerified, isQueuePhaseActive, setQueuePhaseActive, shouldBlockContextWrite, shouldBlockQueueExecution, } from "./bootstrap/write-gate.js";
1
+ export { isDepthConfirmationAnswer, isDepthVerified, isGateQuestionId, isQueuePhaseActive, setQueuePhaseActive, shouldBlockContextWrite, shouldBlockPendingGate, shouldBlockPendingGateBash, shouldBlockQueueExecution, setPendingGate, clearPendingGate, getPendingGate, } from "./bootstrap/write-gate.js";
2
2
  export default async function registerExtension(pi) {
3
3
  const { registerGsdExtension } = await import("./bootstrap/register-extension.js");
4
4
  registerGsdExtension(pi);
@@ -63,6 +63,6 @@ Then:
63
63
  13. Do not commit manually — the system auto-commits your changes after this unit completes.
64
64
  - Say: "Milestone {{milestoneId}} complete."
65
65
 
66
- **Important:** Do NOT skip the code change verification, success criteria, or definition of done verification (steps 3-5). The milestone summary must reflect actual verified outcomes, not assumed success. Verification failures BLOCK completion — there is no override. The milestone stays in its current state until issues are resolved and verification is re-run.
66
+ **Important:** Do NOT skip the code change verification, success criteria, or definition of done verification (steps 3-5). The milestone summary must reflect actual verified outcomes, not assumed success. Verification failures BLOCK completion — there is no override. The milestone stays in its current state until issues are resolved and verification is re-run. **If a verification tool itself fails, errors, or returns unexpected output, treat it as a verification failure** — never rationalize past a tool error ("tool didn't respond, assuming success" is forbidden). A tool that cannot verify is a tool that did not verify.
67
67
 
68
68
  **File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories.
@@ -109,7 +109,7 @@ Options:
109
109
  - "Needs adjustment" — user will clarify, then re-present scope
110
110
  ```
111
111
 
112
- **Do NOT proceed to Layer 2 until the user explicitly approves the scope.**
112
+ **CRITICAL — Non-bypassable gate:** Do NOT proceed to Layer 2 until the user explicitly approves the scope. If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block. "Tool not responding, I'll proceed," "auth issues," or "I'll use my recommended scope" are all **forbidden**. The gate exists to protect the user's work; treat a block as an instruction to wait, not an obstacle to work around.
113
113
 
114
114
  ---
115
115
 
@@ -120,7 +120,7 @@ Before presenting Layer 2 findings, use your available web search tools to resea
120
120
  1. Search for "[technology] [version] best practices [current year]"
121
121
  2. Search for "[technology] [version] known issues"
122
122
 
123
- Summarize findings concisely. If no search tools are available, note this and proceed using your training knowledge — don't block the discussion on missing search results.
123
+ Summarize findings concisely. If search tools fail or are unavailable, note this and proceed using your training knowledge — but do NOT use a search failure as justification to skip any gate.
124
124
 
125
125
  Present ecosystem findings at the start of Layer 2 alongside your architecture recommendation.
126
126
 
@@ -180,7 +180,7 @@ Options:
180
180
  - "Want to adjust" — user will clarify, then re-present architecture
181
181
  ```
182
182
 
183
- **Do NOT proceed to Layer 3 until the user explicitly approves the architecture.**
183
+ **CRITICAL — Non-bypassable gate:** Do NOT proceed to Layer 3 until the user explicitly approves the architecture. If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block. The gate exists to protect the user's work; treat a block as an instruction to wait, not an obstacle to work around.
184
184
 
185
185
  ---
186
186
 
@@ -243,7 +243,7 @@ Options:
243
243
  - "Want to adjust error handling" — user will clarify, then re-present errors
244
244
  ```
245
245
 
246
- **Do NOT proceed to Layer 4 until the user explicitly approves error handling.**
246
+ **CRITICAL — Non-bypassable gate:** Do NOT proceed to Layer 4 until the user explicitly approves error handling. If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block. The gate exists to protect the user's work; treat a block as an instruction to wait, not an obstacle to work around.
247
247
 
248
248
  ---
249
249
 
@@ -297,7 +297,7 @@ Options:
297
297
  - "Want to adjust the quality bar" — user will clarify, then re-present quality
298
298
  ```
299
299
 
300
- **Do NOT proceed to Output Phase until the user explicitly approves the quality bar.**
300
+ **CRITICAL — Non-bypassable gate:** Do NOT proceed to Output Phase until the user explicitly approves the quality bar. If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block. The gate exists to protect the user's work; treat a block as an instruction to wait, not an obstacle to work around.
301
301
 
302
302
  ---
303
303
 
@@ -315,13 +315,13 @@ Before writing a roadmap, produce or update `.gsd/REQUIREMENTS.md`.
315
315
 
316
316
  Use it as the project's explicit capability contract. Requirements discovered during the 4-layer discussion should be captured here with source `user` or `inferred` as appropriate.
317
317
 
318
- **Print the requirements in chat before writing the roadmap.** Print a markdown table with columns: ID, Title, Status, Owner, Source. Group by status (Active, Deferred, Out of Scope). After the table, ask: "Confirm, adjust, or add?"
318
+ **Print the requirements in chat before writing the roadmap.** Print a markdown table with columns: ID, Title, Status, Owner, Source. Group by status (Active, Deferred, Out of Scope). After the table, ask: "Confirm, adjust, or add?" **Non-bypassable:** If the user does not respond or gives an ambiguous answer, you MUST re-ask — never proceed to roadmap creation without explicit requirement confirmation.
319
319
 
320
320
  ### Roadmap Preview
321
321
 
322
322
  Before writing any files, **print the planned roadmap in chat** so the user can see and approve it. Print a markdown table with columns: Slice, Title, Risk, Depends, Demo. One row per slice. Below the table, print the milestone definition of done as a bullet list.
323
323
 
324
- If the user raises a substantive objection, adjust the roadmap. Otherwise, present the roadmap and ask: "Ready to write, or want to adjust?" — one gate, not two.
324
+ If the user raises a substantive objection, adjust the roadmap. Otherwise, present the roadmap and ask: "Ready to write, or want to adjust?" — one gate, not two. **Non-bypassable:** If the user does not respond or gives an ambiguous answer, you MUST re-ask — never write files without explicit approval. A missing response is not a "yes."
325
325
 
326
326
  ### Naming Convention
327
327
 
@@ -173,7 +173,7 @@ For multi-milestone projects, requirements should span the full vision. Requirem
173
173
 
174
174
  If the project is new or has no `REQUIREMENTS.md`, surface candidate requirements in chat before writing the roadmap. Ask for correction only on material omissions, wrong ownership, or wrong scope. If the user has already been specific and raises no substantive objection, treat the requirement set as confirmed and continue.
175
175
 
176
- **Print the requirements in chat before writing the roadmap.** Do not say "here are the requirements" and then only write them to a file. The user must see them in the terminal. Print a markdown table with columns: ID, Title, Status, Owner, Source. Group by status (Active, Deferred, Out of Scope). After the table, ask: "Confirm, adjust, or add?"
176
+ **Print the requirements in chat before writing the roadmap.** Do not say "here are the requirements" and then only write them to a file. The user must see them in the terminal. Print a markdown table with columns: ID, Title, Status, Owner, Source. Group by status (Active, Deferred, Out of Scope). After the table, ask: "Confirm, adjust, or add?" **Non-bypassable:** If the user does not respond or gives an ambiguous answer, you MUST re-ask — never proceed to roadmap creation without explicit requirement confirmation.
177
177
 
178
178
  ## Scope Assessment
179
179
 
@@ -185,7 +185,7 @@ Before moving to output, confirm the size estimate from your reflection still ho
185
185
 
186
186
  Before writing any files, **print the planned roadmap in chat** so the user can see and approve it. Print a markdown table with columns: Slice, Title, Risk, Depends, Demo. One row per slice. Below the table, print the milestone definition of done as a bullet list.
187
187
 
188
- If the user raises a substantive objection, adjust the roadmap. Otherwise, present the roadmap and ask: "Ready to write, or want to adjust?" — one gate, not two.
188
+ If the user raises a substantive objection, adjust the roadmap. Otherwise, present the roadmap and ask: "Ready to write, or want to adjust?" — one gate, not two. **Non-bypassable:** If the user does not respond or gives an ambiguous answer, you MUST re-ask — never write files without explicit approval. A missing response is not a "yes."
189
189
 
190
190
  ### Naming Convention
191
191
 
@@ -242,7 +242,7 @@ If a milestone has no dependencies, omit the frontmatter. The dependency chain f
242
242
 
243
243
  #### Phase 3: Sequential readiness gate for remaining milestones
244
244
 
245
- For each remaining milestone **one at a time, in sequence**, decide the most likely readiness mode from the evidence you already have, then use `ask_user_questions` to let the user correct that recommendation. Present three options:
245
+ For each remaining milestone **one at a time, in sequence**, decide the most likely readiness mode from the evidence you already have, then use `ask_user_questions` to let the user correct that recommendation. **Non-bypassable:** If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block or auto-select a readiness mode. Present three options:
246
246
 
247
247
  - **"Discuss now"** — The user wants to conduct a focused discussion for this milestone in the current session, while the context from the broader discussion is still fresh. Proceed with a focused discussion for this milestone (reflection → investigation → questioning → depth verification). When the discussion concludes, write a full `CONTEXT.md`. Then move to the gate for the next milestone.
248
248
  - **"Write draft for later"** — This milestone has seed material from the current conversation but needs its own dedicated discussion in a future session. Write a `CONTEXT-DRAFT.md` capturing the seed material (what was discussed, key ideas, provisional scope, open questions). Mark it clearly as a draft, not a finalized context. **What happens downstream:** When auto-mode reaches this milestone, it pauses and notifies the user: "M00x has draft context — needs discussion. Run /gsd." The `/gsd` wizard shows a "Discuss from draft" option that seeds the new discussion with this draft, so nothing from the current conversation is lost. After the dedicated discussion produces a full CONTEXT.md, the draft file is automatically deleted.
@@ -94,13 +94,13 @@ Before moving to the wrap-up gate, verify you have covered:
94
94
  - options: "Yes, you got it (Recommended)", "Not quite — let me clarify"
95
95
  - **The question ID must contain `depth_verification`** (e.g. `depth_verification_confirm`) — this enables the write-gate downstream.
96
96
 
97
- **If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text: "Did I capture that correctly? If not, tell me what I missed." Wait for confirmation before proceeding.
97
+ **If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text: "Did I capture that correctly? If not, tell me what I missed." Wait for explicit confirmation before proceeding. **The same non-bypassable gate applies to the plain-text path** — if the user does not respond, gives an ambiguous answer, or does not explicitly confirm, you MUST re-ask. Never rationalize past a missing confirmation.
98
98
 
99
99
  If they clarify, absorb the correction and re-verify.
100
100
 
101
101
  The depth verification is the only required confirmation gate. Do not add a second "ready to proceed?" gate after it.
102
102
 
103
- **CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option. If the user declines, cancels, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
103
+ **CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option (structured path) or explicitly confirms (plain-text path). If the user declines, cancels, does not respond, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
104
104
 
105
105
  ---
106
106
 
@@ -41,11 +41,13 @@ After each round of answers, decide whether you already have enough signal to wr
41
41
  - Ask a single wrap-up question only when you genuinely believe the slice is well understood or the user signals they want to stop.
42
42
  - When you do ask it, offer two choices: "Write the context file" *(recommended when the slice is well understood)* or "One more pass". Use `ask_user_questions` if available, otherwise ask in plain text.
43
43
 
44
+ **CRITICAL — Non-bypassable gate:** Do NOT write the context file until the user explicitly selects "Write the context file." If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block. "Tool not responding, I'll proceed," "auth issues," or "the slice seems well understood, I'll write it" are all **forbidden**. The gate exists to protect the user's work; treat a block as an instruction to wait, not an obstacle to work around.
45
+
44
46
  ---
45
47
 
46
48
  ## Output
47
49
 
48
- Once the user is ready to wrap up:
50
+ Once the user has explicitly confirmed they are ready to write the context file:
49
51
 
50
52
  1. Use the **Slice Context** output template below
51
53
  2. `mkdir -p {{sliceDirPath}}`
@@ -12,7 +12,7 @@ You are a project reorganization assistant for a GSD (Get Shit Done) project. Th
12
12
 
13
13
  1. Present the current milestone order as a clear numbered list with status indicators (e.g. ✅ complete, ▶ active, ⏳ pending, ⏸ parked)
14
14
  2. Ask: **"What would you like to change?"**
15
- 3. Execute changes conversationally, confirming destructive operations before proceeding
15
+ 3. Execute changes conversationally, confirming destructive operations before proceeding. **Non-bypassable:** For any destructive operation (discard, skip, reorder that breaks dependencies), you MUST get explicit user confirmation before executing. If the user does not respond, gives an ambiguous answer, or `ask_user_questions` fails, you MUST re-ask — never rationalize past the block. A missing confirmation is a "do not proceed."
16
16
 
17
17
  ## Supported Operations
18
18
 
@@ -53,8 +53,12 @@ gsd_skip_slice({ milestoneId: "M003", sliceId: "S02", reason: "Descoped — feat
53
53
  Skipped slices are treated as closed by the state machine (like "complete" but distinct). Use when a slice is no longer needed or has been superseded. The slice data is preserved for reference.
54
54
  **Do NOT** just check the slice checkbox in the roadmap — this does not update the DB and auto-mode will resume the slice.
55
55
 
56
+ **CRITICAL — Non-bypassable gate:** Skipping a slice is a permanent DB operation. You MUST confirm with the user before calling `gsd_skip_slice`. If the user does not respond or gives an ambiguous answer, you MUST re-ask — never proceed without explicit approval.
57
+
56
58
  ### Discard a milestone
57
- **Permanently** delete a milestone directory and prune it from QUEUE-ORDER.json. **Always confirm with the user before discarding.** Warn explicitly if the milestone has completed work.
59
+ **Permanently** delete a milestone directory and prune it from QUEUE-ORDER.json.
60
+
61
+ **CRITICAL — Non-bypassable gate:** Discarding is irreversible. You MUST confirm with the user before discarding. Warn explicitly if the milestone has completed work. If the user does not respond or gives an ambiguous answer, you MUST re-ask — never rationalize past the block. A missing confirmation is a "do not discard."
58
62
 
59
63
  ### Add a new milestone
60
64
  Use the `gsd_milestone_generate_id` tool to get the next ID, then call `gsd_summary_save` with `milestone_id: {ID}`, `artifact_type: "CONTEXT"`, and the scope/goals/success criteria as `content` — the tool writes the context file to disk and persists to DB. Update QUEUE-ORDER.json to place it at the desired position.
@@ -38,7 +38,7 @@ GSD ships with bundled skills. Load the relevant skill file with the `read` tool
38
38
  - Never print, echo, log, or restate secrets or credentials. Report only key names and applied/skipped status.
39
39
  - Never ask the user to edit `.env` files or set secrets manually. Use `secure_env_collect`.
40
40
  - In enduring files, write current state only unless the file is explicitly historical.
41
- - **Never take outward-facing actions on GitHub (or any external service) without explicit user confirmation.** This includes: creating issues, closing issues, merging PRs, approving PRs, posting comments, pushing to remote branches, publishing packages, or any other action that affects state outside the local filesystem. Read-only operations (listing, viewing, diffing) are fine. Always present what you intend to do and get a clear "yes" before executing.
41
+ - **Never take outward-facing actions on GitHub (or any external service) without explicit user confirmation.** This includes: creating issues, closing issues, merging PRs, approving PRs, posting comments, pushing to remote branches, publishing packages, or any other action that affects state outside the local filesystem. Read-only operations (listing, viewing, diffing) are fine. Always present what you intend to do and get a clear "yes" before executing. **Non-bypassable:** If the user does not respond, gives an ambiguous answer, or `ask_user_questions` fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). A missing "yes" is a "no."
42
42
 
43
43
  If a `GSD Skill Preferences` block is present below this contract, treat it as explicit durable guidance for which skills to use, prefer, or avoid during GSD work. Follow it where it does not conflict with required GSD artifact rules, verification requirements, or higher-priority system/developer instructions.
44
44
 
@@ -51,7 +51,7 @@ For each capture, classify it as one of:
51
51
 
52
52
  For captures classified as **note** or **defer**, auto-confirm without asking — these are low-impact.
53
53
  For captures classified as **stop** or **backtrack**, auto-confirm without asking — these are urgent user directives that must be honored immediately.
54
- For captures classified as **quick-task**, **inject**, or **replan**, ask the user to confirm or choose a different classification.
54
+ For captures classified as **quick-task**, **inject**, or **replan**, ask the user to confirm or choose a different classification. **Non-bypassable:** If `ask_user_questions` fails, errors, or the user does not respond, you MUST re-ask — never auto-confirm these classifications without explicit user approval.
55
55
 
56
56
  3. **Update** `.gsd/CAPTURES.md` — for each capture, update its section with the confirmed classification:
57
57
  - Change `**Status:** pending` to `**Status:** resolved`
@@ -90,9 +90,11 @@ Present a merge plan to the user:
90
90
 
91
91
  Ask the user to confirm the merge plan before proceeding.
92
92
 
93
+ **CRITICAL — Non-bypassable gate:** Do NOT execute any merge commands until the user explicitly approves the merge plan. If `ask_user_questions` fails, errors, returns no response, or the user's response is ambiguous, you MUST re-ask — never rationalize past the block. "No response, I'll proceed with the clean merges," "the plan looks safe, merging," or any other self-authorization is **forbidden**. The gate exists to protect the user's branches; treat a block as an instruction to wait, not an obstacle to work around.
94
+
93
95
  ### Step 4: Execute Merge
94
96
 
95
- Once confirmed, run all commands from `{{mainTreePath}}` (your CWD):
97
+ Once the user has explicitly confirmed, run all commands from `{{mainTreePath}}` (your CWD):
96
98
 
97
99
  1. Ensure you are on the target branch: `git checkout {{mainBranch}}`
98
100
  2. If there are conflicts requiring manual reconciliation, apply the reconciled versions first
@@ -8,6 +8,14 @@ import { SlackAdapter } from "./slack-adapter.js";
8
8
  import { TelegramAdapter } from "./telegram-adapter.js";
9
9
  import { createPromptRecord, writePromptRecord, markPromptAnswered, markPromptDispatched, markPromptStatus, updatePromptRecord } from "./store.js";
10
10
  import { sanitizeError } from "../shared/sanitize.js";
11
+ /**
12
+ * Check whether a remote channel is configured without triggering any
13
+ * side effects (no HTTP requests, no prompt records). Used by the race
14
+ * logic to decide routing before committing to a remote dispatch.
15
+ */
16
+ export function isRemoteConfigured() {
17
+ return resolveRemoteConfig() !== null;
18
+ }
11
19
  export async function tryRemoteQuestions(questions, signal) {
12
20
  const config = resolveRemoteConfig();
13
21
  if (!config)
@@ -1 +1 @@
1
- UR2XjRqocvGBpbjt8dHCS
1
+ TCMOrUQ1suscla6CiwNya