gsd-pi 2.71.0-dev.e17e0ce → 2.72.0-dev.593fa74

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 (169) hide show
  1. package/README.md +34 -1
  2. package/dist/cli.js +17 -0
  3. package/dist/mcp-server.js +37 -14
  4. package/dist/resources/agents/debugger.md +58 -0
  5. package/dist/resources/agents/doc-writer.md +43 -0
  6. package/dist/resources/agents/git-ops.md +56 -0
  7. package/dist/resources/agents/javascript-pro.md +46 -271
  8. package/dist/resources/agents/planner.md +55 -0
  9. package/dist/resources/agents/refactorer.md +47 -0
  10. package/dist/resources/agents/reviewer.md +48 -0
  11. package/dist/resources/agents/security.md +59 -0
  12. package/dist/resources/agents/tester.md +50 -0
  13. package/dist/resources/agents/typescript-pro.md +41 -235
  14. package/dist/resources/extensions/claude-code-cli/partial-builder.js +40 -12
  15. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +103 -6
  16. package/dist/resources/extensions/gsd/auto/phases.js +4 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
  18. package/dist/resources/extensions/gsd/auto-start.js +24 -4
  19. package/dist/resources/extensions/gsd/auto.js +4 -0
  20. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  21. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +2 -5
  22. package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
  23. package/dist/resources/extensions/gsd/error-classifier.js +4 -1
  24. package/dist/resources/extensions/gsd/gate-registry.js +208 -0
  25. package/dist/resources/extensions/gsd/gsd-db.js +41 -0
  26. package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
  27. package/dist/resources/extensions/gsd/notification-overlay.js +26 -12
  28. package/dist/resources/extensions/gsd/notification-store.js +5 -4
  29. package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
  30. package/dist/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  31. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  32. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  33. package/dist/resources/extensions/gsd/shortcut-defs.js +7 -1
  34. package/dist/resources/extensions/gsd/state.js +9 -2
  35. package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
  36. package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
  37. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
  38. package/dist/resources/extensions/ollama/index.js +13 -5
  39. package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
  40. package/dist/resources/extensions/subagent/agents.js +8 -0
  41. package/dist/resources/extensions/subagent/index.js +17 -0
  42. package/dist/startup-model-validation.d.ts +0 -1
  43. package/dist/startup-model-validation.js +6 -2
  44. package/dist/web/standalone/.next/BUILD_ID +1 -1
  45. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  46. package/dist/web/standalone/.next/build-manifest.json +2 -2
  47. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +1 -1
  77. package/packages/mcp-server/dist/server.d.ts +12 -1
  78. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  79. package/packages/mcp-server/dist/server.js +90 -42
  80. package/packages/mcp-server/dist/server.js.map +1 -1
  81. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  82. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  83. package/packages/mcp-server/src/server.ts +110 -38
  84. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  85. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
  86. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
  88. package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +5 -0
  90. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/retry-handler.js +55 -1
  92. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +57 -0
  94. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +87 -12
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
  106. package/packages/pi-coding-agent/package.json +1 -1
  107. package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
  108. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +83 -0
  109. package/packages/pi-coding-agent/src/core/retry-handler.ts +60 -1
  110. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +72 -0
  111. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
  112. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -12
  113. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
  114. package/pkg/package.json +1 -1
  115. package/src/resources/agents/debugger.md +58 -0
  116. package/src/resources/agents/doc-writer.md +43 -0
  117. package/src/resources/agents/git-ops.md +56 -0
  118. package/src/resources/agents/javascript-pro.md +46 -271
  119. package/src/resources/agents/planner.md +55 -0
  120. package/src/resources/agents/refactorer.md +47 -0
  121. package/src/resources/agents/reviewer.md +48 -0
  122. package/src/resources/agents/security.md +59 -0
  123. package/src/resources/agents/tester.md +50 -0
  124. package/src/resources/agents/typescript-pro.md +41 -235
  125. package/src/resources/extensions/claude-code-cli/partial-builder.ts +45 -12
  126. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +109 -3
  127. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +91 -2
  128. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +133 -2
  129. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  130. package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
  131. package/src/resources/extensions/gsd/auto-start.ts +31 -4
  132. package/src/resources/extensions/gsd/auto.ts +4 -0
  133. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  134. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +2 -5
  135. package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
  136. package/src/resources/extensions/gsd/error-classifier.ts +4 -1
  137. package/src/resources/extensions/gsd/gate-registry.ts +251 -0
  138. package/src/resources/extensions/gsd/gsd-db.ts +51 -0
  139. package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
  140. package/src/resources/extensions/gsd/notification-overlay.ts +27 -11
  141. package/src/resources/extensions/gsd/notification-store.ts +5 -4
  142. package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
  143. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  144. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  145. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  146. package/src/resources/extensions/gsd/shortcut-defs.ts +8 -1
  147. package/src/resources/extensions/gsd/state.ts +13 -2
  148. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +14 -0
  149. package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
  150. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
  151. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +16 -0
  152. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
  153. package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
  154. package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
  155. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
  156. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +3 -2
  157. package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
  158. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
  159. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
  160. package/src/resources/extensions/gsd/types.ts +26 -0
  161. package/src/resources/extensions/ollama/index.ts +13 -3
  162. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
  163. package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
  164. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
  165. package/src/resources/extensions/subagent/agents.ts +10 -0
  166. package/src/resources/extensions/subagent/index.ts +18 -0
  167. package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
  168. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → h8B07q4xc-ujHRD7esO6O}/_buildManifest.js +0 -0
  169. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → h8B07q4xc-ujHRD7esO6O}/_ssgManifest.js +0 -0
