gsd-pi 2.66.1-dev.9a14d3d → 2.66.1-dev.e700a1b
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/dist/resources/extensions/ask-user-questions.js +79 -11
- package/dist/resources/extensions/gsd/auto-model-selection.js +11 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +49 -2
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +111 -0
- package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
- package/dist/resources/extensions/gsd/detection.js +6 -0
- package/dist/resources/extensions/gsd/index.js +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
- package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
- package/dist/resources/extensions/remote-questions/manager.js +8 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
- 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 +2 -2
- 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 +19 -19
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +4 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +6 -5
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +19 -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__/provider-display-name.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +15 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
- 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/src/core/retry-handler.test.ts +22 -0
- package/packages/pi-coding-agent/src/core/retry-handler.ts +6 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +16 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
- package/src/resources/extensions/ask-user-questions.ts +103 -11
- package/src/resources/extensions/gsd/auto-model-selection.ts +11 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +57 -2
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +128 -0
- package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
- package/src/resources/extensions/gsd/detection.ts +6 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
- package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +21 -7
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +156 -0
- package/src/resources/extensions/remote-questions/manager.ts +9 -0
- /package/dist/web/standalone/.next/static/{UR2XjRqocvGBpbjt8dHCS → TCMOrUQ1suscla6CiwNya}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UR2XjRqocvGBpbjt8dHCS → TCMOrUQ1suscla6CiwNya}/_ssgManifest.js +0 -0
|
@@ -143,17 +143,17 @@ test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", (
|
|
|
143
143
|
|
|
144
144
|
// ─── resolveModelId tests ─────────────────────────────────────────────────
|
|
145
145
|
|
|
146
|
-
test("resolveModelId: bare ID resolves to
|
|
146
|
+
test("resolveModelId: bare ID resolves to claude-code when session is claude-code (#3772)", () => {
|
|
147
147
|
const availableModels = [
|
|
148
148
|
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
|
149
149
|
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
150
150
|
];
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
152
|
+
// When currentProvider is "claude-code" (set by startup migration for subscription
|
|
153
|
+
// users), bare IDs must resolve to claude-code to avoid the third-party block (#3772).
|
|
154
154
|
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
|
155
155
|
assert.ok(result, "should resolve a model");
|
|
156
|
-
assert.equal(result.provider, "
|
|
156
|
+
assert.equal(result.provider, "claude-code", "bare ID must resolve to claude-code when session provider is claude-code");
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
test("resolveModelId: bare ID still prefers current provider when it is a first-class API provider", () => {
|
|
@@ -227,14 +227,28 @@ test("model change notify in selectAndApplyModel is gated behind verbose flag",
|
|
|
227
227
|
);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
test("resolveModelId: anthropic wins over claude-code
|
|
230
|
+
test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
|
|
231
231
|
const availableModels = [
|
|
232
232
|
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
233
233
|
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
|
234
234
|
];
|
|
235
235
|
|
|
236
|
-
//
|
|
236
|
+
// When the session is NOT on claude-code, bare IDs should resolve to
|
|
237
|
+
// the canonical anthropic provider (original #2905 behavior preserved).
|
|
238
|
+
const result = resolveModelId("claude-sonnet-4-6", availableModels, undefined);
|
|
239
|
+
assert.ok(result, "should resolve a model");
|
|
240
|
+
assert.equal(result.provider, "anthropic", "anthropic must win when session is not claude-code");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("resolveModelId: claude-code wins when session is claude-code regardless of list order", () => {
|
|
244
|
+
const availableModels = [
|
|
245
|
+
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
246
|
+
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
// When session provider is claude-code (subscription user migration), it must
|
|
250
|
+
// win regardless of candidate ordering to avoid the third-party block (#3772).
|
|
237
251
|
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
|
238
252
|
assert.ok(result, "should resolve a model");
|
|
239
|
-
assert.equal(result.provider, "
|
|
253
|
+
assert.equal(result.provider, "claude-code", "claude-code must win when it is the session provider");
|
|
240
254
|
});
|
|
@@ -138,6 +138,28 @@ test("generateCodebaseMap: excludes .gsd/ files", () => {
|
|
|
138
138
|
}
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
test("generateCodebaseMap: excludes .claude/ and other tool directories", () => {
|
|
142
|
+
const base = makeTmpRepo();
|
|
143
|
+
try {
|
|
144
|
+
addFile(base, "src/main.ts");
|
|
145
|
+
addFile(base, ".claude/CLAUDE.md");
|
|
146
|
+
addFile(base, ".claude/memory/user.md");
|
|
147
|
+
addFile(base, ".plans/plan.md");
|
|
148
|
+
addFile(base, ".cursor/settings.json");
|
|
149
|
+
addFile(base, ".vscode/settings.json");
|
|
150
|
+
|
|
151
|
+
const result = generateCodebaseMap(base);
|
|
152
|
+
assert.ok(result.content.includes("`src/main.ts`"), "should include src/main.ts");
|
|
153
|
+
assert.ok(!result.content.includes("CLAUDE.md"), "should exclude .claude/ files");
|
|
154
|
+
assert.ok(!result.content.includes("user.md"), "should exclude .claude/memory/ files");
|
|
155
|
+
assert.ok(!result.content.includes(".plans"), "should exclude .plans/ files");
|
|
156
|
+
assert.ok(!result.content.includes(".cursor"), "should exclude .cursor/ files");
|
|
157
|
+
assert.ok(!result.content.includes(".vscode"), "should exclude .vscode/ files");
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup(base);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
141
163
|
test("generateCodebaseMap: excludes binary and lock files", () => {
|
|
142
164
|
const base = makeTmpRepo();
|
|
143
165
|
try {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
detectProjectState,
|
|
18
18
|
detectV1Planning,
|
|
19
19
|
detectProjectSignals,
|
|
20
|
+
scanProjectFiles,
|
|
20
21
|
} from "../detection.ts";
|
|
21
22
|
|
|
22
23
|
function makeTempDir(prefix: string): string {
|
|
@@ -1188,3 +1189,39 @@ test("detectProjectSignals: Spring Boot settings-defined catalog accessor emits
|
|
|
1188
1189
|
cleanup(dir);
|
|
1189
1190
|
}
|
|
1190
1191
|
});
|
|
1192
|
+
|
|
1193
|
+
// ─── scanProjectFiles: RECURSIVE_SCAN_IGNORED_DIRS ──────────────────────
|
|
1194
|
+
|
|
1195
|
+
test("scanProjectFiles: excludes .claude, .gsd, .planning, .plans, .cursor, .vscode directories", () => {
|
|
1196
|
+
const dir = makeTempDir("scan-ignore-dotdirs");
|
|
1197
|
+
try {
|
|
1198
|
+
// Create project files that should be included
|
|
1199
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
1200
|
+
writeFileSync(join(dir, "src", "main.ts"), "// main\n", "utf-8");
|
|
1201
|
+
writeFileSync(join(dir, "README.md"), "# Project\n", "utf-8");
|
|
1202
|
+
|
|
1203
|
+
// Create tool directories that should be excluded
|
|
1204
|
+
const excludedDirs = [".claude", ".gsd", ".planning", ".plans", ".cursor", ".vscode"];
|
|
1205
|
+
for (const d of excludedDirs) {
|
|
1206
|
+
mkdirSync(join(dir, d), { recursive: true });
|
|
1207
|
+
writeFileSync(join(dir, d, "config.json"), "{}\n", "utf-8");
|
|
1208
|
+
}
|
|
1209
|
+
// Nested .claude directory
|
|
1210
|
+
mkdirSync(join(dir, ".claude", "memory"), { recursive: true });
|
|
1211
|
+
writeFileSync(join(dir, ".claude", "memory", "user.md"), "# Memory\n", "utf-8");
|
|
1212
|
+
|
|
1213
|
+
const files = scanProjectFiles(dir);
|
|
1214
|
+
|
|
1215
|
+
// Should include project files
|
|
1216
|
+
assert.ok(files.includes("src/main.ts"), "should include src/main.ts");
|
|
1217
|
+
assert.ok(files.includes("README.md"), "should include README.md");
|
|
1218
|
+
|
|
1219
|
+
// Should exclude all tool directories
|
|
1220
|
+
for (const d of excludedDirs) {
|
|
1221
|
+
const hasExcluded = files.some((f) => f.startsWith(`${d}/`));
|
|
1222
|
+
assert.ok(!hasExcluded, `should exclude ${d}/ directory but found: ${files.filter((f) => f.startsWith(`${d}/`)).join(", ")}`);
|
|
1223
|
+
}
|
|
1224
|
+
} finally {
|
|
1225
|
+
cleanup(dir);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
@@ -760,6 +760,104 @@ test("ask-user-questions source-level: tryRemoteQuestions is called before the h
|
|
|
760
760
|
);
|
|
761
761
|
});
|
|
762
762
|
|
|
763
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
764
|
+
// Race model tests (#3810) — local TUI races against remote channel
|
|
765
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
766
|
+
|
|
767
|
+
test("ask-user-questions source-level: raceRemoteAndLocal function exists", () => {
|
|
768
|
+
const src = readFileSync(
|
|
769
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
770
|
+
"utf-8",
|
|
771
|
+
);
|
|
772
|
+
assert.ok(
|
|
773
|
+
src.includes("async function raceRemoteAndLocal("),
|
|
774
|
+
"raceRemoteAndLocal helper should exist for racing local TUI against remote channel",
|
|
775
|
+
);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("ask-user-questions source-level: race path uses isRemoteConfigured for routing", () => {
|
|
779
|
+
const src = readFileSync(
|
|
780
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
781
|
+
"utf-8",
|
|
782
|
+
);
|
|
783
|
+
assert.ok(
|
|
784
|
+
src.includes("isRemoteConfigured()"),
|
|
785
|
+
"execute() should call isRemoteConfigured() for lightweight routing decision",
|
|
786
|
+
);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("ask-user-questions source-level: race path checks both hasRemote and ctx.hasUI", () => {
|
|
790
|
+
// Regression: #3810 — the race should only activate when BOTH remote and local UI
|
|
791
|
+
// are available. Headless mode should still use remote-only, and no-remote should
|
|
792
|
+
// use local-only.
|
|
793
|
+
const src = readFileSync(
|
|
794
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
795
|
+
"utf-8",
|
|
796
|
+
);
|
|
797
|
+
assert.ok(
|
|
798
|
+
src.includes("hasRemote && ctx.hasUI"),
|
|
799
|
+
"Race path should require both remote configured and local UI available",
|
|
800
|
+
);
|
|
801
|
+
assert.ok(
|
|
802
|
+
src.includes("hasRemote && !ctx.hasUI"),
|
|
803
|
+
"Headless path should handle remote-only when no local UI",
|
|
804
|
+
);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("ask-user-questions source-level: race treats remote timeout as non-win", () => {
|
|
808
|
+
// Regression: the whole point of the race is that a remote timeout should NOT
|
|
809
|
+
// block the local TUI. The race helper must filter out timed_out results.
|
|
810
|
+
const src = readFileSync(
|
|
811
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
812
|
+
"utf-8",
|
|
813
|
+
);
|
|
814
|
+
const raceFnStart = src.indexOf("async function raceRemoteAndLocal(");
|
|
815
|
+
const raceFnEnd = src.indexOf("\n}", raceFnStart);
|
|
816
|
+
const raceFnBody = src.slice(raceFnStart, raceFnEnd);
|
|
817
|
+
assert.ok(
|
|
818
|
+
raceFnBody.includes("timed_out"),
|
|
819
|
+
"raceRemoteAndLocal should check for timed_out in remote results",
|
|
820
|
+
);
|
|
821
|
+
assert.ok(
|
|
822
|
+
raceFnBody.includes("details?.error"),
|
|
823
|
+
"raceRemoteAndLocal should check for error in remote results",
|
|
824
|
+
);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test("ask-user-questions source-level: race uses AbortController to cancel loser", () => {
|
|
828
|
+
const src = readFileSync(
|
|
829
|
+
join(__dirname, "..", "..", "ask-user-questions.ts"),
|
|
830
|
+
"utf-8",
|
|
831
|
+
);
|
|
832
|
+
assert.ok(
|
|
833
|
+
src.includes("new AbortController()"),
|
|
834
|
+
"Race path should create an AbortController for cancellation",
|
|
835
|
+
);
|
|
836
|
+
assert.ok(
|
|
837
|
+
src.includes("controller.abort()"),
|
|
838
|
+
"raceRemoteAndLocal should abort the controller to cancel the losing side",
|
|
839
|
+
);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("manager source-level: isRemoteConfigured export exists", () => {
|
|
843
|
+
const src = readFileSync(
|
|
844
|
+
join(__dirname, "..", "..", "remote-questions", "manager.ts"),
|
|
845
|
+
"utf-8",
|
|
846
|
+
);
|
|
847
|
+
assert.ok(
|
|
848
|
+
src.includes("export function isRemoteConfigured()"),
|
|
849
|
+
"manager.ts should export isRemoteConfigured for lightweight config checking",
|
|
850
|
+
);
|
|
851
|
+
// Must delegate to resolveRemoteConfig — no separate config parsing
|
|
852
|
+
const fnStart = src.indexOf("export function isRemoteConfigured()");
|
|
853
|
+
const fnEnd = src.indexOf("\n}", fnStart);
|
|
854
|
+
const fnBody = src.slice(fnStart, fnEnd);
|
|
855
|
+
assert.ok(
|
|
856
|
+
fnBody.includes("resolveRemoteConfig()"),
|
|
857
|
+
"isRemoteConfigured should delegate to resolveRemoteConfig",
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
763
861
|
test("config source-level: removeProviderToken uses auth.remove not auth.set with empty key", () => {
|
|
764
862
|
const commandSrc = readFileSync(
|
|
765
863
|
join(__dirname, "..", "..", "remote-questions", "remote-command.ts"),
|
|
@@ -195,6 +195,162 @@ test('write-gate: markDepthVerified unblocks queue-mode writes when milestoneId
|
|
|
195
195
|
clearDiscussionFlowState();
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
199
|
+
// Discussion gate enforcement tests (pending gate mechanism)
|
|
200
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
201
|
+
|
|
202
|
+
import {
|
|
203
|
+
isGateQuestionId,
|
|
204
|
+
shouldBlockPendingGate,
|
|
205
|
+
shouldBlockPendingGateBash,
|
|
206
|
+
setPendingGate,
|
|
207
|
+
clearPendingGate,
|
|
208
|
+
getPendingGate,
|
|
209
|
+
} from '../bootstrap/write-gate.ts';
|
|
210
|
+
|
|
211
|
+
// ─── Scenario 19: isGateQuestionId recognizes all gate patterns ──
|
|
212
|
+
|
|
213
|
+
test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
|
|
214
|
+
assert.strictEqual(isGateQuestionId('layer1_scope_gate'), true);
|
|
215
|
+
assert.strictEqual(isGateQuestionId('layer2_architecture_gate'), true);
|
|
216
|
+
assert.strictEqual(isGateQuestionId('layer3_error_gate'), true);
|
|
217
|
+
assert.strictEqual(isGateQuestionId('layer4_quality_gate'), true);
|
|
218
|
+
assert.strictEqual(isGateQuestionId('depth_verification'), true);
|
|
219
|
+
assert.strictEqual(isGateQuestionId('depth_verification_M002'), true);
|
|
220
|
+
assert.strictEqual(isGateQuestionId('my_layer1_scope_gate_question'), true);
|
|
221
|
+
// Non-gate question IDs
|
|
222
|
+
assert.strictEqual(isGateQuestionId('project_intent'), false);
|
|
223
|
+
assert.strictEqual(isGateQuestionId('feature_priority'), false);
|
|
224
|
+
assert.strictEqual(isGateQuestionId(''), false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ─── Scenario 20: setPendingGate / getPendingGate / clearPendingGate lifecycle ──
|
|
228
|
+
|
|
229
|
+
test('write-gate: pending gate lifecycle (set, get, clear)', () => {
|
|
230
|
+
clearDiscussionFlowState();
|
|
231
|
+
assert.strictEqual(getPendingGate(), null, 'starts null');
|
|
232
|
+
|
|
233
|
+
setPendingGate('layer1_scope_gate');
|
|
234
|
+
assert.strictEqual(getPendingGate(), 'layer1_scope_gate', 'set correctly');
|
|
235
|
+
|
|
236
|
+
clearPendingGate();
|
|
237
|
+
assert.strictEqual(getPendingGate(), null, 'cleared correctly');
|
|
238
|
+
|
|
239
|
+
// clearDiscussionFlowState also clears pending gate
|
|
240
|
+
setPendingGate('layer2_architecture_gate');
|
|
241
|
+
clearDiscussionFlowState();
|
|
242
|
+
assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── Scenario 21: shouldBlockPendingGate blocks non-safe tools when gate is pending ──
|
|
246
|
+
|
|
247
|
+
test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
|
|
248
|
+
clearDiscussionFlowState();
|
|
249
|
+
setPendingGate('layer1_scope_gate');
|
|
250
|
+
|
|
251
|
+
// write should be blocked during discussion
|
|
252
|
+
const writeResult = shouldBlockPendingGate('write', 'M001', false);
|
|
253
|
+
assert.strictEqual(writeResult.block, true, 'write should be blocked');
|
|
254
|
+
assert.ok(writeResult.reason!.includes('layer1_scope_gate'), 'reason mentions the gate');
|
|
255
|
+
|
|
256
|
+
// edit should be blocked
|
|
257
|
+
const editResult = shouldBlockPendingGate('edit', 'M001', false);
|
|
258
|
+
assert.strictEqual(editResult.block, true, 'edit should be blocked');
|
|
259
|
+
|
|
260
|
+
// gsd tools should be blocked
|
|
261
|
+
const gsdResult = shouldBlockPendingGate('gsd_plan_milestone', 'M001', false);
|
|
262
|
+
assert.strictEqual(gsdResult.block, true, 'gsd tools should be blocked');
|
|
263
|
+
|
|
264
|
+
clearDiscussionFlowState();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ─── Scenario 22: shouldBlockPendingGate allows safe tools when gate is pending ──
|
|
268
|
+
|
|
269
|
+
test('write-gate: shouldBlockPendingGate allows read-only and ask_user_questions during pending gate', () => {
|
|
270
|
+
clearDiscussionFlowState();
|
|
271
|
+
setPendingGate('layer1_scope_gate');
|
|
272
|
+
|
|
273
|
+
// ask_user_questions is always safe (model needs to re-ask)
|
|
274
|
+
assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
|
|
275
|
+
// read-only tools are safe
|
|
276
|
+
assert.strictEqual(shouldBlockPendingGate('read', 'M001').block, false);
|
|
277
|
+
assert.strictEqual(shouldBlockPendingGate('grep', 'M001').block, false);
|
|
278
|
+
assert.strictEqual(shouldBlockPendingGate('glob', 'M001').block, false);
|
|
279
|
+
assert.strictEqual(shouldBlockPendingGate('ls', 'M001').block, false);
|
|
280
|
+
|
|
281
|
+
clearDiscussionFlowState();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─── Scenario 23: shouldBlockPendingGate does not block outside discussion ──
|
|
285
|
+
|
|
286
|
+
test('write-gate: shouldBlockPendingGate does not block outside discussion', () => {
|
|
287
|
+
clearDiscussionFlowState();
|
|
288
|
+
setPendingGate('layer1_scope_gate');
|
|
289
|
+
|
|
290
|
+
// No milestoneId and no queue phase — not in discussion
|
|
291
|
+
const result = shouldBlockPendingGate('write', null, false);
|
|
292
|
+
assert.strictEqual(result.block, false, 'should not block outside discussion');
|
|
293
|
+
|
|
294
|
+
clearDiscussionFlowState();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ─── Scenario 24: shouldBlockPendingGate blocks in queue mode ──
|
|
298
|
+
|
|
299
|
+
test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pending', () => {
|
|
300
|
+
clearDiscussionFlowState();
|
|
301
|
+
setQueuePhaseActive(true);
|
|
302
|
+
setPendingGate('depth_verification');
|
|
303
|
+
|
|
304
|
+
const result = shouldBlockPendingGate('write', null, true);
|
|
305
|
+
assert.strictEqual(result.block, true, 'should block in queue mode');
|
|
306
|
+
|
|
307
|
+
clearDiscussionFlowState();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ─── Scenario 25: shouldBlockPendingGateBash allows read-only commands ──
|
|
311
|
+
|
|
312
|
+
test('write-gate: shouldBlockPendingGateBash allows read-only commands during pending gate', () => {
|
|
313
|
+
clearDiscussionFlowState();
|
|
314
|
+
setPendingGate('layer2_architecture_gate');
|
|
315
|
+
|
|
316
|
+
assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, false);
|
|
317
|
+
assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, false);
|
|
318
|
+
assert.strictEqual(shouldBlockPendingGateBash('grep -r pattern .', 'M001').block, false);
|
|
319
|
+
assert.strictEqual(shouldBlockPendingGateBash('ls -la', 'M001').block, false);
|
|
320
|
+
|
|
321
|
+
clearDiscussionFlowState();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ─── Scenario 26: shouldBlockPendingGateBash blocks mutating commands ──
|
|
325
|
+
|
|
326
|
+
test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
|
|
327
|
+
clearDiscussionFlowState();
|
|
328
|
+
setPendingGate('layer2_architecture_gate');
|
|
329
|
+
|
|
330
|
+
const result = shouldBlockPendingGateBash('npm run build', 'M001');
|
|
331
|
+
assert.strictEqual(result.block, true, 'mutating bash should be blocked');
|
|
332
|
+
assert.ok(result.reason!.includes('layer2_architecture_gate'));
|
|
333
|
+
|
|
334
|
+
clearDiscussionFlowState();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ─── Scenario 27: no pending gate means no blocking ──
|
|
338
|
+
|
|
339
|
+
test('write-gate: no pending gate means no blocking', () => {
|
|
340
|
+
clearDiscussionFlowState();
|
|
341
|
+
|
|
342
|
+
assert.strictEqual(shouldBlockPendingGate('write', 'M001').block, false);
|
|
343
|
+
assert.strictEqual(shouldBlockPendingGateBash('npm run build', 'M001').block, false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ─── Scenario 28: resetWriteGateState clears pending gate ──
|
|
347
|
+
|
|
348
|
+
test('write-gate: resetWriteGateState clears pending gate', () => {
|
|
349
|
+
setPendingGate('layer3_error_gate');
|
|
350
|
+
resetWriteGateState();
|
|
351
|
+
assert.strictEqual(getPendingGate(), null);
|
|
352
|
+
});
|
|
353
|
+
|
|
198
354
|
// ─── Standard options fixture used across depth confirmation tests ──
|
|
199
355
|
|
|
200
356
|
const STANDARD_OPTIONS = [
|
|
@@ -24,6 +24,15 @@ interface QuestionInput {
|
|
|
24
24
|
allowMultiple?: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Check whether a remote channel is configured without triggering any
|
|
29
|
+
* side effects (no HTTP requests, no prompt records). Used by the race
|
|
30
|
+
* logic to decide routing before committing to a remote dispatch.
|
|
31
|
+
*/
|
|
32
|
+
export function isRemoteConfigured(): boolean {
|
|
33
|
+
return resolveRemoteConfig() !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
export async function tryRemoteQuestions(
|
|
28
37
|
questions: QuestionInput[],
|
|
29
38
|
signal?: AbortSignal,
|
|
File without changes
|
|
File without changes
|