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
|
@@ -574,6 +574,42 @@ test("runProviderChecks reports ok for OpenAI via openai-codex auth.json (#2922)
|
|
|
574
574
|
rmSync(tmpHome, { recursive: true, force: true });
|
|
575
575
|
});
|
|
576
576
|
|
|
577
|
+
test("runProviderChecks reports ok for claude-code without any API key", () => {
|
|
578
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-repo-")));
|
|
579
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
580
|
+
writeFileSync(
|
|
581
|
+
join(repo, ".gsd", "PREFERENCES.md"),
|
|
582
|
+
[
|
|
583
|
+
"---",
|
|
584
|
+
"models:",
|
|
585
|
+
" execution:",
|
|
586
|
+
" model: claude-sonnet-4-6",
|
|
587
|
+
" provider: claude-code",
|
|
588
|
+
"---",
|
|
589
|
+
"",
|
|
590
|
+
].join("\n"),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-home-")));
|
|
594
|
+
|
|
595
|
+
withEnv({
|
|
596
|
+
HOME: tmpHome,
|
|
597
|
+
ANTHROPIC_API_KEY: undefined,
|
|
598
|
+
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
599
|
+
}, () => {
|
|
600
|
+
withCwd(repo, () => {
|
|
601
|
+
const results = runProviderChecks();
|
|
602
|
+
const cc = results.find(r => r.name === "claude-code");
|
|
603
|
+
assert.ok(cc, "claude-code result should exist");
|
|
604
|
+
assert.equal(cc!.status, "ok", "claude-code uses CLI auth — must be ok without API keys");
|
|
605
|
+
assert.ok(cc!.message.includes("CLI auth"), "should indicate CLI auth");
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
rmSync(repo, { recursive: true, force: true });
|
|
610
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
611
|
+
});
|
|
612
|
+
|
|
577
613
|
test("PROVIDER_ROUTES includes google-gemini-cli as route for google (#2922)", async () => {
|
|
578
614
|
const { readFileSync: readFS } = await import("node:fs");
|
|
579
615
|
const { dirname: dirn, join: joinPath } = await import("node:path");
|
|
@@ -82,3 +82,19 @@ test("shortcut-defs: formats shortcut pair using platform symbols", () => {
|
|
|
82
82
|
assert.equal(pair, "Ctrl+Alt+N / Ctrl+Shift+N");
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
test("shortcut-defs: parallel shortcut omits fallback (hasFallback: false)", () => {
|
|
87
|
+
const pair = formattedShortcutPair("parallel");
|
|
88
|
+
if (process.platform === "darwin") {
|
|
89
|
+
assert.equal(pair, "⌃⌥P", "parallel should only show primary combo");
|
|
90
|
+
} else {
|
|
91
|
+
assert.equal(pair, "Ctrl+Alt+P", "parallel should only show primary combo");
|
|
92
|
+
}
|
|
93
|
+
// Verify it does NOT contain the fallback separator
|
|
94
|
+
assert.ok(!pair.includes("/"), "parallel pair should not contain fallback separator");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("shortcut-defs: dashboard shortcut includes fallback (hasFallback: true)", () => {
|
|
98
|
+
const pair = formattedShortcutPair("dashboard");
|
|
99
|
+
assert.ok(pair.includes("/"), "dashboard pair should contain fallback separator");
|
|
100
|
+
});
|
|
@@ -186,4 +186,31 @@ describe("evaluating-gates phase", () => {
|
|
|
186
186
|
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q5", scope: "task", taskId: "T01" });
|
|
187
187
|
assert.equal(getPendingSliceGateCount("M001", "S01"), 1);
|
|
188
188
|
});
|
|
189
|
+
|
|
190
|
+
test("Q8 (owned by complete-slice) does not block evaluating-gates phase", async () => {
|
|
191
|
+
// Regression: Q8 is stored with scope:"slice" but owned by the
|
|
192
|
+
// complete-slice turn. Before the gate registry landed, deriveState
|
|
193
|
+
// counted Q8 as a blocker for evaluating-gates while the gate-evaluate
|
|
194
|
+
// prompt silently dropped Q8 — an unrecoverable stall. After the
|
|
195
|
+
// registry change, deriveState filters by owner turn, so Q8 never
|
|
196
|
+
// blocks evaluating-gates.
|
|
197
|
+
planSlice(tmpDir);
|
|
198
|
+
await renderPlanFromDb(tmpDir, "M001", "S01");
|
|
199
|
+
|
|
200
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
|
|
201
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", scope: "slice" });
|
|
202
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q8", scope: "slice" });
|
|
203
|
+
|
|
204
|
+
saveGateResult({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", verdict: "pass", rationale: "OK", findings: "" });
|
|
205
|
+
saveGateResult({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", verdict: "omitted", rationale: "N/A", findings: "" });
|
|
206
|
+
// Q8 deliberately left pending — it's complete-slice's problem.
|
|
207
|
+
|
|
208
|
+
invalidateStateCache();
|
|
209
|
+
const state = await deriveState(tmpDir);
|
|
210
|
+
assert.equal(
|
|
211
|
+
state.phase,
|
|
212
|
+
"executing",
|
|
213
|
+
`pending Q8 must not stall evaluating-gates — got phase=${state.phase}`,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
189
216
|
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate registry tests — enforce that every declared GateId has a registry
|
|
3
|
+
* entry, that every owner-turn bucket is non-empty, and that coverage
|
|
4
|
+
* assertions fail loudly instead of silently skipping unknown gates.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
GATE_REGISTRY,
|
|
12
|
+
assertGateCoverage,
|
|
13
|
+
getGateDefinition,
|
|
14
|
+
getGateIdsForTurn,
|
|
15
|
+
getGatesForTurn,
|
|
16
|
+
getOwnerTurn,
|
|
17
|
+
type OwnerTurn,
|
|
18
|
+
} from "../gate-registry.ts";
|
|
19
|
+
import type { GateId } from "../types.ts";
|
|
20
|
+
|
|
21
|
+
/** Authoritative list of GateIds as declared in types.ts. */
|
|
22
|
+
const ALL_GATE_IDS: readonly GateId[] = [
|
|
23
|
+
"Q3", "Q4", "Q5", "Q6", "Q7", "Q8",
|
|
24
|
+
"MV01", "MV02", "MV03", "MV04",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const ALL_OWNER_TURNS: readonly OwnerTurn[] = [
|
|
28
|
+
"gate-evaluate",
|
|
29
|
+
"execute-task",
|
|
30
|
+
"complete-slice",
|
|
31
|
+
"validate-milestone",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
describe("gate-registry", () => {
|
|
35
|
+
test("every declared GateId has a registry entry", () => {
|
|
36
|
+
for (const id of ALL_GATE_IDS) {
|
|
37
|
+
const def = GATE_REGISTRY[id];
|
|
38
|
+
assert.ok(def, `missing registry entry for gate ${id}`);
|
|
39
|
+
assert.equal(def.id, id);
|
|
40
|
+
assert.ok(def.question.length > 0, `${id} missing question`);
|
|
41
|
+
assert.ok(def.guidance.length > 0, `${id} missing guidance`);
|
|
42
|
+
assert.ok(def.promptSection.length > 0, `${id} missing promptSection`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("registry contains no extra gate entries", () => {
|
|
47
|
+
const registryIds = new Set(Object.keys(GATE_REGISTRY));
|
|
48
|
+
const declaredIds = new Set<string>(ALL_GATE_IDS);
|
|
49
|
+
for (const id of registryIds) {
|
|
50
|
+
assert.ok(declaredIds.has(id), `registry has unknown gate ${id}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("every owner turn owns at least one gate", () => {
|
|
55
|
+
for (const turn of ALL_OWNER_TURNS) {
|
|
56
|
+
const gates = getGatesForTurn(turn);
|
|
57
|
+
assert.ok(
|
|
58
|
+
gates.length > 0,
|
|
59
|
+
`owner turn "${turn}" has no gates — likely a registry mistake`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("owner turn buckets are disjoint", () => {
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
for (const turn of ALL_OWNER_TURNS) {
|
|
67
|
+
for (const def of getGatesForTurn(turn)) {
|
|
68
|
+
assert.ok(!seen.has(def.id), `gate ${def.id} claimed by two turns`);
|
|
69
|
+
seen.add(def.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Every gate should appear in exactly one bucket.
|
|
73
|
+
assert.equal(seen.size, ALL_GATE_IDS.length);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("getOwnerTurn round-trips against GATE_REGISTRY", () => {
|
|
77
|
+
for (const id of ALL_GATE_IDS) {
|
|
78
|
+
const turn = getOwnerTurn(id);
|
|
79
|
+
const idsForTurn = getGateIdsForTurn(turn);
|
|
80
|
+
assert.ok(idsForTurn.has(id), `${id} not in ${turn} bucket`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("getGateDefinition returns undefined for unknown ids", () => {
|
|
85
|
+
assert.equal(getGateDefinition("Q99"), undefined);
|
|
86
|
+
assert.equal(getGateDefinition("not-a-gate"), undefined);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("assertGateCoverage", () => {
|
|
91
|
+
test("throws when a row is owned by a different turn", () => {
|
|
92
|
+
// Q8 is owned by complete-slice, not gate-evaluate — this used to be
|
|
93
|
+
// silently dropped by the old `if (!meta) continue;` filter, causing
|
|
94
|
+
// the evaluating-gates phase to stall.
|
|
95
|
+
assert.throws(
|
|
96
|
+
() => assertGateCoverage([{ gate_id: "Q8" }], "gate-evaluate"),
|
|
97
|
+
(err: Error) =>
|
|
98
|
+
err.message.includes("Q8") && err.message.includes("gate-evaluate"),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("throws when a row has an unknown gate id", () => {
|
|
103
|
+
assert.throws(
|
|
104
|
+
() => assertGateCoverage([{ gate_id: "Q999" as GateId }], "gate-evaluate", { requireAll: false }),
|
|
105
|
+
(err: Error) => err.message.includes("Q999"),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("throws when requireAll is true and an owned gate is missing", () => {
|
|
110
|
+
// gate-evaluate owns Q3 and Q4. Passing only Q3 should fail.
|
|
111
|
+
assert.throws(
|
|
112
|
+
() => assertGateCoverage([{ gate_id: "Q3" }], "gate-evaluate", { requireAll: true }),
|
|
113
|
+
(err: Error) => err.message.includes("Q4"),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("passes when requireAll is false and only a subset is pending", () => {
|
|
118
|
+
// execute-task owns Q5/Q6/Q7, but a task with no external dependencies
|
|
119
|
+
// may only have Q7 seeded. That's still valid coverage.
|
|
120
|
+
assert.doesNotThrow(() =>
|
|
121
|
+
assertGateCoverage([{ gate_id: "Q7" }], "execute-task", { requireAll: false }),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("passes when requireAll is true and every owned gate is pending", () => {
|
|
126
|
+
assert.doesNotThrow(() =>
|
|
127
|
+
assertGateCoverage(
|
|
128
|
+
[{ gate_id: "Q3" }, { gate_id: "Q4" }],
|
|
129
|
+
"gate-evaluate",
|
|
130
|
+
{ requireAll: true },
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("empty pending list passes when requireAll is false", () => {
|
|
136
|
+
assert.doesNotThrow(() =>
|
|
137
|
+
assertGateCoverage([], "complete-slice", { requireAll: false }),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-system gate coverage tests.
|
|
3
|
+
*
|
|
4
|
+
* These tests pin the invariants the plan file documents:
|
|
5
|
+
* 1. Every pending slice-scoped gate is routed to exactly one owner turn.
|
|
6
|
+
* Q8 (owned by complete-slice) MUST NOT leak into gate-evaluate and
|
|
7
|
+
* get silently dropped the way it used to before the registry landed.
|
|
8
|
+
* 2. getPendingGatesForTurn filters by the registry's owner turn, not
|
|
9
|
+
* just the DB scope column.
|
|
10
|
+
* 3. Output validators recognize artifacts that contain the required
|
|
11
|
+
* gate section headings, and flag ones that don't.
|
|
12
|
+
* 4. Prompt output produced by the validators reflects MV01-MV04.
|
|
13
|
+
*
|
|
14
|
+
* They also assert the VALIDATION.md renderer still produces headings
|
|
15
|
+
* matching the registry's promptSection strings, so future renderer
|
|
16
|
+
* edits that drift from the registry fail the suite loudly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
openDatabase,
|
|
27
|
+
closeDatabase,
|
|
28
|
+
insertMilestone,
|
|
29
|
+
insertSlice,
|
|
30
|
+
insertTask,
|
|
31
|
+
insertGateRow,
|
|
32
|
+
getPendingGates,
|
|
33
|
+
getPendingGatesForTurn,
|
|
34
|
+
} from "../gsd-db.ts";
|
|
35
|
+
import {
|
|
36
|
+
GATE_REGISTRY,
|
|
37
|
+
getGatesForTurn,
|
|
38
|
+
type OwnerTurn,
|
|
39
|
+
} from "../gate-registry.ts";
|
|
40
|
+
import {
|
|
41
|
+
validateSliceSummaryOutput,
|
|
42
|
+
validateTaskSummaryOutput,
|
|
43
|
+
validateMilestoneValidationOutput,
|
|
44
|
+
validateGateSections,
|
|
45
|
+
} from "../prompt-validation.ts";
|
|
46
|
+
|
|
47
|
+
function setupTestDb(): string {
|
|
48
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "prompt-gate-coverage-"));
|
|
49
|
+
const dbPath = join(tmpDir, "gsd.db");
|
|
50
|
+
openDatabase(dbPath);
|
|
51
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
52
|
+
insertSlice({
|
|
53
|
+
milestoneId: "M001",
|
|
54
|
+
id: "S01",
|
|
55
|
+
title: "Test Slice",
|
|
56
|
+
status: "pending",
|
|
57
|
+
risk: "medium",
|
|
58
|
+
depends: [],
|
|
59
|
+
});
|
|
60
|
+
insertTask({
|
|
61
|
+
id: "T01",
|
|
62
|
+
sliceId: "S01",
|
|
63
|
+
milestoneId: "M001",
|
|
64
|
+
title: "Test Task",
|
|
65
|
+
status: "pending",
|
|
66
|
+
});
|
|
67
|
+
return tmpDir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("getPendingGatesForTurn routes by owner turn, not scope column", () => {
|
|
71
|
+
let tmpDir: string;
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
tmpDir = setupTestDb();
|
|
74
|
+
});
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
closeDatabase();
|
|
77
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("Q8 stored as scope:'slice' is owned by complete-slice, not gate-evaluate", () => {
|
|
81
|
+
// Seed the three slice-scoped gates plan-slice writes today.
|
|
82
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
|
|
83
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", scope: "slice" });
|
|
84
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q8", scope: "slice" });
|
|
85
|
+
|
|
86
|
+
// getPendingGates(..., "slice") returns all three (unchanged).
|
|
87
|
+
const allSlicePending = getPendingGates("M001", "S01", "slice");
|
|
88
|
+
assert.equal(allSlicePending.length, 3);
|
|
89
|
+
|
|
90
|
+
// But the turn-aware helper routes them correctly.
|
|
91
|
+
const gateEval = getPendingGatesForTurn("M001", "S01", "gate-evaluate");
|
|
92
|
+
assert.deepEqual(gateEval.map((g) => g.gate_id).sort(), ["Q3", "Q4"]);
|
|
93
|
+
|
|
94
|
+
const completeSlice = getPendingGatesForTurn("M001", "S01", "complete-slice");
|
|
95
|
+
assert.deepEqual(completeSlice.map((g) => g.gate_id), ["Q8"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("task-scoped gates are scoped to the requested task id", () => {
|
|
99
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q5", scope: "task", taskId: "T01" });
|
|
100
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q6", scope: "task", taskId: "T01" });
|
|
101
|
+
insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q5", scope: "task", taskId: "T02" });
|
|
102
|
+
|
|
103
|
+
const t1 = getPendingGatesForTurn("M001", "S01", "execute-task", "T01");
|
|
104
|
+
assert.equal(t1.length, 2);
|
|
105
|
+
assert.ok(t1.every((g) => g.gate_id === "Q5" || g.gate_id === "Q6"));
|
|
106
|
+
|
|
107
|
+
const t2 = getPendingGatesForTurn("M001", "S01", "execute-task", "T02");
|
|
108
|
+
assert.equal(t2.length, 1);
|
|
109
|
+
assert.equal(t2[0].gate_id, "Q5");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("per-turn output validators", () => {
|
|
114
|
+
test("validateSliceSummaryOutput flags missing Operational Readiness", () => {
|
|
115
|
+
const md = `# S01: Test Slice\n\n## What Happened\nstuff\n\n## Verification\nstuff\n`;
|
|
116
|
+
const result = validateSliceSummaryOutput(md);
|
|
117
|
+
assert.equal(result.valid, false);
|
|
118
|
+
assert.ok(result.missing.some((m) => m.includes("Q8")));
|
|
119
|
+
assert.ok(result.missing.some((m) => m.includes("Operational Readiness")));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("validateSliceSummaryOutput passes when Operational Readiness heading is present", () => {
|
|
123
|
+
const md = `# S01\n\n## Operational Readiness\n- Health: /health\n- Failure: alert\n`;
|
|
124
|
+
const result = validateSliceSummaryOutput(md);
|
|
125
|
+
assert.equal(result.valid, true);
|
|
126
|
+
assert.equal(result.missing.length, 0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("validateMilestoneValidationOutput requires all four MV headings", () => {
|
|
130
|
+
// Missing Requirement Coverage.
|
|
131
|
+
const md = [
|
|
132
|
+
"# Milestone Validation: M001",
|
|
133
|
+
"## Success Criteria Checklist",
|
|
134
|
+
"ok",
|
|
135
|
+
"## Slice Delivery Audit",
|
|
136
|
+
"ok",
|
|
137
|
+
"## Cross-Slice Integration",
|
|
138
|
+
"ok",
|
|
139
|
+
].join("\n\n");
|
|
140
|
+
const result = validateMilestoneValidationOutput(md);
|
|
141
|
+
assert.equal(result.valid, false);
|
|
142
|
+
assert.ok(result.missing.some((m) => m.includes("MV04")));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("validateMilestoneValidationOutput passes for a complete VALIDATION.md", () => {
|
|
146
|
+
const md = [
|
|
147
|
+
"# Milestone Validation: M001",
|
|
148
|
+
"## Success Criteria Checklist",
|
|
149
|
+
"ok",
|
|
150
|
+
"## Slice Delivery Audit",
|
|
151
|
+
"ok",
|
|
152
|
+
"## Cross-Slice Integration",
|
|
153
|
+
"ok",
|
|
154
|
+
"## Requirement Coverage",
|
|
155
|
+
"ok",
|
|
156
|
+
].join("\n\n");
|
|
157
|
+
const result = validateMilestoneValidationOutput(md);
|
|
158
|
+
assert.equal(result.valid, true, `unexpected missing: ${result.missing.join(", ")}`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("validateTaskSummaryOutput flags missing task-gate sections", () => {
|
|
162
|
+
const md = `# T01\n\n## What Happened\nstuff\n\n## Verification\nstuff\n`;
|
|
163
|
+
const result = validateTaskSummaryOutput(md);
|
|
164
|
+
assert.equal(result.valid, false);
|
|
165
|
+
const idsInMissing = result.missing.join(" ");
|
|
166
|
+
assert.ok(idsInMissing.includes("Q5"));
|
|
167
|
+
assert.ok(idsInMissing.includes("Q6"));
|
|
168
|
+
assert.ok(idsInMissing.includes("Q7"));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("validateGateSections returns empty missing when gate bucket is empty", () => {
|
|
172
|
+
// Build a phoney owner turn that owns nothing (simulate by validating
|
|
173
|
+
// against a real turn against an artifact containing every section).
|
|
174
|
+
const fullMd = getGatesForTurn("validate-milestone")
|
|
175
|
+
.map((g) => `## ${g.promptSection}\n\nstuff`)
|
|
176
|
+
.join("\n\n");
|
|
177
|
+
const result = validateGateSections(fullMd, "validate-milestone");
|
|
178
|
+
assert.equal(result.valid, true);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("registry / renderer parity", () => {
|
|
183
|
+
test("MV promptSections match the validate-milestone renderer H2 headings", () => {
|
|
184
|
+
// Mirror the string literals from tools/validate-milestone.ts
|
|
185
|
+
// renderValidationMarkdown() so a rename there flips this test red.
|
|
186
|
+
const expectedHeadings = [
|
|
187
|
+
"Success Criteria Checklist",
|
|
188
|
+
"Slice Delivery Audit",
|
|
189
|
+
"Cross-Slice Integration",
|
|
190
|
+
"Requirement Coverage",
|
|
191
|
+
];
|
|
192
|
+
const registryHeadings = getGatesForTurn("validate-milestone").map((g) => g.promptSection);
|
|
193
|
+
assert.deepEqual(registryHeadings.sort(), [...expectedHeadings].sort());
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("Q8 promptSection matches the complete-slice renderer H2 heading", () => {
|
|
197
|
+
// Mirror the slice-summary H2 introduced in tools/complete-slice.ts.
|
|
198
|
+
assert.equal(GATE_REGISTRY.Q8.promptSection, "Operational Readiness");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("registry owner turns cover every turn gate-registry.ts declares", () => {
|
|
202
|
+
const ownerTurns = new Set<OwnerTurn>(Object.values(GATE_REGISTRY).map((g) => g.ownerTurn));
|
|
203
|
+
assert.ok(ownerTurns.has("gate-evaluate"));
|
|
204
|
+
assert.ok(ownerTurns.has("execute-task"));
|
|
205
|
+
assert.ok(ownerTurns.has("complete-slice"));
|
|
206
|
+
assert.ok(ownerTurns.has("validate-milestone"));
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -32,6 +32,15 @@ test("classifyError detects rate limit from message", () => {
|
|
|
32
32
|
assert.equal(result.kind, "rate-limit");
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
test("classifyError treats OpenRouter affordability errors as transient rate-limit class", () => {
|
|
36
|
+
const result = classifyError(
|
|
37
|
+
"402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.",
|
|
38
|
+
);
|
|
39
|
+
assert.ok(isTransient(result));
|
|
40
|
+
assert.equal(result.kind, "rate-limit");
|
|
41
|
+
assert.ok("retryAfterMs" in result && result.retryAfterMs > 0);
|
|
42
|
+
});
|
|
43
|
+
|
|
35
44
|
test("classifyError extracts reset delay from message", () => {
|
|
36
45
|
const result = classifyError("rate limit exceeded, reset in 45s");
|
|
37
46
|
assert.equal(result.kind, "rate-limit");
|
|
@@ -69,14 +69,15 @@ test("dashboard shortcut resolves the project root instead of the current worktr
|
|
|
69
69
|
|
|
70
70
|
assert.ok(customCalls > 0, "shortcut opens the dashboard overlay when project root is resolved");
|
|
71
71
|
assert.equal(notices.length, 0, "shortcut does not fall back to the missing-.gsd warning");
|
|
72
|
-
assert.equal(shortcuts.length,
|
|
72
|
+
assert.equal(shortcuts.length, 5, "all GSD shortcuts are still registered");
|
|
73
73
|
const keys = shortcuts.map((shortcut) => shortcut.key);
|
|
74
74
|
assert.ok(keys.includes("ctrl+alt+g"), "primary dashboard shortcut is registered");
|
|
75
75
|
assert.ok(keys.includes("ctrl+shift+g"), "fallback dashboard shortcut is registered");
|
|
76
76
|
assert.ok(keys.includes("ctrl+alt+n"), "primary notifications shortcut is registered");
|
|
77
77
|
assert.ok(keys.includes("ctrl+shift+n"), "fallback notifications shortcut is registered");
|
|
78
78
|
assert.ok(keys.includes("ctrl+alt+p"), "primary parallel shortcut is registered");
|
|
79
|
-
|
|
79
|
+
// No Ctrl+Shift+P fallback — conflicts with cycleModelBackward (shift+ctrl+p)
|
|
80
|
+
assert.ok(!keys.includes("ctrl+shift+p"), "parallel fallback must not be registered (conflicts with cycleModelBackward)");
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
test("parallel shortcut passes resolved project root into overlay", async (t) => {
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
getMilestone,
|
|
22
22
|
updateSliceStatus,
|
|
23
23
|
setSliceSummaryMd,
|
|
24
|
+
saveGateResult,
|
|
25
|
+
getPendingGatesForTurn,
|
|
24
26
|
} from "../gsd-db.js";
|
|
27
|
+
import { getGatesForTurn } from "../gate-registry.js";
|
|
25
28
|
import { resolveSliceFile, resolveSlicePath, clearPathCache } from "../paths.js";
|
|
26
29
|
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
27
30
|
import { saveFile, clearParseCache } from "../files.js";
|
|
@@ -39,6 +42,23 @@ export interface CompleteSliceResult {
|
|
|
39
42
|
uatPath: string;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Map a complete-slice-owned gate id to the CompleteSliceParams field
|
|
47
|
+
* whose presence drives `pass` vs. `omitted`. Keep this in lockstep with
|
|
48
|
+
* the gates declared in gate-registry.ts under ownerTurn "complete-slice".
|
|
49
|
+
*/
|
|
50
|
+
function sliceGateFieldForId(
|
|
51
|
+
id: string,
|
|
52
|
+
params: CompleteSliceParams,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
switch (id) {
|
|
55
|
+
case "Q8":
|
|
56
|
+
return params.operationalReadiness;
|
|
57
|
+
default:
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* Render slice summary markdown matching the template format.
|
|
44
64
|
* YAML frontmatter uses snake_case keys for parseSummary() compatibility.
|
|
@@ -169,6 +189,10 @@ ${reqSurfaced}
|
|
|
169
189
|
|
|
170
190
|
${reqInvalidated}
|
|
171
191
|
|
|
192
|
+
## Operational Readiness
|
|
193
|
+
|
|
194
|
+
${params.operationalReadiness?.trim() || "None."}
|
|
195
|
+
|
|
172
196
|
## Deviations
|
|
173
197
|
|
|
174
198
|
${params.deviations || "None."}
|
|
@@ -330,6 +354,45 @@ export async function handleCompleteSlice(
|
|
|
330
354
|
// Store rendered markdown in DB for D004 recovery
|
|
331
355
|
setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
|
|
332
356
|
|
|
357
|
+
// ── Close gates owned by complete-slice (Q8) ───────────────────────────
|
|
358
|
+
// Each owned gate maps to a specific summary section via the registry.
|
|
359
|
+
// If the caller populated the corresponding field, record `pass`; if the
|
|
360
|
+
// field is empty, record `omitted`. Without this loop, Q8 would stay
|
|
361
|
+
// pending forever and block future state derivation (see gate-registry).
|
|
362
|
+
try {
|
|
363
|
+
const pendingGates = getPendingGatesForTurn(
|
|
364
|
+
params.milestoneId,
|
|
365
|
+
params.sliceId,
|
|
366
|
+
"complete-slice",
|
|
367
|
+
);
|
|
368
|
+
if (pendingGates.length > 0) {
|
|
369
|
+
const ownedDefs = new Map(getGatesForTurn("complete-slice").map((g) => [g.id, g] as const));
|
|
370
|
+
for (const row of pendingGates) {
|
|
371
|
+
const def = ownedDefs.get(row.gate_id);
|
|
372
|
+
if (!def) continue;
|
|
373
|
+
// Map gate id → param field it maps to. Keep the map local so
|
|
374
|
+
// adding a new complete-slice gate is a single place change.
|
|
375
|
+
const field = sliceGateFieldForId(def.id, params);
|
|
376
|
+
const hasContent = typeof field === "string" && field.trim().length > 0;
|
|
377
|
+
saveGateResult({
|
|
378
|
+
milestoneId: params.milestoneId,
|
|
379
|
+
sliceId: params.sliceId,
|
|
380
|
+
gateId: def.id,
|
|
381
|
+
verdict: hasContent ? "pass" : "omitted",
|
|
382
|
+
rationale: hasContent
|
|
383
|
+
? `${def.promptSection} section populated in slice summary`
|
|
384
|
+
: `${def.promptSection} section left empty — recorded as omitted`,
|
|
385
|
+
findings: hasContent ? (field as string).trim() : "",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch (gateErr) {
|
|
390
|
+
logWarning(
|
|
391
|
+
"tool",
|
|
392
|
+
`complete-slice gate close warning for ${params.milestoneId}/${params.sliceId}: ${(gateErr as Error).message}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
333
396
|
// Invalidate all caches
|
|
334
397
|
invalidateStateCache();
|
|
335
398
|
clearPathCache();
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
updateTaskStatus,
|
|
25
25
|
setTaskSummaryMd,
|
|
26
26
|
deleteVerificationEvidence,
|
|
27
|
+
saveGateResult,
|
|
28
|
+
getPendingGatesForTurn,
|
|
27
29
|
} from "../gsd-db.js";
|
|
30
|
+
import { getGatesForTurn } from "../gate-registry.js";
|
|
28
31
|
import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
|
|
29
32
|
import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
|
|
30
33
|
import { saveFile, clearParseCache } from "../files.js";
|
|
@@ -44,6 +47,27 @@ export interface CompleteTaskResult {
|
|
|
44
47
|
|
|
45
48
|
import type { TaskRow } from "../gsd-db.js";
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Map an execute-task-owned gate id to the CompleteTaskParams field whose
|
|
52
|
+
* presence drives `pass` vs. `omitted`. Keep in lockstep with the gates
|
|
53
|
+
* declared in gate-registry.ts under ownerTurn "execute-task".
|
|
54
|
+
*/
|
|
55
|
+
function taskGateFieldForId(
|
|
56
|
+
id: string,
|
|
57
|
+
params: CompleteTaskParams,
|
|
58
|
+
): string | undefined {
|
|
59
|
+
switch (id) {
|
|
60
|
+
case "Q5":
|
|
61
|
+
return params.failureModes;
|
|
62
|
+
case "Q6":
|
|
63
|
+
return params.loadProfile;
|
|
64
|
+
case "Q7":
|
|
65
|
+
return params.negativeTests;
|
|
66
|
+
default:
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
/**
|
|
48
72
|
* Normalize a list parameter that may arrive as a string (newline-delimited
|
|
49
73
|
* bullet list from the LLM) into a string array (#3361).
|
|
@@ -236,6 +260,45 @@ export async function handleCompleteTask(
|
|
|
236
260
|
// Store rendered markdown in DB for D004 recovery
|
|
237
261
|
setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
|
|
238
262
|
|
|
263
|
+
// ── Close gates owned by execute-task (Q5/Q6/Q7) for this task ────────
|
|
264
|
+
// Each gate id maps to a specific params field via taskGateFieldForId.
|
|
265
|
+
// When the model populates the field, record `pass`; when it's empty,
|
|
266
|
+
// record `omitted`. Task-scoped rows are filtered by taskId so a single
|
|
267
|
+
// task's completion doesn't touch sibling tasks' gate rows.
|
|
268
|
+
try {
|
|
269
|
+
const pendingGates = getPendingGatesForTurn(
|
|
270
|
+
params.milestoneId,
|
|
271
|
+
params.sliceId,
|
|
272
|
+
"execute-task",
|
|
273
|
+
params.taskId,
|
|
274
|
+
);
|
|
275
|
+
if (pendingGates.length > 0) {
|
|
276
|
+
const ownedDefs = new Map(getGatesForTurn("execute-task").map((g) => [g.id, g] as const));
|
|
277
|
+
for (const row of pendingGates) {
|
|
278
|
+
const def = ownedDefs.get(row.gate_id);
|
|
279
|
+
if (!def) continue;
|
|
280
|
+
const field = taskGateFieldForId(def.id, params);
|
|
281
|
+
const hasContent = typeof field === "string" && field.trim().length > 0;
|
|
282
|
+
saveGateResult({
|
|
283
|
+
milestoneId: params.milestoneId,
|
|
284
|
+
sliceId: params.sliceId,
|
|
285
|
+
taskId: params.taskId,
|
|
286
|
+
gateId: def.id,
|
|
287
|
+
verdict: hasContent ? "pass" : "omitted",
|
|
288
|
+
rationale: hasContent
|
|
289
|
+
? `${def.promptSection} section populated in task summary`
|
|
290
|
+
: `${def.promptSection} section left empty — recorded as omitted`,
|
|
291
|
+
findings: hasContent ? (field as string).trim() : "",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (gateErr) {
|
|
296
|
+
logWarning(
|
|
297
|
+
"tool",
|
|
298
|
+
`complete-task gate close warning for ${params.milestoneId}/${params.sliceId}/${params.taskId}: ${(gateErr as Error).message}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
239
302
|
// Invalidate all caches
|
|
240
303
|
invalidateStateCache();
|
|
241
304
|
clearPathCache();
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
_getAdapter,
|
|
9
9
|
saveGateResult,
|
|
10
10
|
} from "../gsd-db.js";
|
|
11
|
+
import { GATE_REGISTRY } from "../gate-registry.js";
|
|
11
12
|
import { saveArtifactToDb } from "../db-writer.js";
|
|
12
13
|
import type { CompleteMilestoneParams } from "./complete-milestone.js";
|
|
13
14
|
import { handleCompleteMilestone } from "./complete-milestone.js";
|
|
@@ -427,7 +428,9 @@ export async function executeSaveGateResult(
|
|
|
427
428
|
};
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
|
|
431
|
+
// Source of truth: gate-registry.ts. Every declared GateId is accepted,
|
|
432
|
+
// so adding a new gate in one place automatically flows through here.
|
|
433
|
+
const validGates = Object.keys(GATE_REGISTRY);
|
|
431
434
|
if (!validGates.includes(params.gateId)) {
|
|
432
435
|
return {
|
|
433
436
|
content: [{ type: "text", text: `Error: Invalid gateId "${params.gateId}". Must be one of: ${validGates.join(", ")}` }],
|