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
|
@@ -8,6 +8,8 @@ type GSDShortcutDef = {
|
|
|
8
8
|
key: "g" | "n" | "p";
|
|
9
9
|
action: string;
|
|
10
10
|
command: string;
|
|
11
|
+
/** Whether the Ctrl+Shift fallback is registered (false when it conflicts with an app keybinding). */
|
|
12
|
+
hasFallback: boolean;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export const GSD_SHORTCUTS: Record<GSDShortcutId, GSDShortcutDef> = {
|
|
@@ -15,16 +17,19 @@ export const GSD_SHORTCUTS: Record<GSDShortcutId, GSDShortcutDef> = {
|
|
|
15
17
|
key: "g",
|
|
16
18
|
action: "Open GSD dashboard",
|
|
17
19
|
command: "/gsd status",
|
|
20
|
+
hasFallback: true,
|
|
18
21
|
},
|
|
19
22
|
notifications: {
|
|
20
23
|
key: "n",
|
|
21
24
|
action: "Open notification history",
|
|
22
25
|
command: "/gsd notifications",
|
|
26
|
+
hasFallback: true,
|
|
23
27
|
},
|
|
24
28
|
parallel: {
|
|
25
29
|
key: "p",
|
|
26
30
|
action: "Open parallel worker monitor",
|
|
27
31
|
command: "/gsd parallel watch",
|
|
32
|
+
hasFallback: false, // Ctrl+Shift+P conflicts with cycleModelBackward
|
|
28
33
|
},
|
|
29
34
|
};
|
|
30
35
|
|
|
@@ -41,7 +46,9 @@ export function fallbackShortcutCombo(id: GSDShortcutId): string {
|
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export function shortcutPair(id: GSDShortcutId, formatter: (combo: string) => string = (combo) => combo): string {
|
|
44
|
-
|
|
49
|
+
const primary = formatter(primaryShortcutCombo(id));
|
|
50
|
+
if (!GSD_SHORTCUTS[id].hasFallback) return primary;
|
|
51
|
+
return `${primary} / ${formatter(fallbackShortcutCombo(id))}`;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export function formattedShortcutPair(id: GSDShortcutId): string {
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
insertSlice,
|
|
59
59
|
insertTask,
|
|
60
60
|
updateTaskStatus,
|
|
61
|
-
|
|
61
|
+
getPendingGateCountForTurn,
|
|
62
62
|
type MilestoneRow,
|
|
63
63
|
type SliceRow,
|
|
64
64
|
type TaskRow,
|
|
@@ -864,7 +864,18 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
|
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
-
|
|
867
|
+
// ── Quality gate evaluation check ──────────────────────────────────
|
|
868
|
+
// Pause before execution only when gates owned by the `gate-evaluate`
|
|
869
|
+
// turn (Q3/Q4) are still pending. Q8 is also `scope:"slice"` but is
|
|
870
|
+
// owned by `complete-slice`, so it must NOT block the evaluating-gates
|
|
871
|
+
// phase — otherwise auto-loop stalls forever waiting for a gate that
|
|
872
|
+
// this turn never evaluates. See gate-registry.ts for the ownership map.
|
|
873
|
+
// Slices with zero gate rows (pre-feature or simple) skip straight through.
|
|
874
|
+
const pendingGateCount = getPendingGateCountForTurn(
|
|
875
|
+
activeMilestone.id,
|
|
876
|
+
activeSlice.id,
|
|
877
|
+
"gate-evaluate",
|
|
878
|
+
);
|
|
868
879
|
if (pendingGateCount > 0) {
|
|
869
880
|
return {
|
|
870
881
|
activeMilestone, activeSlice, activeTask: null,
|
|
@@ -48,3 +48,17 @@ test("bootstrapAutoSession checks manual session override before preferences", (
|
|
|
48
48
|
"manual override and preference fallback must be resolved before building startModelSnapshot",
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test("bootstrapAutoSession validates preferred model against live registry auth (#unconfigured-models)", () => {
|
|
53
|
+
// The raw PREFERENCES.md value must be validated against getAvailable()
|
|
54
|
+
// before being captured as the snapshot, so an unconfigured provider
|
|
55
|
+
// (no API key / OAuth) can't become autoModeStartModel.
|
|
56
|
+
const validationIdx = source.indexOf("ctx.modelRegistry.getAvailable()");
|
|
57
|
+
assert.ok(validationIdx > -1, "auto-start.ts should validate preferred model against getAvailable()");
|
|
58
|
+
|
|
59
|
+
const resolveModelIdIdx = source.indexOf("resolveModelId");
|
|
60
|
+
assert.ok(resolveModelIdIdx > -1, "auto-start.ts should resolve preferred model against the registry");
|
|
61
|
+
|
|
62
|
+
const warningIdx = source.indexOf("is not configured; falling back to session default");
|
|
63
|
+
assert.ok(warningIdx > -1, "auto-start.ts should warn when preferred model is unconfigured");
|
|
64
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* complete-slice gate closure integration test.
|
|
3
|
+
*
|
|
4
|
+
* Pins the fix for the Q8-stall bug: complete-slice must close every gate
|
|
5
|
+
* owned by the complete-slice turn based on the content of the matching
|
|
6
|
+
* CompleteSliceParams field. Without this, Q8 stays pending forever and
|
|
7
|
+
* blocks state derivation on subsequent loops.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
openDatabase,
|
|
18
|
+
closeDatabase,
|
|
19
|
+
insertMilestone,
|
|
20
|
+
insertSlice,
|
|
21
|
+
insertTask,
|
|
22
|
+
insertGateRow,
|
|
23
|
+
getGateResults,
|
|
24
|
+
} from "../gsd-db.ts";
|
|
25
|
+
import { handleCompleteSlice } from "../tools/complete-slice.ts";
|
|
26
|
+
import type { CompleteSliceParams } from "../types.ts";
|
|
27
|
+
|
|
28
|
+
function makeValidSliceParams(overrides: Partial<CompleteSliceParams> = {}): CompleteSliceParams {
|
|
29
|
+
return {
|
|
30
|
+
sliceId: "S01",
|
|
31
|
+
milestoneId: "M001",
|
|
32
|
+
sliceTitle: "Test Slice",
|
|
33
|
+
oneLiner: "Implemented test slice",
|
|
34
|
+
narrative: "Built and tested.",
|
|
35
|
+
verification: "All tests pass.",
|
|
36
|
+
deviations: "None.",
|
|
37
|
+
knownLimitations: "None.",
|
|
38
|
+
followUps: "None.",
|
|
39
|
+
keyFiles: ["src/foo.ts"],
|
|
40
|
+
keyDecisions: [],
|
|
41
|
+
patternsEstablished: [],
|
|
42
|
+
observabilitySurfaces: [],
|
|
43
|
+
provides: [],
|
|
44
|
+
requirementsSurfaced: [],
|
|
45
|
+
drillDownPaths: [],
|
|
46
|
+
affects: [],
|
|
47
|
+
requirementsAdvanced: [],
|
|
48
|
+
requirementsValidated: [],
|
|
49
|
+
requirementsInvalidated: [],
|
|
50
|
+
filesModified: [],
|
|
51
|
+
requires: [],
|
|
52
|
+
uatContent: "## Smoke Test\n\nVerify happy path.",
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("complete-slice closes complete-slice-owned gates", () => {
|
|
58
|
+
let dbPath: string;
|
|
59
|
+
let basePath: string;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
dbPath = path.join(
|
|
63
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "gsd-slice-gate-")),
|
|
64
|
+
"test.db",
|
|
65
|
+
);
|
|
66
|
+
openDatabase(dbPath);
|
|
67
|
+
|
|
68
|
+
basePath = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-slice-gate-handler-"));
|
|
69
|
+
const sliceDir = path.join(
|
|
70
|
+
basePath, ".gsd", "milestones", "M001", "slices", "S01", "tasks",
|
|
71
|
+
);
|
|
72
|
+
fs.mkdirSync(sliceDir, { recursive: true });
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(basePath, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
75
|
+
[
|
|
76
|
+
"# M001: Test Milestone",
|
|
77
|
+
"",
|
|
78
|
+
"## Slices",
|
|
79
|
+
"",
|
|
80
|
+
'- [ ] **S01: Test Slice** `risk:medium` `depends:[]`',
|
|
81
|
+
" - After this: basic functionality works",
|
|
82
|
+
].join("\n"),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
insertMilestone({ id: "M001" });
|
|
86
|
+
insertSlice({ id: "S01", milestoneId: "M001" });
|
|
87
|
+
insertTask({
|
|
88
|
+
id: "T01", sliceId: "S01", milestoneId: "M001",
|
|
89
|
+
status: "complete", title: "Task 1",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Seed Q8 as pending — this is what plan-slice does today.
|
|
93
|
+
insertGateRow({
|
|
94
|
+
milestoneId: "M001", sliceId: "S01",
|
|
95
|
+
gateId: "Q8", scope: "slice",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
closeDatabase();
|
|
101
|
+
fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
|
|
102
|
+
fs.rmSync(basePath, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("Q8 closes as 'pass' when operationalReadiness is populated", async () => {
|
|
106
|
+
const params = makeValidSliceParams({
|
|
107
|
+
operationalReadiness: [
|
|
108
|
+
"- Health signal: /health endpoint returns 200",
|
|
109
|
+
"- Failure signal: error rate alert in observability dashboard",
|
|
110
|
+
"- Recovery: systemd auto-restart",
|
|
111
|
+
].join("\n"),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
115
|
+
assert.ok(!("error" in result), `handler failed: ${(result as any).error}`);
|
|
116
|
+
|
|
117
|
+
const gates = getGateResults("M001", "S01", "slice");
|
|
118
|
+
const q8 = gates.find((g) => g.gate_id === "Q8");
|
|
119
|
+
assert.ok(q8, "Q8 row must exist after complete-slice");
|
|
120
|
+
assert.equal(q8.status, "complete");
|
|
121
|
+
assert.equal(q8.verdict, "pass");
|
|
122
|
+
assert.ok(
|
|
123
|
+
q8.findings.includes("Health signal"),
|
|
124
|
+
"Q8 findings must capture the operationalReadiness content",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("Q8 closes as 'omitted' when operationalReadiness is empty", async () => {
|
|
129
|
+
const params = makeValidSliceParams({ operationalReadiness: "" });
|
|
130
|
+
|
|
131
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
132
|
+
assert.ok(!("error" in result), `handler failed: ${(result as any).error}`);
|
|
133
|
+
|
|
134
|
+
const gates = getGateResults("M001", "S01", "slice");
|
|
135
|
+
const q8 = gates.find((g) => g.gate_id === "Q8");
|
|
136
|
+
assert.ok(q8, "Q8 row must exist after complete-slice");
|
|
137
|
+
assert.equal(q8.status, "complete");
|
|
138
|
+
assert.equal(q8.verdict, "omitted");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("Q8 also closes when operationalReadiness is omitted entirely", async () => {
|
|
142
|
+
// A model that doesn't pass operationalReadiness at all must still
|
|
143
|
+
// move Q8 out of 'pending' — leaving it pending produces the stall.
|
|
144
|
+
const params = makeValidSliceParams();
|
|
145
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
146
|
+
assert.ok(!("error" in result), `handler failed: ${(result as any).error}`);
|
|
147
|
+
|
|
148
|
+
const gates = getGateResults("M001", "S01", "slice");
|
|
149
|
+
const q8 = gates.find((g) => g.gate_id === "Q8");
|
|
150
|
+
assert.ok(q8);
|
|
151
|
+
assert.notEqual(q8.status, "pending", "Q8 must never remain pending after complete-slice");
|
|
152
|
+
assert.equal(q8.verdict, "omitted");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("summary markdown contains Operational Readiness section", async () => {
|
|
156
|
+
const params = makeValidSliceParams({
|
|
157
|
+
operationalReadiness: "- Health signal: /health\n- Failure signal: alert",
|
|
158
|
+
});
|
|
159
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
160
|
+
assert.ok(!("error" in result));
|
|
161
|
+
if (!("error" in result)) {
|
|
162
|
+
const summary = fs.readFileSync(result.summaryPath, "utf-8");
|
|
163
|
+
assert.match(summary, /^## Operational Readiness/m);
|
|
164
|
+
assert.match(summary, /Health signal: \/health/);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -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
|
+
});
|