@@ -15,7 +15,8 @@ import { getLoadedSkills } from "@gsd/pi-coding-agent";
15
15
  import { join, basename } from "node:path";
16
16
  import { existsSync } from "node:fs";
17
17
  import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
18
- import { getPendingGates } from "./gsd-db.js";
18
+ import { getPendingGatesForTurn } from "./gsd-db.js";
19
+ import { assertGateCoverage, getGatesForTurn, } from "./gate-registry.js";
19
20
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
20
21
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
21
22
  import { logWarning } from "./workflow-logger.js";
@@ -1221,6 +1222,13 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
1221
1222
  ? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
1222
1223
  : "";
1223
1224
  const phaseAnchorSection = planAnchor ? formatAnchorForPrompt(planAnchor) : "";
1225
+ // Task-scoped gates owned by execute-task (Q5/Q6/Q7). Pull only the
1226
+ // gates that plan-slice actually seeded for this task — tasks with no
1227
+ // external dependencies legitimately skip Q5, tasks with no runtime
1228
+ // load dimension skip Q6, etc.
1229
+ const etPending = getPendingGatesForTurn(mid, sid, "execute-task", tid);
1230
+ assertGateCoverage(etPending, "execute-task", { requireAll: false });
1231
+ const gatesToClose = renderGatesToCloseBlock(getGatesForTurn("execute-task"), { pending: new Set(etPending.map((g) => g.gate_id)), allowOmit: true });
1224
1232
  return loadPrompt("execute-task", {
1225
1233
  overridesSection,
1226
1234
  runtimeContext,
@@ -1238,6 +1246,7 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
1238
1246
  taskSummaryPath,
1239
1247
  inlinedTemplates,
1240
1248
  verificationBudget,
1249
+ gatesToClose,
1241
1250
  skillActivation: buildSkillActivationBlock({
1242
1251
  base,
1243
1252
  milestoneId: mid,
@@ -1298,6 +1307,15 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
1298
1307
  const sliceRel = relSlicePath(base, mid, sid);
1299
1308
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
1300
1309
  const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
1310
+ // Gates owned by complete-slice (e.g. Q8). Pull from the DB so the
1311
+ // prompt only prompts for gates the plan actually seeded. The tool
1312
+ // handler closes each gate based on the SUMMARY.md section content
1313
+ // after the assistant calls gsd_complete_slice.
1314
+ const csPending = getPendingGatesForTurn(mid, sid, "complete-slice");
1315
+ // coverage check: every pending row must be owned by complete-slice.
1316
+ // requireAll:false because a slice may have already closed some gates.
1317
+ assertGateCoverage(csPending, "complete-slice", { requireAll: false });
1318
+ const gatesToClose = renderGatesToCloseBlock(getGatesForTurn("complete-slice"), { pending: new Set(csPending.map((g) => g.gate_id)), allowOmit: true });
1301
1319
  return loadPrompt("complete-slice", {
1302
1320
  workingDirectory: base,
1303
1321
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
@@ -1306,6 +1324,7 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
1306
1324
  inlinedContext,
1307
1325
  sliceSummaryPath,
1308
1326
  sliceUatPath,
1327
+ gatesToClose,
1309
1328
  });
1310
1329
  }
1311
1330
  export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
@@ -1498,6 +1517,15 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
1498
1517
  const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1499
1518
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
1500
1519
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
1520
+ // Every milestone validation turn owns MV01–MV04 unconditionally: the
1521
+ // registry is the source of truth for which gates the validator must
1522
+ // address, and the block below is what the template renders so the
1523
+ // assistant can never accidentally skip one.
1524
+ const mvGates = getGatesForTurn("validate-milestone");
1525
+ const gatesToEvaluate = renderGatesToCloseBlock(mvGates, {
1526
+ pending: new Set(mvGates.map((g) => g.id)),
1527
+ allowOmit: false,
1528
+ });
1501
1529
  return loadPrompt("validate-milestone", {
1502
1530
  workingDirectory: base,
1503
1531
  milestoneId: mid,
@@ -1506,6 +1534,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
1506
1534
  inlinedContext,
1507
1535
  validationPath: validationOutputPath,
1508
1536
  remediationRound: String(remediationRound),
1537
+ gatesToEvaluate,
1509
1538
  skillActivation: buildSkillActivationBlock({
1510
1539
  base,
1511
1540
  milestoneId: mid,
@@ -1740,26 +1769,43 @@ export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, rea
1740
1769
  });
1741
1770
  }
1742
1771
  // ─── Gate Evaluation ──────────────────────────────────────────────────────
1743
- const GATE_QUESTIONS = {
1744
- Q3: {
1745
- question: "How can this be exploited?",
1746
- guidance: [
1747
- "Identify abuse scenarios: parameter tampering, replay attacks, privilege escalation.",
1748
- "Map data exposure risks: PII, tokens, secrets accessible through this slice.",
1749
- "Define input trust boundaries: untrusted user input reaching DB, API, or filesystem.",
1750
- "If none apply, return verdict 'omitted' with rationale explaining why.",
1751
- ].join("\n"),
1752
- },
1753
- Q4: {
1754
- question: "What existing promises does this break?",
1755
- guidance: [
1756
- "List which existing requirements (R001, R003, etc.) are touched by this slice.",
1757
- "Identify what must be re-tested after shipping.",
1758
- "Flag decisions that should be revisited given the new scope.",
1759
- "If no existing requirements are affected, return verdict 'omitted'.",
1760
- ].join("\n"),
1761
- },
1762
- };
1772
+ //
1773
+ // Gate definitions (question, guidance, owner turn) now live in
1774
+ // gate-registry.ts so that prompt builders, dispatch rules, state
1775
+ // derivation, and tool handlers all consult the same source of truth.
1776
+ // See gate-registry.ts for the full ownership map.
1777
+ /**
1778
+ * Render a "Gates to Close" block for turns like `complete-slice` and
1779
+ * `validate-milestone` that own gates which are closed as a side-effect
1780
+ * of writing artifact sections (not via a dedicated gate-evaluate
1781
+ * subagent loop).
1782
+ *
1783
+ * Returns a plain-text block or an empty string if there are no gates to
1784
+ * close, so callers can drop it straight into a template variable.
1785
+ */
1786
+ function renderGatesToCloseBlock(gates, opts) {
1787
+ const applicable = gates.filter((g) => opts.pending.has(g.id));
1788
+ if (applicable.length === 0)
1789
+ return "";
1790
+ const lines = [];
1791
+ lines.push("## Gates to Close");
1792
+ lines.push("");
1793
+ lines.push("These quality gates are still pending for this unit. You MUST address every one before calling the closing tool — the handler closes the DB row based on whether the corresponding artifact section is present.");
1794
+ lines.push("");
1795
+ for (const def of applicable) {
1796
+ lines.push(`### ${def.id} — ${def.promptSection}`);
1797
+ lines.push("");
1798
+ lines.push(`**Question:** ${def.question}`);
1799
+ lines.push("");
1800
+ lines.push(def.guidance);
1801
+ if (opts.allowOmit) {
1802
+ lines.push("");
1803
+ lines.push(`If this gate genuinely does not apply to this unit, leave the **${def.promptSection}** section empty and the handler will record it as \`omitted\`. Otherwise, fill the section with concrete evidence.`);
1804
+ }
1805
+ lines.push("");
1806
+ }
1807
+ return lines.join("\n").trimEnd();
1808
+ }
1763
1809
  export async function buildParallelResearchSlicesPrompt(mid, midTitle, slices, basePath) {
1764
1810
  // Build individual research-slice prompts for each slice
1765
1811
  const subagentSections = [];
@@ -1784,24 +1830,33 @@ export async function buildParallelResearchSlicesPrompt(mid, midTitle, slices, b
1784
1830
  });
1785
1831
  }
1786
1832
  export async function buildGateEvaluatePrompt(mid, midTitle, sid, sTitle, base) {
1787
- const pending = getPendingGates(mid, sid, "slice");
1833
+ // Pull only the gates this turn actually owns (Q3/Q4). Filter via the
1834
+ // registry so that scope:"slice" gates owned by other turns (Q8) can't
1835
+ // leak into this prompt and can't block dispatch via silent skip.
1836
+ const pending = getPendingGatesForTurn(mid, sid, "gate-evaluate");
1837
+ // Fails loudly if the pending list contains a gate id the registry
1838
+ // doesn't own for this turn. Missing owned gates is allowed here —
1839
+ // `gate-evaluate` is dispatched whenever *any* of its owned gates are
1840
+ // pending, not only when all of them are.
1841
+ assertGateCoverage(pending, "gate-evaluate", { requireAll: false });
1788
1842
  // Load the slice plan for context
1789
1843
  const planFile = resolveSliceFile(base, mid, sid, "PLAN");
1790
1844
  const planContent = planFile ? (await loadFile(planFile)) ?? "(plan file empty)" : "(plan file not found)";
1791
- // Build per-gate subagent prompts
1845
+ // Build per-gate subagent prompts from the pending rows. Because the
1846
+ // registry has already validated every row, `getGateDefinition` cannot
1847
+ // return undefined here.
1848
+ const pendingIds = new Set(pending.map((g) => g.gate_id));
1849
+ const gateDefs = getGatesForTurn("gate-evaluate").filter((def) => pendingIds.has(def.id));
1792
1850
  const subagentSections = [];
1793
1851
  const gateListLines = [];
1794
- for (const gate of pending) {
1795
- const meta = GATE_QUESTIONS[gate.gate_id];
1796
- if (!meta)
1797
- continue;
1798
- gateListLines.push(`- **${gate.gate_id}**: ${meta.question}`);
1852
+ for (const def of gateDefs) {
1853
+ gateListLines.push(`- **${def.id}**: ${def.question}`);
1799
1854
  const subPrompt = [
1800
- `You are evaluating quality gate **${gate.gate_id}** for slice ${sid} (${sTitle}).`,
1855
+ `You are evaluating quality gate **${def.id}** for slice ${sid} (${sTitle}).`,
1801
1856
  "",
1802
- `## Question: ${meta.question}`,
1857
+ `## Question: ${def.question}`,
1803
1858
  "",
1804
- meta.guidance,
1859
+ def.guidance,
1805
1860
  "",
1806
1861
  "## Slice Plan",
1807
1862
  "",
@@ -1813,13 +1868,13 @@ export async function buildGateEvaluatePrompt(mid, midTitle, sid, sTitle, base)
1813
1868
  `Call the \`gsd_save_gate_result\` tool with:`,
1814
1869
  `- \`milestoneId\`: "${mid}"`,
1815
1870
  `- \`sliceId\`: "${sid}"`,
1816
- `- \`gateId\`: "${gate.gate_id}"`,
1871
+ `- \`gateId\`: "${def.id}"`,
1817
1872
  "- `verdict`: \"pass\" (no concerns), \"flag\" (concerns found), or \"omitted\" (not applicable)",
1818
1873
  "- `rationale`: one-sentence justification",
1819
1874
  "- `findings`: detailed markdown findings (or empty if omitted)",
1820
1875
  ].join("\n");
1821
1876
  subagentSections.push([
1822
- `### ${gate.gate_id}: ${meta.question}`,
1877
+ `### ${def.id}: ${def.question}`,
1823
1878
  "",
1824
1879
  "Use this as the prompt for a `subagent` call:",
1825
1880
  "",
@@ -190,16 +190,33 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
190
190
  //
191
191
  // Precedence:
192
192
  // 1) Explicit session override via /gsd model (this session)
193
- // 2) GSD model preferences from PREFERENCES.md
194
- // 3) Current session model from settings/session restore
193
+ // 2) GSD model preferences from PREFERENCES.md (validated against live auth)
194
+ // 3) Current session model from settings/session restore (if provider ready)
195
195
  //
196
196
  // This preserves #3517 defaults while honoring explicit runtime model
197
197
  // selection for subsequent /gsd runs in the same session.
198
198
  const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
199
199
  const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
200
+ // Validate the preferred model against the live registry + provider auth so
201
+ // an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
202
+ // start-model snapshot. Without this, every subsequent unit would try to
203
+ // fall back to an unusable model.
204
+ let validatedPreferredModel;
205
+ if (preferredModel) {
206
+ const { resolveModelId } = await import("./auto-model-selection.js");
207
+ const available = ctx.modelRegistry.getAvailable();
208
+ const match = resolveModelId(`${preferredModel.provider}/${preferredModel.id}`, available, ctx.model?.provider);
209
+ if (match) {
210
+ validatedPreferredModel = { provider: match.provider, id: match.id };
211
+ }
212
+ else {
213
+ ctx.ui.notify(`Preferred model ${preferredModel.provider}/${preferredModel.id} from PREFERENCES.md is not configured; falling back to session default.`, "warning");
214
+ }
215
+ }
216
+ const sessionModelReady = ctx.model && ctx.modelRegistry.isProviderRequestReady(ctx.model.provider);
200
217
  const startModelSnapshot = manualSessionOverride
201
- ?? preferredModel
202
- ?? (ctx.model
218
+ ?? validatedPreferredModel
219
+ ?? (sessionModelReady && ctx.model
203
220
  ? { provider: ctx.model.provider, id: ctx.model.id }
204
221
  : null);
205
222
  try {
@@ -453,6 +470,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
453
470
  // Successfully resolved an active milestone — reset the re-entry guard
454
471
  s.consecutiveCompleteBootstraps = 0;
455
472
  // ── Initialize session state ──
473
+ // Notify shared phase state so subagent conflict checks can fire
474
+ const { activateGSD: activateGSDPhaseState } = await import("../shared/gsd-phase-state.js");
475
+ activateGSDPhaseState();
456
476
  s.active = true;
457
477
  s.stepMode = requestedStepMode;
458
478
  s.verbose = verboseMode;
@@ -34,6 +34,7 @@ import { preDispatchHealthGate, resetProactiveHealing, setLevelChangeCallback, }
34
34
  import { clearSkillSnapshot } from "./skill-discovery.js";
35
35
  import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
36
36
  import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
37
+ import { deactivateGSD } from "../shared/gsd-phase-state.js";
37
38
  import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
38
39
  import { logWarning } from "./workflow-logger.js";
39
40
  import { homedir } from "node:os";
@@ -374,6 +375,7 @@ function handleLostSessionLock(ctx, lockStatus) {
374
375
  });
375
376
  s.active = false;
376
377
  s.paused = false;
378
+ deactivateGSD();
377
379
  clearUnitTimeout();
378
380
  restoreProjectRootEnv();
379
381
  restoreMilestoneLockEnv();
@@ -406,6 +408,7 @@ function handleLostSessionLock(ctx, lockStatus) {
406
408
  function cleanupAfterLoopExit(ctx) {
407
409
  s.currentUnit = null;
408
410
  s.active = false;
411
+ deactivateGSD();
409
412
  clearUnitTimeout();
410
413
  restoreProjectRootEnv();
411
414
  restoreMilestoneLockEnv();
@@ -743,6 +746,7 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
743
746
  _resetPendingResolve();
744
747
  s.active = false;
745
748
  s.paused = true;
749
+ deactivateGSD();
746
750
  restoreProjectRootEnv();
747
751
  restoreMilestoneLockEnv();
748
752
  s.pendingVerificationRetry = null;
@@ -919,12 +919,12 @@ export function registerDbTools(pi) {
919
919
  const saveGateResultTool = {
920
920
  name: "gsd_save_gate_result",
921
921
  label: "Save Gate Result",
922
- description: "Save the result of a quality gate evaluation (Q3-Q8) to the GSD database. " +
922
+ description: "Save the result of a quality gate evaluation (Q3-Q8 or MV01-MV04) to the GSD database. " +
923
923
  "Called by gate evaluation sub-agents after analyzing a specific quality question.",
924
924
  promptSnippet: "Save quality gate evaluation result (verdict, rationale, findings)",
925
925
  promptGuidelines: [
926
926
  "Use gsd_save_gate_result after evaluating a quality gate question.",
927
- "gateId must be one of: Q3, Q4, Q5, Q6, Q7, Q8.",
927
+ "gateId must be one of: Q3, Q4, Q5, Q6, Q7, Q8, MV01, MV02, MV03, MV04.",
928
928
  "verdict must be: pass (no concerns), flag (concerns found), or omitted (not applicable).",
929
929
  "rationale should be a one-sentence justification for the verdict.",
930
930
  "findings should contain detailed markdown analysis (or empty string if omitted).",
@@ -932,7 +932,7 @@ export function registerDbTools(pi) {
932
932
  parameters: Type.Object({
933
933
  milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
934
934
  sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
935
- gateId: Type.String({ description: "Gate ID: Q3, Q4, Q5, Q6, Q7, or Q8" }),
935
+ gateId: Type.String({ description: "Gate ID: Q3, Q4, Q5, Q6, Q7, Q8, MV01, MV02, MV03, or MV04" }),
936
936
  taskId: Type.Optional(Type.String({ description: "Task ID for task-scoped gates (Q5/Q6/Q7)" })),
937
937
  verdict: Type.String({ description: "pass, flag, or omitted" }),
938
938
  rationale: Type.String({ description: "One-sentence justification" }),
@@ -71,9 +71,6 @@ export function registerShortcuts(pi) {
71
71
  description: shortcutDesc(GSD_SHORTCUTS.parallel.action, GSD_SHORTCUTS.parallel.command),
72
72
  handler: openParallelOverlay,
73
73
  });
74
- // Fallback for terminals where Ctrl+Alt letter chords are not forwarded reliably.
75
- pi.registerShortcut(Key.ctrlShift(GSD_SHORTCUTS.parallel.key), {
76
- description: shortcutDesc(`${GSD_SHORTCUTS.parallel.action} (fallback)`, GSD_SHORTCUTS.parallel.command),
77
- handler: openParallelOverlay,
78
- });
74
+ // No Ctrl+Shift+P fallback conflicts with cycleModelBackward (shift+ctrl+p).
75
+ // Use Ctrl+Alt+P or /gsd parallel watch instead.
79
76
  }
@@ -147,10 +147,33 @@ const PROVIDER_ROUTES = {
147
147
  openai: ["github-copilot", "openai-codex"],
148
148
  google: ["google-gemini-cli"],
149
149
  };
150
+ /**
151
+ * Providers that use external CLI authentication (not API keys).
152
+ * These are always considered "ok" — the host CLI handles auth.
153
+ */
154
+ const CLI_AUTH_PROVIDERS = new Set([
155
+ "claude-code",
156
+ "openai-codex",
157
+ "google-gemini-cli",
158
+ "google-antigravity",
159
+ ]);
150
160
  function checkLlmProviders() {
151
161
  const required = collectConfiguredModelProviders();
152
162
  const results = [];
153
163
  for (const providerId of required) {
164
+ // CLI-authenticated providers don't need API keys — skip key check
165
+ if (CLI_AUTH_PROVIDERS.has(providerId)) {
166
+ const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
167
+ results.push({
168
+ name: providerId,
169
+ label: info?.label ?? providerId,
170
+ category: "llm",
171
+ status: "ok",
172
+ message: `${info?.label ?? providerId} — CLI auth (no key needed)`,
173
+ required: true,
174
+ });
175
+ continue;
176
+ }
154
177
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
155
178
  const label = providerId === "anthropic-vertex"
156
179
  ? "Anthropic Vertex"
@@ -20,6 +20,9 @@ export function resetRetryState(state) {
20
20
  // ── Classification ──────────────────────────────────────────────────────────
21
21
  const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i;
22
22
  const RATE_LIMIT_RE = /rate.?limit|too many requests|429/i;
23
+ // OpenRouter affordability-style quota errors should be treated as transient
24
+ // so core retry logic can lower maxTokens and continue in-session.
25
+ const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
23
26
  const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i;
24
27
  const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
25
28
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
@@ -42,7 +45,7 @@ const RESET_DELAY_RE = /reset in (\d+)s/i;
42
45
  */
43
46
  export function classifyError(errorMsg, retryAfterMs) {
44
47
  const isPermanent = PERMANENT_RE.test(errorMsg);
45
- const isRateLimit = RATE_LIMIT_RE.test(errorMsg);
48
+ const isRateLimit = RATE_LIMIT_RE.test(errorMsg) || AFFORDABILITY_RE.test(errorMsg);
46
49
  // 1. Permanent — but rate limit takes precedence
47
50
  if (isPermanent && !isRateLimit) {
48
51
  return { kind: "permanent" };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * GSD Gate Registry — single source of truth for quality-gate ownership.
3
+ *
4
+ * Each gate declares which workflow turn owns it, the scope at which it is
5
+ * persisted in the `quality_gates` table, and the question/guidance text used
6
+ * in the prompt that turn sends. The registry replaces the ad-hoc
7
+ * `GATE_QUESTIONS` table that used to live in `auto-prompts.ts`, and every
8
+ * layer of the prompt system (prompt builders, dispatch rules, state
9
+ * derivation, tool handlers) consults it so a pending gate can never be
10
+ * silently dropped.
11
+ *
12
+ * Design notes:
13
+ * - `GATE_REGISTRY` is exhaustiveness-checked against `GateId` via
14
+ * `satisfies Record<GateId, GateDefinition>`, so adding a new GateId
15
+ * without a registry entry is a compile error.
16
+ * - `getGatesForTurn(turn)` returns the definitions a turn owns.
17
+ * - `assertGateCoverage(pending, turn)` throws a GSDError if the pending
18
+ * list for a turn contains unknown gates, or if any gate owned by the
19
+ * turn is missing from the pending list.
20
+ */
21
+ import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
22
+ export const GATE_REGISTRY = {
23
+ Q3: {
24
+ id: "Q3",
25
+ scope: "slice",
26
+ ownerTurn: "gate-evaluate",
27
+ question: "How can this be exploited?",
28
+ guidance: [
29
+ "Identify abuse scenarios: parameter tampering, replay attacks, privilege escalation.",
30
+ "Map data exposure risks: PII, tokens, secrets accessible through this slice.",
31
+ "Define input trust boundaries: untrusted user input reaching DB, API, or filesystem.",
32
+ "If none apply, return verdict 'omitted' with rationale explaining why.",
33
+ ].join("\n"),
34
+ promptSection: "Abuse Surface",
35
+ },
36
+ Q4: {
37
+ id: "Q4",
38
+ scope: "slice",
39
+ ownerTurn: "gate-evaluate",
40
+ question: "What existing promises does this break?",
41
+ guidance: [
42
+ "List which existing requirements (R001, R003, etc.) are touched by this slice.",
43
+ "Identify what must be re-tested after shipping.",
44
+ "Flag decisions that should be revisited given the new scope.",
45
+ "If no existing requirements are affected, return verdict 'omitted'.",
46
+ ].join("\n"),
47
+ promptSection: "Broken Promises",
48
+ },
49
+ Q5: {
50
+ id: "Q5",
51
+ scope: "task",
52
+ ownerTurn: "execute-task",
53
+ question: "What breaks when dependencies fail?",
54
+ guidance: [
55
+ "Enumerate the task's external dependencies (APIs, filesystem, network, subprocesses).",
56
+ "Describe the failure path for each: timeout, malformed response, connection loss.",
57
+ "Verify the implementation handles each failure or explicitly bubbles the error.",
58
+ "Return verdict 'omitted' only if the task has no external dependencies.",
59
+ ].join("\n"),
60
+ promptSection: "Failure Modes",
61
+ },
62
+ Q6: {
63
+ id: "Q6",
64
+ scope: "task",
65
+ ownerTurn: "execute-task",
66
+ question: "What is the 10x load breakpoint?",
67
+ guidance: [
68
+ "Identify the resource that saturates first at 10x the expected load.",
69
+ "Describe the protection applied (pool sizing, rate limiting, pagination, caching).",
70
+ "Return verdict 'omitted' if the task has no runtime load dimension.",
71
+ ].join("\n"),
72
+ promptSection: "Load Profile",
73
+ },
74
+ Q7: {
75
+ id: "Q7",
76
+ scope: "task",
77
+ ownerTurn: "execute-task",
78
+ question: "What negative tests protect this task?",
79
+ guidance: [
80
+ "List malformed inputs, error paths, and boundary conditions the tests cover.",
81
+ "Point to the specific test files or cases that assert each negative scenario.",
82
+ "Return verdict 'omitted' only if the task has no meaningful negative surface.",
83
+ ].join("\n"),
84
+ promptSection: "Negative Tests",
85
+ },
86
+ Q8: {
87
+ id: "Q8",
88
+ scope: "slice",
89
+ ownerTurn: "complete-slice",
90
+ question: "How will ops know this slice is healthy or broken?",
91
+ guidance: [
92
+ "Describe the health signal (metric, log line, dashboard) that proves the slice works.",
93
+ "Describe the failure signal that triggers an alert or paging.",
94
+ "Document the recovery procedure and any monitoring gaps.",
95
+ "Return verdict 'omitted' only for slices with no runtime behavior at all.",
96
+ ].join("\n"),
97
+ promptSection: "Operational Readiness",
98
+ },
99
+ MV01: {
100
+ id: "MV01",
101
+ scope: "milestone",
102
+ ownerTurn: "validate-milestone",
103
+ question: "Is every success criterion in the milestone roadmap satisfied?",
104
+ guidance: [
105
+ "Walk the success-criteria checklist from the milestone roadmap.",
106
+ "For each criterion, point to the slice / assessment / verification evidence that proves it.",
107
+ "Return verdict 'flag' if any criterion is unmet or unverifiable.",
108
+ ].join("\n"),
109
+ promptSection: "Success Criteria Checklist",
110
+ },
111
+ MV02: {
112
+ id: "MV02",
113
+ scope: "milestone",
114
+ ownerTurn: "validate-milestone",
115
+ question: "Does every slice have a SUMMARY.md and a passing assessment?",
116
+ guidance: [
117
+ "Confirm every slice listed in the roadmap has a SUMMARY.md.",
118
+ "Confirm each slice has an ASSESSMENT verdict of 'pass' (or justified 'omitted').",
119
+ "Flag missing artifacts and slices with outstanding follow-ups or known limitations.",
120
+ ].join("\n"),
121
+ promptSection: "Slice Delivery Audit",
122
+ },
123
+ MV03: {
124
+ id: "MV03",
125
+ scope: "milestone",
126
+ ownerTurn: "validate-milestone",
127
+ question: "Do the slices integrate end-to-end?",
128
+ guidance: [
129
+ "Trace at least one cross-slice flow proving the pieces compose.",
130
+ "Flag gaps where two slices were built in isolation with no integration evidence.",
131
+ ].join("\n"),
132
+ promptSection: "Cross-Slice Integration",
133
+ },
134
+ MV04: {
135
+ id: "MV04",
136
+ scope: "milestone",
137
+ ownerTurn: "validate-milestone",
138
+ question: "Are all touched requirements covered and still coherent?",
139
+ guidance: [
140
+ "For each requirement advanced, validated, surfaced, or invalidated across the milestone's slices, confirm the milestone-level evidence matches.",
141
+ "Flag requirements that slices claim to advance but no artifact proves.",
142
+ ].join("\n"),
143
+ promptSection: "Requirement Coverage",
144
+ },
145
+ };
146
+ /** Stable ordered lists per owner turn — iteration order matches declaration. */
147
+ const ORDERED_GATES = Object.values(GATE_REGISTRY);
148
+ /** Return every gate owned by a turn, in stable declaration order. */
149
+ export function getGatesForTurn(turn) {
150
+ return ORDERED_GATES.filter((g) => g.ownerTurn === turn);
151
+ }
152
+ /** Return the set of gate ids a turn owns. */
153
+ export function getGateIdsForTurn(turn) {
154
+ return new Set(getGatesForTurn(turn).map((g) => g.id));
155
+ }
156
+ /** Look up a definition by gate id, or undefined if unknown. */
157
+ export function getGateDefinition(id) {
158
+ return GATE_REGISTRY[id];
159
+ }
160
+ /** Look up the owner turn for a gate id. Throws if the gate is unknown. */
161
+ export function getOwnerTurn(id) {
162
+ const def = GATE_REGISTRY[id];
163
+ if (!def) {
164
+ throw new GSDError(GSD_PARSE_ERROR, `gate-registry: unknown gate id "${id}"`);
165
+ }
166
+ return def.ownerTurn;
167
+ }
168
+ /**
169
+ * Assert that the pending gate rows for a turn match what the registry says
170
+ * the turn owns. Fails loudly rather than silently skipping.
171
+ *
172
+ * - Every row in `pending` must have a definition whose `ownerTurn` matches `turn`.
173
+ * (The caller is responsible for scoping the pending list — e.g. filtering
174
+ * by slice scope before passing it in.)
175
+ * - `options.requireAll` (default true): every gate the turn owns must appear
176
+ * in `pending`. Set to false for turns like `execute-task` that only need
177
+ * coverage for the subset of gates that were seeded (e.g. tasks with no
178
+ * external dependencies have no Q5 row).
179
+ */
180
+ export function assertGateCoverage(pending, turn, options = {}) {
181
+ const requireAll = options.requireAll ?? true;
182
+ const expected = getGateIdsForTurn(turn);
183
+ const pendingIds = new Set(pending.map((g) => g.gate_id));
184
+ const unknown = [];
185
+ for (const id of pendingIds) {
186
+ const def = getGateDefinition(id);
187
+ if (!def) {
188
+ unknown.push(id);
189
+ continue;
190
+ }
191
+ if (def.ownerTurn !== turn) {
192
+ unknown.push(`${id} (owned by ${def.ownerTurn}, not ${turn})`);
193
+ }
194
+ }
195
+ if (unknown.length > 0) {
196
+ throw new GSDError(GSD_PARSE_ERROR, `assertGateCoverage: turn "${turn}" received pending gates it does not own: ${unknown.join(", ")}`);
197
+ }
198
+ if (requireAll) {
199
+ const missing = [];
200
+ for (const id of expected) {
201
+ if (!pendingIds.has(id))
202
+ missing.push(id);
203
+ }
204
+ if (missing.length > 0) {
205
+ throw new GSDError(GSD_PARSE_ERROR, `assertGateCoverage: turn "${turn}" is missing required gates: ${missing.join(", ")}`);
206
+ }
207
+ }
208
+ }
@@ -8,6 +8,7 @@ import { createRequire } from "node:module";
8
8
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
9
9
  import { dirname } from "node:path";
10
10
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
11
+ import { getGateIdsForTurn } from "./gate-registry.js";
11
12
  import { logError, logWarning } from "./workflow-logger.js";
12
13
  const _require = createRequire(import.meta.url);
13
14
  let providerName = null;
@@ -1927,3 +1928,43 @@ export function getPendingSliceGateCount(milestoneId, sliceId) {
1927
1928
  WHERE milestone_id = :mid AND slice_id = :sid AND scope = 'slice' AND status = 'pending'`).get({ ":mid": milestoneId, ":sid": sliceId });
1928
1929
  return row ? row["cnt"] : 0;
1929
1930
  }
1931
+ /**
1932
+ * Return pending gate rows owned by a specific workflow turn.
1933
+ *
1934
+ * Unlike `getPendingGates(..., scope)`, this filters by the registry's
1935
+ * `ownerTurn` metadata so callers can distinguish Q3/Q4 (owned by
1936
+ * gate-evaluate) from Q8 (owned by complete-slice) even though both are
1937
+ * scope:"slice". Pass `taskId` to narrow task-scoped results to one task.
1938
+ */
1939
+ export function getPendingGatesForTurn(milestoneId, sliceId, turn, taskId) {
1940
+ if (!currentDb)
1941
+ return [];
1942
+ const ids = getGateIdsForTurn(turn);
1943
+ if (ids.size === 0)
1944
+ return [];
1945
+ const idList = [...ids];
1946
+ const placeholders = idList.map((_, i) => `:gid${i}`).join(",");
1947
+ const params = {
1948
+ ":mid": milestoneId,
1949
+ ":sid": sliceId,
1950
+ };
1951
+ idList.forEach((id, i) => {
1952
+ params[`:gid${i}`] = id;
1953
+ });
1954
+ let sql = `SELECT * FROM quality_gates
1955
+ WHERE milestone_id = :mid AND slice_id = :sid
1956
+ AND status = 'pending'
1957
+ AND gate_id IN (${placeholders})`;
1958
+ if (taskId !== undefined) {
1959
+ sql += ` AND task_id = :tid`;
1960
+ params[":tid"] = taskId;
1961
+ }
1962
+ return currentDb.prepare(sql).all(params).map(rowToGate);
1963
+ }
1964
+ /**
1965
+ * Count pending gates for a turn. Convenience wrapper used by state
1966
+ * derivation to decide whether a phase transition should pause.
1967
+ */
1968
+ export function getPendingGateCountForTurn(milestoneId, sliceId, turn) {
1969
+ return getPendingGatesForTurn(milestoneId, sliceId, turn).length;
1970
+ }