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.
- package/README.md +34 -1
- package/dist/cli.js +17 -0
- package/dist/mcp-server.js +37 -14
- package/dist/resources/agents/debugger.md +58 -0
- package/dist/resources/agents/doc-writer.md +43 -0
- package/dist/resources/agents/git-ops.md +56 -0
- package/dist/resources/agents/javascript-pro.md +46 -271
- package/dist/resources/agents/planner.md +55 -0
- package/dist/resources/agents/refactorer.md +47 -0
- package/dist/resources/agents/reviewer.md +48 -0
- package/dist/resources/agents/security.md +59 -0
- package/dist/resources/agents/tester.md +50 -0
- package/dist/resources/agents/typescript-pro.md +41 -235
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +40 -12
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +103 -6
- package/dist/resources/extensions/gsd/auto/phases.js +4 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
- package/dist/resources/extensions/gsd/auto-start.js +24 -4
- package/dist/resources/extensions/gsd/auto.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +2 -5
- package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
- package/dist/resources/extensions/gsd/error-classifier.js +4 -1
- package/dist/resources/extensions/gsd/gate-registry.js +208 -0
- package/dist/resources/extensions/gsd/gsd-db.js +41 -0
- package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
- package/dist/resources/extensions/gsd/notification-overlay.js +26 -12
- package/dist/resources/extensions/gsd/notification-store.js +5 -4
- package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/shortcut-defs.js +7 -1
- package/dist/resources/extensions/gsd/state.js +9 -2
- package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
- package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
- package/dist/resources/extensions/ollama/index.js +13 -5
- package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
- package/dist/resources/extensions/subagent/agents.js +8 -0
- package/dist/resources/extensions/subagent/index.js +17 -0
- package/dist/startup-model-validation.d.ts +0 -1
- package/dist/startup-model-validation.js +6 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/server.d.ts +12 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +90 -42
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/server.ts +110 -38
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +55 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +87 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +83 -0
- package/packages/pi-coding-agent/src/core/retry-handler.ts +60 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +72 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -12
- package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
- package/pkg/package.json +1 -1
- package/src/resources/agents/debugger.md +58 -0
- package/src/resources/agents/doc-writer.md +43 -0
- package/src/resources/agents/git-ops.md +56 -0
- package/src/resources/agents/javascript-pro.md +46 -271
- package/src/resources/agents/planner.md +55 -0
- package/src/resources/agents/refactorer.md +47 -0
- package/src/resources/agents/reviewer.md +48 -0
- package/src/resources/agents/security.md +59 -0
- package/src/resources/agents/tester.md +50 -0
- package/src/resources/agents/typescript-pro.md +41 -235
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +45 -12
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +109 -3
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +91 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +133 -2
- package/src/resources/extensions/gsd/auto/phases.ts +4 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
- package/src/resources/extensions/gsd/auto-start.ts +31 -4
- package/src/resources/extensions/gsd/auto.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +2 -5
- package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
- package/src/resources/extensions/gsd/error-classifier.ts +4 -1
- package/src/resources/extensions/gsd/gate-registry.ts +251 -0
- package/src/resources/extensions/gsd/gsd-db.ts +51 -0
- package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
- package/src/resources/extensions/gsd/notification-overlay.ts +27 -11
- package/src/resources/extensions/gsd/notification-store.ts +5 -4
- package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/shortcut-defs.ts +8 -1
- package/src/resources/extensions/gsd/state.ts +13 -2
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +3 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
- package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
- package/src/resources/extensions/gsd/types.ts +26 -0
- package/src/resources/extensions/ollama/index.ts +13 -3
- package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
- package/src/resources/extensions/subagent/agents.ts +10 -0
- package/src/resources/extensions/subagent/index.ts +18 -0
- package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
- /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → h8B07q4xc-ujHRD7esO6O}/_buildManifest.js +0 -0
- /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 {
|
|
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
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
|
|
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
|
|
1795
|
-
|
|
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 **${
|
|
1855
|
+
`You are evaluating quality gate **${def.id}** for slice ${sid} (${sTitle}).`,
|
|
1801
1856
|
"",
|
|
1802
|
-
`## Question: ${
|
|
1857
|
+
`## Question: ${def.question}`,
|
|
1803
1858
|
"",
|
|
1804
|
-
|
|
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\`: "${
|
|
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
|
-
`### ${
|
|
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
|
-
??
|
|
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
|
|
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
|
-
//
|
|
75
|
-
|
|
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
|
+
}
|