gsd-pi 2.71.0-dev.e17e0ce → 2.72.0-dev.de4c4b3
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/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 +8 -8
- 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 +8 -8
- 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/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/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/model-selector.ts +15 -6
- 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/stream-adapter.ts +109 -3
- 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 → f-Gremw0nLxxFUySaHRPw}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → f-Gremw0nLxxFUySaHRPw}/_ssgManifest.js +0 -0
|
@@ -24,7 +24,13 @@ import { getLoadedSkills, type Skill } from "@gsd/pi-coding-agent";
|
|
|
24
24
|
import { join, basename } from "node:path";
|
|
25
25
|
import { existsSync } from "node:fs";
|
|
26
26
|
import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
|
|
27
|
-
import { getPendingGates } from "./gsd-db.js";
|
|
27
|
+
import { getPendingGates, getPendingGatesForTurn } from "./gsd-db.js";
|
|
28
|
+
import {
|
|
29
|
+
GATE_REGISTRY,
|
|
30
|
+
assertGateCoverage,
|
|
31
|
+
getGatesForTurn,
|
|
32
|
+
type GateDefinition,
|
|
33
|
+
} from "./gate-registry.js";
|
|
28
34
|
import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
|
|
29
35
|
import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
|
|
30
36
|
import { logWarning } from "./workflow-logger.js";
|
|
@@ -1395,6 +1401,17 @@ export async function buildExecuteTaskPrompt(
|
|
|
1395
1401
|
|
|
1396
1402
|
const phaseAnchorSection = planAnchor ? formatAnchorForPrompt(planAnchor) : "";
|
|
1397
1403
|
|
|
1404
|
+
// Task-scoped gates owned by execute-task (Q5/Q6/Q7). Pull only the
|
|
1405
|
+
// gates that plan-slice actually seeded for this task — tasks with no
|
|
1406
|
+
// external dependencies legitimately skip Q5, tasks with no runtime
|
|
1407
|
+
// load dimension skip Q6, etc.
|
|
1408
|
+
const etPending = getPendingGatesForTurn(mid, sid, "execute-task", tid);
|
|
1409
|
+
assertGateCoverage(etPending, "execute-task", { requireAll: false });
|
|
1410
|
+
const gatesToClose = renderGatesToCloseBlock(
|
|
1411
|
+
getGatesForTurn("execute-task"),
|
|
1412
|
+
{ pending: new Set(etPending.map((g) => g.gate_id)), allowOmit: true },
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1398
1415
|
return loadPrompt("execute-task", {
|
|
1399
1416
|
overridesSection,
|
|
1400
1417
|
runtimeContext,
|
|
@@ -1412,6 +1429,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
1412
1429
|
taskSummaryPath,
|
|
1413
1430
|
inlinedTemplates,
|
|
1414
1431
|
verificationBudget,
|
|
1432
|
+
gatesToClose,
|
|
1415
1433
|
skillActivation: buildSkillActivationBlock({
|
|
1416
1434
|
base,
|
|
1417
1435
|
milestoneId: mid,
|
|
@@ -1477,6 +1495,19 @@ export async function buildCompleteSlicePrompt(
|
|
|
1477
1495
|
const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
|
|
1478
1496
|
const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
|
|
1479
1497
|
|
|
1498
|
+
// Gates owned by complete-slice (e.g. Q8). Pull from the DB so the
|
|
1499
|
+
// prompt only prompts for gates the plan actually seeded. The tool
|
|
1500
|
+
// handler closes each gate based on the SUMMARY.md section content
|
|
1501
|
+
// after the assistant calls gsd_complete_slice.
|
|
1502
|
+
const csPending = getPendingGatesForTurn(mid, sid, "complete-slice");
|
|
1503
|
+
// coverage check: every pending row must be owned by complete-slice.
|
|
1504
|
+
// requireAll:false because a slice may have already closed some gates.
|
|
1505
|
+
assertGateCoverage(csPending, "complete-slice", { requireAll: false });
|
|
1506
|
+
const gatesToClose = renderGatesToCloseBlock(
|
|
1507
|
+
getGatesForTurn("complete-slice"),
|
|
1508
|
+
{ pending: new Set(csPending.map((g) => g.gate_id)), allowOmit: true },
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1480
1511
|
return loadPrompt("complete-slice", {
|
|
1481
1512
|
workingDirectory: base,
|
|
1482
1513
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
@@ -1485,6 +1516,7 @@ export async function buildCompleteSlicePrompt(
|
|
|
1485
1516
|
inlinedContext,
|
|
1486
1517
|
sliceSummaryPath,
|
|
1487
1518
|
sliceUatPath,
|
|
1519
|
+
gatesToClose,
|
|
1488
1520
|
});
|
|
1489
1521
|
}
|
|
1490
1522
|
|
|
@@ -1675,6 +1707,16 @@ export async function buildValidateMilestonePrompt(
|
|
|
1675
1707
|
const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
|
|
1676
1708
|
const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
|
|
1677
1709
|
|
|
1710
|
+
// Every milestone validation turn owns MV01–MV04 unconditionally: the
|
|
1711
|
+
// registry is the source of truth for which gates the validator must
|
|
1712
|
+
// address, and the block below is what the template renders so the
|
|
1713
|
+
// assistant can never accidentally skip one.
|
|
1714
|
+
const mvGates = getGatesForTurn("validate-milestone");
|
|
1715
|
+
const gatesToEvaluate = renderGatesToCloseBlock(mvGates, {
|
|
1716
|
+
pending: new Set(mvGates.map((g) => g.id)),
|
|
1717
|
+
allowOmit: false,
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1678
1720
|
return loadPrompt("validate-milestone", {
|
|
1679
1721
|
workingDirectory: base,
|
|
1680
1722
|
milestoneId: mid,
|
|
@@ -1683,6 +1725,7 @@ export async function buildValidateMilestonePrompt(
|
|
|
1683
1725
|
inlinedContext,
|
|
1684
1726
|
validationPath: validationOutputPath,
|
|
1685
1727
|
remediationRound: String(remediationRound),
|
|
1728
|
+
gatesToEvaluate,
|
|
1686
1729
|
skillActivation: buildSkillActivationBlock({
|
|
1687
1730
|
base,
|
|
1688
1731
|
milestoneId: mid,
|
|
@@ -1955,27 +1998,51 @@ export async function buildReactiveExecutePrompt(
|
|
|
1955
1998
|
}
|
|
1956
1999
|
|
|
1957
2000
|
// ─── Gate Evaluation ──────────────────────────────────────────────────────
|
|
2001
|
+
//
|
|
2002
|
+
// Gate definitions (question, guidance, owner turn) now live in
|
|
2003
|
+
// gate-registry.ts so that prompt builders, dispatch rules, state
|
|
2004
|
+
// derivation, and tool handlers all consult the same source of truth.
|
|
2005
|
+
// See gate-registry.ts for the full ownership map.
|
|
1958
2006
|
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2007
|
+
/**
|
|
2008
|
+
* Render a "Gates to Close" block for turns like `complete-slice` and
|
|
2009
|
+
* `validate-milestone` that own gates which are closed as a side-effect
|
|
2010
|
+
* of writing artifact sections (not via a dedicated gate-evaluate
|
|
2011
|
+
* subagent loop).
|
|
2012
|
+
*
|
|
2013
|
+
* Returns a plain-text block or an empty string if there are no gates to
|
|
2014
|
+
* close, so callers can drop it straight into a template variable.
|
|
2015
|
+
*/
|
|
2016
|
+
function renderGatesToCloseBlock(
|
|
2017
|
+
gates: ReadonlyArray<GateDefinition>,
|
|
2018
|
+
opts: { pending: ReadonlySet<string>; allowOmit: boolean },
|
|
2019
|
+
): string {
|
|
2020
|
+
const applicable = gates.filter((g) => opts.pending.has(g.id));
|
|
2021
|
+
if (applicable.length === 0) return "";
|
|
2022
|
+
|
|
2023
|
+
const lines: string[] = [];
|
|
2024
|
+
lines.push("## Gates to Close");
|
|
2025
|
+
lines.push("");
|
|
2026
|
+
lines.push(
|
|
2027
|
+
"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.",
|
|
2028
|
+
);
|
|
2029
|
+
lines.push("");
|
|
2030
|
+
for (const def of applicable) {
|
|
2031
|
+
lines.push(`### ${def.id} — ${def.promptSection}`);
|
|
2032
|
+
lines.push("");
|
|
2033
|
+
lines.push(`**Question:** ${def.question}`);
|
|
2034
|
+
lines.push("");
|
|
2035
|
+
lines.push(def.guidance);
|
|
2036
|
+
if (opts.allowOmit) {
|
|
2037
|
+
lines.push("");
|
|
2038
|
+
lines.push(
|
|
2039
|
+
`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.`,
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
lines.push("");
|
|
2043
|
+
}
|
|
2044
|
+
return lines.join("\n").trimEnd();
|
|
2045
|
+
}
|
|
1979
2046
|
|
|
1980
2047
|
export async function buildParallelResearchSlicesPrompt(
|
|
1981
2048
|
mid: string,
|
|
@@ -2011,28 +2078,39 @@ export async function buildGateEvaluatePrompt(
|
|
|
2011
2078
|
mid: string, midTitle: string, sid: string, sTitle: string,
|
|
2012
2079
|
base: string,
|
|
2013
2080
|
): Promise<string> {
|
|
2014
|
-
|
|
2081
|
+
// Pull only the gates this turn actually owns (Q3/Q4). Filter via the
|
|
2082
|
+
// registry so that scope:"slice" gates owned by other turns (Q8) can't
|
|
2083
|
+
// leak into this prompt and can't block dispatch via silent skip.
|
|
2084
|
+
const pending = getPendingGatesForTurn(mid, sid, "gate-evaluate");
|
|
2085
|
+
|
|
2086
|
+
// Fails loudly if the pending list contains a gate id the registry
|
|
2087
|
+
// doesn't own for this turn. Missing owned gates is allowed here —
|
|
2088
|
+
// `gate-evaluate` is dispatched whenever *any* of its owned gates are
|
|
2089
|
+
// pending, not only when all of them are.
|
|
2090
|
+
assertGateCoverage(pending, "gate-evaluate", { requireAll: false });
|
|
2015
2091
|
|
|
2016
2092
|
// Load the slice plan for context
|
|
2017
2093
|
const planFile = resolveSliceFile(base, mid, sid, "PLAN");
|
|
2018
2094
|
const planContent = planFile ? (await loadFile(planFile)) ?? "(plan file empty)" : "(plan file not found)";
|
|
2019
2095
|
|
|
2020
|
-
// Build per-gate subagent prompts
|
|
2096
|
+
// Build per-gate subagent prompts from the pending rows. Because the
|
|
2097
|
+
// registry has already validated every row, `getGateDefinition` cannot
|
|
2098
|
+
// return undefined here.
|
|
2099
|
+
const pendingIds = new Set(pending.map((g) => g.gate_id));
|
|
2100
|
+
const gateDefs = getGatesForTurn("gate-evaluate").filter((def) => pendingIds.has(def.id));
|
|
2101
|
+
|
|
2021
2102
|
const subagentSections: string[] = [];
|
|
2022
2103
|
const gateListLines: string[] = [];
|
|
2023
2104
|
|
|
2024
|
-
for (const
|
|
2025
|
-
|
|
2026
|
-
if (!meta) continue;
|
|
2027
|
-
|
|
2028
|
-
gateListLines.push(`- **${gate.gate_id}**: ${meta.question}`);
|
|
2105
|
+
for (const def of gateDefs) {
|
|
2106
|
+
gateListLines.push(`- **${def.id}**: ${def.question}`);
|
|
2029
2107
|
|
|
2030
2108
|
const subPrompt = [
|
|
2031
|
-
`You are evaluating quality gate **${
|
|
2109
|
+
`You are evaluating quality gate **${def.id}** for slice ${sid} (${sTitle}).`,
|
|
2032
2110
|
"",
|
|
2033
|
-
`## Question: ${
|
|
2111
|
+
`## Question: ${def.question}`,
|
|
2034
2112
|
"",
|
|
2035
|
-
|
|
2113
|
+
def.guidance,
|
|
2036
2114
|
"",
|
|
2037
2115
|
"## Slice Plan",
|
|
2038
2116
|
"",
|
|
@@ -2044,14 +2122,14 @@ export async function buildGateEvaluatePrompt(
|
|
|
2044
2122
|
`Call the \`gsd_save_gate_result\` tool with:`,
|
|
2045
2123
|
`- \`milestoneId\`: "${mid}"`,
|
|
2046
2124
|
`- \`sliceId\`: "${sid}"`,
|
|
2047
|
-
`- \`gateId\`: "${
|
|
2125
|
+
`- \`gateId\`: "${def.id}"`,
|
|
2048
2126
|
"- `verdict`: \"pass\" (no concerns), \"flag\" (concerns found), or \"omitted\" (not applicable)",
|
|
2049
2127
|
"- `rationale`: one-sentence justification",
|
|
2050
2128
|
"- `findings`: detailed markdown findings (or empty if omitted)",
|
|
2051
2129
|
].join("\n");
|
|
2052
2130
|
|
|
2053
2131
|
subagentSections.push([
|
|
2054
|
-
`### ${
|
|
2132
|
+
`### ${def.id}: ${def.question}`,
|
|
2055
2133
|
"",
|
|
2056
2134
|
"Use this as the prompt for a `subagent` call:",
|
|
2057
2135
|
"",
|
|
@@ -269,16 +269,40 @@ export async function bootstrapAutoSession(
|
|
|
269
269
|
//
|
|
270
270
|
// Precedence:
|
|
271
271
|
// 1) Explicit session override via /gsd model (this session)
|
|
272
|
-
// 2) GSD model preferences from PREFERENCES.md
|
|
273
|
-
// 3) Current session model from settings/session restore
|
|
272
|
+
// 2) GSD model preferences from PREFERENCES.md (validated against live auth)
|
|
273
|
+
// 3) Current session model from settings/session restore (if provider ready)
|
|
274
274
|
//
|
|
275
275
|
// This preserves #3517 defaults while honoring explicit runtime model
|
|
276
276
|
// selection for subsequent /gsd runs in the same session.
|
|
277
277
|
const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
|
|
278
278
|
const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
|
|
279
|
+
// Validate the preferred model against the live registry + provider auth so
|
|
280
|
+
// an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
|
|
281
|
+
// start-model snapshot. Without this, every subsequent unit would try to
|
|
282
|
+
// fall back to an unusable model.
|
|
283
|
+
let validatedPreferredModel: { provider: string; id: string } | undefined;
|
|
284
|
+
if (preferredModel) {
|
|
285
|
+
const { resolveModelId } = await import("./auto-model-selection.js");
|
|
286
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
287
|
+
const match = resolveModelId(
|
|
288
|
+
`${preferredModel.provider}/${preferredModel.id}`,
|
|
289
|
+
available,
|
|
290
|
+
ctx.model?.provider,
|
|
291
|
+
);
|
|
292
|
+
if (match) {
|
|
293
|
+
validatedPreferredModel = { provider: match.provider, id: match.id };
|
|
294
|
+
} else {
|
|
295
|
+
ctx.ui.notify(
|
|
296
|
+
`Preferred model ${preferredModel.provider}/${preferredModel.id} from PREFERENCES.md is not configured; falling back to session default.`,
|
|
297
|
+
"warning",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const sessionModelReady =
|
|
302
|
+
ctx.model && ctx.modelRegistry.isProviderRequestReady(ctx.model.provider);
|
|
279
303
|
const startModelSnapshot = manualSessionOverride
|
|
280
|
-
??
|
|
281
|
-
?? (ctx.model
|
|
304
|
+
?? validatedPreferredModel
|
|
305
|
+
?? (sessionModelReady && ctx.model
|
|
282
306
|
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
283
307
|
: null);
|
|
284
308
|
|
|
@@ -600,6 +624,9 @@ export async function bootstrapAutoSession(
|
|
|
600
624
|
s.consecutiveCompleteBootstraps = 0;
|
|
601
625
|
|
|
602
626
|
// ── Initialize session state ──
|
|
627
|
+
// Notify shared phase state so subagent conflict checks can fire
|
|
628
|
+
const { activateGSD: activateGSDPhaseState } = await import("../shared/gsd-phase-state.js");
|
|
629
|
+
activateGSDPhaseState();
|
|
603
630
|
s.active = true;
|
|
604
631
|
s.stepMode = requestedStepMode;
|
|
605
632
|
s.verbose = verboseMode;
|
|
@@ -115,6 +115,7 @@ import {
|
|
|
115
115
|
resetSkillTelemetry,
|
|
116
116
|
} from "./skill-telemetry.js";
|
|
117
117
|
import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
|
|
118
|
+
import { deactivateGSD } from "../shared/gsd-phase-state.js";
|
|
118
119
|
import {
|
|
119
120
|
initMetrics,
|
|
120
121
|
resetMetrics,
|
|
@@ -622,6 +623,7 @@ function handleLostSessionLock(
|
|
|
622
623
|
});
|
|
623
624
|
s.active = false;
|
|
624
625
|
s.paused = false;
|
|
626
|
+
deactivateGSD();
|
|
625
627
|
clearUnitTimeout();
|
|
626
628
|
restoreProjectRootEnv();
|
|
627
629
|
restoreMilestoneLockEnv();
|
|
@@ -659,6 +661,7 @@ function handleLostSessionLock(
|
|
|
659
661
|
function cleanupAfterLoopExit(ctx: ExtensionContext): void {
|
|
660
662
|
s.currentUnit = null;
|
|
661
663
|
s.active = false;
|
|
664
|
+
deactivateGSD();
|
|
662
665
|
clearUnitTimeout();
|
|
663
666
|
restoreProjectRootEnv();
|
|
664
667
|
restoreMilestoneLockEnv();
|
|
@@ -1024,6 +1027,7 @@ export async function pauseAuto(
|
|
|
1024
1027
|
|
|
1025
1028
|
s.active = false;
|
|
1026
1029
|
s.paused = true;
|
|
1030
|
+
deactivateGSD();
|
|
1027
1031
|
restoreProjectRootEnv();
|
|
1028
1032
|
restoreMilestoneLockEnv();
|
|
1029
1033
|
s.pendingVerificationRetry = null;
|
|
@@ -1026,12 +1026,12 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
1026
1026
|
name: "gsd_save_gate_result",
|
|
1027
1027
|
label: "Save Gate Result",
|
|
1028
1028
|
description:
|
|
1029
|
-
"Save the result of a quality gate evaluation (Q3-Q8) to the GSD database. " +
|
|
1029
|
+
"Save the result of a quality gate evaluation (Q3-Q8 or MV01-MV04) to the GSD database. " +
|
|
1030
1030
|
"Called by gate evaluation sub-agents after analyzing a specific quality question.",
|
|
1031
1031
|
promptSnippet: "Save quality gate evaluation result (verdict, rationale, findings)",
|
|
1032
1032
|
promptGuidelines: [
|
|
1033
1033
|
"Use gsd_save_gate_result after evaluating a quality gate question.",
|
|
1034
|
-
"gateId must be one of: Q3, Q4, Q5, Q6, Q7, Q8.",
|
|
1034
|
+
"gateId must be one of: Q3, Q4, Q5, Q6, Q7, Q8, MV01, MV02, MV03, MV04.",
|
|
1035
1035
|
"verdict must be: pass (no concerns), flag (concerns found), or omitted (not applicable).",
|
|
1036
1036
|
"rationale should be a one-sentence justification for the verdict.",
|
|
1037
1037
|
"findings should contain detailed markdown analysis (or empty string if omitted).",
|
|
@@ -1039,7 +1039,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
1039
1039
|
parameters: Type.Object({
|
|
1040
1040
|
milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
|
|
1041
1041
|
sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
|
|
1042
|
-
gateId: Type.String({ description: "Gate ID: Q3, Q4, Q5, Q6, Q7, or
|
|
1042
|
+
gateId: Type.String({ description: "Gate ID: Q3, Q4, Q5, Q6, Q7, Q8, MV01, MV02, MV03, or MV04" }),
|
|
1043
1043
|
taskId: Type.Optional(Type.String({ description: "Task ID for task-scoped gates (Q5/Q6/Q7)" })),
|
|
1044
1044
|
verdict: Type.String({ description: "pass, flag, or omitted" }),
|
|
1045
1045
|
rationale: Type.String({ description: "One-sentence justification" }),
|
|
@@ -93,9 +93,6 @@ export function registerShortcuts(pi: ExtensionAPI): void {
|
|
|
93
93
|
handler: openParallelOverlay,
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
description: shortcutDesc(`${GSD_SHORTCUTS.parallel.action} (fallback)`, GSD_SHORTCUTS.parallel.command),
|
|
99
|
-
handler: openParallelOverlay,
|
|
100
|
-
});
|
|
96
|
+
// No Ctrl+Shift+P fallback — conflicts with cycleModelBackward (shift+ctrl+p).
|
|
97
|
+
// Use Ctrl+Alt+P or /gsd parallel watch instead.
|
|
101
98
|
}
|
|
@@ -185,11 +185,35 @@ const PROVIDER_ROUTES: Record<string, string[]> = {
|
|
|
185
185
|
google: ["google-gemini-cli"],
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Providers that use external CLI authentication (not API keys).
|
|
190
|
+
* These are always considered "ok" — the host CLI handles auth.
|
|
191
|
+
*/
|
|
192
|
+
const CLI_AUTH_PROVIDERS = new Set([
|
|
193
|
+
"claude-code",
|
|
194
|
+
"openai-codex",
|
|
195
|
+
"google-gemini-cli",
|
|
196
|
+
"google-antigravity",
|
|
197
|
+
]);
|
|
198
|
+
|
|
188
199
|
function checkLlmProviders(): ProviderCheckResult[] {
|
|
189
200
|
const required = collectConfiguredModelProviders();
|
|
190
201
|
const results: ProviderCheckResult[] = [];
|
|
191
202
|
|
|
192
203
|
for (const providerId of required) {
|
|
204
|
+
// CLI-authenticated providers don't need API keys — skip key check
|
|
205
|
+
if (CLI_AUTH_PROVIDERS.has(providerId)) {
|
|
206
|
+
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
207
|
+
results.push({
|
|
208
|
+
name: providerId,
|
|
209
|
+
label: info?.label ?? providerId,
|
|
210
|
+
category: "llm",
|
|
211
|
+
status: "ok",
|
|
212
|
+
message: `${info?.label ?? providerId} — CLI auth (no key needed)`,
|
|
213
|
+
required: true,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
193
217
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
194
218
|
const label = providerId === "anthropic-vertex"
|
|
195
219
|
? "Anthropic Vertex"
|
|
@@ -44,6 +44,9 @@ export function resetRetryState(state: RetryState): void {
|
|
|
44
44
|
|
|
45
45
|
const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i;
|
|
46
46
|
const RATE_LIMIT_RE = /rate.?limit|too many requests|429/i;
|
|
47
|
+
// OpenRouter affordability-style quota errors should be treated as transient
|
|
48
|
+
// so core retry logic can lower maxTokens and continue in-session.
|
|
49
|
+
const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
|
|
47
50
|
const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i;
|
|
48
51
|
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
|
|
49
52
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
@@ -67,7 +70,7 @@ const RESET_DELAY_RE = /reset in (\d+)s/i;
|
|
|
67
70
|
*/
|
|
68
71
|
export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorClass {
|
|
69
72
|
const isPermanent = PERMANENT_RE.test(errorMsg);
|
|
70
|
-
const isRateLimit = RATE_LIMIT_RE.test(errorMsg);
|
|
73
|
+
const isRateLimit = RATE_LIMIT_RE.test(errorMsg) || AFFORDABILITY_RE.test(errorMsg);
|
|
71
74
|
|
|
72
75
|
// 1. Permanent — but rate limit takes precedence
|
|
73
76
|
if (isPermanent && !isRateLimit) {
|
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
|
|
22
|
+
import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
|
|
23
|
+
import type { GateId, GateRow, GateScope } from "./types.js";
|
|
24
|
+
|
|
25
|
+
/** Which workflow turn is responsible for evaluating / closing a gate. */
|
|
26
|
+
export type OwnerTurn =
|
|
27
|
+
| "gate-evaluate"
|
|
28
|
+
| "execute-task"
|
|
29
|
+
| "complete-slice"
|
|
30
|
+
| "validate-milestone";
|
|
31
|
+
|
|
32
|
+
export interface GateDefinition {
|
|
33
|
+
id: GateId;
|
|
34
|
+
scope: GateScope;
|
|
35
|
+
ownerTurn: OwnerTurn;
|
|
36
|
+
/** One-line question the assistant must answer. */
|
|
37
|
+
question: string;
|
|
38
|
+
/** Markdown guidance describing what a good answer looks like. */
|
|
39
|
+
guidance: string;
|
|
40
|
+
/** H3 section header used in the artifact the turn writes
|
|
41
|
+
* (e.g. "Operational Readiness" for Q8 in the slice summary). */
|
|
42
|
+
promptSection: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const GATE_REGISTRY = {
|
|
46
|
+
Q3: {
|
|
47
|
+
id: "Q3",
|
|
48
|
+
scope: "slice",
|
|
49
|
+
ownerTurn: "gate-evaluate",
|
|
50
|
+
question: "How can this be exploited?",
|
|
51
|
+
guidance: [
|
|
52
|
+
"Identify abuse scenarios: parameter tampering, replay attacks, privilege escalation.",
|
|
53
|
+
"Map data exposure risks: PII, tokens, secrets accessible through this slice.",
|
|
54
|
+
"Define input trust boundaries: untrusted user input reaching DB, API, or filesystem.",
|
|
55
|
+
"If none apply, return verdict 'omitted' with rationale explaining why.",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
promptSection: "Abuse Surface",
|
|
58
|
+
},
|
|
59
|
+
Q4: {
|
|
60
|
+
id: "Q4",
|
|
61
|
+
scope: "slice",
|
|
62
|
+
ownerTurn: "gate-evaluate",
|
|
63
|
+
question: "What existing promises does this break?",
|
|
64
|
+
guidance: [
|
|
65
|
+
"List which existing requirements (R001, R003, etc.) are touched by this slice.",
|
|
66
|
+
"Identify what must be re-tested after shipping.",
|
|
67
|
+
"Flag decisions that should be revisited given the new scope.",
|
|
68
|
+
"If no existing requirements are affected, return verdict 'omitted'.",
|
|
69
|
+
].join("\n"),
|
|
70
|
+
promptSection: "Broken Promises",
|
|
71
|
+
},
|
|
72
|
+
Q5: {
|
|
73
|
+
id: "Q5",
|
|
74
|
+
scope: "task",
|
|
75
|
+
ownerTurn: "execute-task",
|
|
76
|
+
question: "What breaks when dependencies fail?",
|
|
77
|
+
guidance: [
|
|
78
|
+
"Enumerate the task's external dependencies (APIs, filesystem, network, subprocesses).",
|
|
79
|
+
"Describe the failure path for each: timeout, malformed response, connection loss.",
|
|
80
|
+
"Verify the implementation handles each failure or explicitly bubbles the error.",
|
|
81
|
+
"Return verdict 'omitted' only if the task has no external dependencies.",
|
|
82
|
+
].join("\n"),
|
|
83
|
+
promptSection: "Failure Modes",
|
|
84
|
+
},
|
|
85
|
+
Q6: {
|
|
86
|
+
id: "Q6",
|
|
87
|
+
scope: "task",
|
|
88
|
+
ownerTurn: "execute-task",
|
|
89
|
+
question: "What is the 10x load breakpoint?",
|
|
90
|
+
guidance: [
|
|
91
|
+
"Identify the resource that saturates first at 10x the expected load.",
|
|
92
|
+
"Describe the protection applied (pool sizing, rate limiting, pagination, caching).",
|
|
93
|
+
"Return verdict 'omitted' if the task has no runtime load dimension.",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
promptSection: "Load Profile",
|
|
96
|
+
},
|
|
97
|
+
Q7: {
|
|
98
|
+
id: "Q7",
|
|
99
|
+
scope: "task",
|
|
100
|
+
ownerTurn: "execute-task",
|
|
101
|
+
question: "What negative tests protect this task?",
|
|
102
|
+
guidance: [
|
|
103
|
+
"List malformed inputs, error paths, and boundary conditions the tests cover.",
|
|
104
|
+
"Point to the specific test files or cases that assert each negative scenario.",
|
|
105
|
+
"Return verdict 'omitted' only if the task has no meaningful negative surface.",
|
|
106
|
+
].join("\n"),
|
|
107
|
+
promptSection: "Negative Tests",
|
|
108
|
+
},
|
|
109
|
+
Q8: {
|
|
110
|
+
id: "Q8",
|
|
111
|
+
scope: "slice",
|
|
112
|
+
ownerTurn: "complete-slice",
|
|
113
|
+
question: "How will ops know this slice is healthy or broken?",
|
|
114
|
+
guidance: [
|
|
115
|
+
"Describe the health signal (metric, log line, dashboard) that proves the slice works.",
|
|
116
|
+
"Describe the failure signal that triggers an alert or paging.",
|
|
117
|
+
"Document the recovery procedure and any monitoring gaps.",
|
|
118
|
+
"Return verdict 'omitted' only for slices with no runtime behavior at all.",
|
|
119
|
+
].join("\n"),
|
|
120
|
+
promptSection: "Operational Readiness",
|
|
121
|
+
},
|
|
122
|
+
MV01: {
|
|
123
|
+
id: "MV01",
|
|
124
|
+
scope: "milestone",
|
|
125
|
+
ownerTurn: "validate-milestone",
|
|
126
|
+
question: "Is every success criterion in the milestone roadmap satisfied?",
|
|
127
|
+
guidance: [
|
|
128
|
+
"Walk the success-criteria checklist from the milestone roadmap.",
|
|
129
|
+
"For each criterion, point to the slice / assessment / verification evidence that proves it.",
|
|
130
|
+
"Return verdict 'flag' if any criterion is unmet or unverifiable.",
|
|
131
|
+
].join("\n"),
|
|
132
|
+
promptSection: "Success Criteria Checklist",
|
|
133
|
+
},
|
|
134
|
+
MV02: {
|
|
135
|
+
id: "MV02",
|
|
136
|
+
scope: "milestone",
|
|
137
|
+
ownerTurn: "validate-milestone",
|
|
138
|
+
question: "Does every slice have a SUMMARY.md and a passing assessment?",
|
|
139
|
+
guidance: [
|
|
140
|
+
"Confirm every slice listed in the roadmap has a SUMMARY.md.",
|
|
141
|
+
"Confirm each slice has an ASSESSMENT verdict of 'pass' (or justified 'omitted').",
|
|
142
|
+
"Flag missing artifacts and slices with outstanding follow-ups or known limitations.",
|
|
143
|
+
].join("\n"),
|
|
144
|
+
promptSection: "Slice Delivery Audit",
|
|
145
|
+
},
|
|
146
|
+
MV03: {
|
|
147
|
+
id: "MV03",
|
|
148
|
+
scope: "milestone",
|
|
149
|
+
ownerTurn: "validate-milestone",
|
|
150
|
+
question: "Do the slices integrate end-to-end?",
|
|
151
|
+
guidance: [
|
|
152
|
+
"Trace at least one cross-slice flow proving the pieces compose.",
|
|
153
|
+
"Flag gaps where two slices were built in isolation with no integration evidence.",
|
|
154
|
+
].join("\n"),
|
|
155
|
+
promptSection: "Cross-Slice Integration",
|
|
156
|
+
},
|
|
157
|
+
MV04: {
|
|
158
|
+
id: "MV04",
|
|
159
|
+
scope: "milestone",
|
|
160
|
+
ownerTurn: "validate-milestone",
|
|
161
|
+
question: "Are all touched requirements covered and still coherent?",
|
|
162
|
+
guidance: [
|
|
163
|
+
"For each requirement advanced, validated, surfaced, or invalidated across the milestone's slices, confirm the milestone-level evidence matches.",
|
|
164
|
+
"Flag requirements that slices claim to advance but no artifact proves.",
|
|
165
|
+
].join("\n"),
|
|
166
|
+
promptSection: "Requirement Coverage",
|
|
167
|
+
},
|
|
168
|
+
} as const satisfies Record<GateId, GateDefinition>;
|
|
169
|
+
|
|
170
|
+
export type GateRegistry = typeof GATE_REGISTRY;
|
|
171
|
+
|
|
172
|
+
/** Stable ordered lists per owner turn — iteration order matches declaration. */
|
|
173
|
+
const ORDERED_GATES: readonly GateDefinition[] = Object.values(GATE_REGISTRY) as readonly GateDefinition[];
|
|
174
|
+
|
|
175
|
+
/** Return every gate owned by a turn, in stable declaration order. */
|
|
176
|
+
export function getGatesForTurn(turn: OwnerTurn): GateDefinition[] {
|
|
177
|
+
return ORDERED_GATES.filter((g) => g.ownerTurn === turn);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Return the set of gate ids a turn owns. */
|
|
181
|
+
export function getGateIdsForTurn(turn: OwnerTurn): Set<GateId> {
|
|
182
|
+
return new Set(getGatesForTurn(turn).map((g) => g.id));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Look up a definition by gate id, or undefined if unknown. */
|
|
186
|
+
export function getGateDefinition(id: string): GateDefinition | undefined {
|
|
187
|
+
return (GATE_REGISTRY as Record<string, GateDefinition>)[id];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Look up the owner turn for a gate id. Throws if the gate is unknown. */
|
|
191
|
+
export function getOwnerTurn(id: GateId): OwnerTurn {
|
|
192
|
+
const def = GATE_REGISTRY[id];
|
|
193
|
+
if (!def) {
|
|
194
|
+
throw new GSDError(GSD_PARSE_ERROR, `gate-registry: unknown gate id "${id}"`);
|
|
195
|
+
}
|
|
196
|
+
return def.ownerTurn;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Assert that the pending gate rows for a turn match what the registry says
|
|
201
|
+
* the turn owns. Fails loudly rather than silently skipping.
|
|
202
|
+
*
|
|
203
|
+
* - Every row in `pending` must have a definition whose `ownerTurn` matches `turn`.
|
|
204
|
+
* (The caller is responsible for scoping the pending list — e.g. filtering
|
|
205
|
+
* by slice scope before passing it in.)
|
|
206
|
+
* - `options.requireAll` (default true): every gate the turn owns must appear
|
|
207
|
+
* in `pending`. Set to false for turns like `execute-task` that only need
|
|
208
|
+
* coverage for the subset of gates that were seeded (e.g. tasks with no
|
|
209
|
+
* external dependencies have no Q5 row).
|
|
210
|
+
*/
|
|
211
|
+
export function assertGateCoverage(
|
|
212
|
+
pending: ReadonlyArray<Pick<GateRow, "gate_id">>,
|
|
213
|
+
turn: OwnerTurn,
|
|
214
|
+
options: { requireAll?: boolean } = {},
|
|
215
|
+
): void {
|
|
216
|
+
const requireAll = options.requireAll ?? true;
|
|
217
|
+
const expected = getGateIdsForTurn(turn);
|
|
218
|
+
const pendingIds = new Set(pending.map((g) => g.gate_id));
|
|
219
|
+
|
|
220
|
+
const unknown: string[] = [];
|
|
221
|
+
for (const id of pendingIds) {
|
|
222
|
+
const def = getGateDefinition(id);
|
|
223
|
+
if (!def) {
|
|
224
|
+
unknown.push(id);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (def.ownerTurn !== turn) {
|
|
228
|
+
unknown.push(`${id} (owned by ${def.ownerTurn}, not ${turn})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (unknown.length > 0) {
|
|
233
|
+
throw new GSDError(
|
|
234
|
+
GSD_PARSE_ERROR,
|
|
235
|
+
`assertGateCoverage: turn "${turn}" received pending gates it does not own: ${unknown.join(", ")}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (requireAll) {
|
|
240
|
+
const missing: GateId[] = [];
|
|
241
|
+
for (const id of expected) {
|
|
242
|
+
if (!pendingIds.has(id)) missing.push(id);
|
|
243
|
+
}
|
|
244
|
+
if (missing.length > 0) {
|
|
245
|
+
throw new GSDError(
|
|
246
|
+
GSD_PARSE_ERROR,
|
|
247
|
+
`assertGateCoverage: turn "${turn}" is missing required gates: ${missing.join(", ")}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|