gsd-pi 2.70.0-dev.c236ea4 → 2.70.1-dev.3591dcf
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/gsd/auto-model-selection.js +33 -19
- package/dist/resources/extensions/gsd/auto-start.js +25 -1
- package/dist/resources/extensions/gsd/guided-flow.js +2 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- 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 +14 -14
- 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/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +39 -25
- package/src/resources/extensions/gsd/auto-start.ts +34 -1
- package/src/resources/extensions/gsd/guided-flow.ts +1 -0
- package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +207 -0
- /package/dist/web/standalone/.next/static/{LWbeDf2XwDjfq_mOlqoGf → KdlODhIktLmeRKpLpHdKb}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{LWbeDf2XwDjfq_mOlqoGf → KdlODhIktLmeRKpLpHdKb}/_ssgManifest.js +0 -0
|
@@ -83,7 +83,7 @@ import { join } from "node:path";
|
|
|
83
83
|
import { sep as pathSep } from "node:path";
|
|
84
84
|
|
|
85
85
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
86
|
-
import { resolveDefaultSessionModel } from "./preferences-models.js";
|
|
86
|
+
import { resolveDefaultSessionModel, resolveDynamicRoutingConfig } from "./preferences-models.js";
|
|
87
87
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
88
88
|
|
|
89
89
|
export interface BootstrapDeps {
|
|
@@ -778,6 +778,39 @@ export async function bootstrapAutoSession(
|
|
|
778
778
|
: "Will loop until milestone complete.";
|
|
779
779
|
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
780
780
|
|
|
781
|
+
// Show dynamic routing status so users know upfront if models will be
|
|
782
|
+
// downgraded for simple tasks (#3962).
|
|
783
|
+
// Use the same effective logic as selectAndApplyModel: check flat-rate
|
|
784
|
+
// provider suppression and resolve the actual ceiling model.
|
|
785
|
+
const routingConfig = resolveDynamicRoutingConfig();
|
|
786
|
+
const startModelLabel = s.autoModeStartModel
|
|
787
|
+
? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
|
|
788
|
+
: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
|
|
789
|
+
|
|
790
|
+
// Flat-rate providers (e.g. GitHub Copilot, claude-code) suppress routing
|
|
791
|
+
// at dispatch time (#3453) — reflect that in the banner.
|
|
792
|
+
const { isFlatRateProvider } = await import("./auto-model-selection.js");
|
|
793
|
+
const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
|
|
794
|
+
const effectivelyEnabled = routingConfig.enabled
|
|
795
|
+
&& !(effectiveProvider && isFlatRateProvider(effectiveProvider));
|
|
796
|
+
|
|
797
|
+
// The actual ceiling may come from tier_models.heavy, not the start model.
|
|
798
|
+
const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
|
|
799
|
+
? routingConfig.tier_models.heavy
|
|
800
|
+
: startModelLabel;
|
|
801
|
+
|
|
802
|
+
if (effectivelyEnabled) {
|
|
803
|
+
ctx.ui.notify(
|
|
804
|
+
`Dynamic routing: enabled — simple tasks may use cheaper models (ceiling: ${effectiveCeiling})`,
|
|
805
|
+
"info",
|
|
806
|
+
);
|
|
807
|
+
} else {
|
|
808
|
+
ctx.ui.notify(
|
|
809
|
+
`Dynamic routing: disabled — all tasks will use ${startModelLabel}`,
|
|
810
|
+
"info",
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
781
814
|
updateSessionLock(
|
|
782
815
|
lockBase(),
|
|
783
816
|
"starting",
|
|
@@ -295,6 +295,7 @@ async function dispatchWorkflow(
|
|
|
295
295
|
const result = await selectAndApplyModel(
|
|
296
296
|
ctx, pi, unitType, /* unitId */ "", /* basePath */ process.cwd(),
|
|
297
297
|
prefs, /* verbose */ false, /* autoModeStartModel */ null,
|
|
298
|
+
/* retryContext */ undefined, /* isAutoMode */ false,
|
|
298
299
|
);
|
|
299
300
|
if (result.appliedModel) {
|
|
300
301
|
debugLog("guided-flow-model-applied", {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// GSD Extension — Interactive Routing Bypass Tests
|
|
2
|
+
// Verifies that dynamic routing is skipped for interactive (guided-flow) dispatches
|
|
3
|
+
// and that model downgrade notifications always fire (#3962).
|
|
4
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
5
|
+
|
|
6
|
+
import test, { describe } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// ─── Source-level structural tests ──────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const modelSelectionSrc = readFileSync(
|
|
17
|
+
join(__dirname, "..", "auto-model-selection.ts"),
|
|
18
|
+
"utf-8",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const guidedFlowSrc = readFileSync(
|
|
22
|
+
join(__dirname, "..", "guided-flow.ts"),
|
|
23
|
+
"utf-8",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const autoStartSrc = readFileSync(
|
|
27
|
+
join(__dirname, "..", "auto-start.ts"),
|
|
28
|
+
"utf-8",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
describe("interactive routing bypass (#3962)", () => {
|
|
32
|
+
test("selectAndApplyModel accepts isAutoMode parameter", () => {
|
|
33
|
+
// The function signature should include isAutoMode with a default of true
|
|
34
|
+
assert.ok(
|
|
35
|
+
modelSelectionSrc.includes("isAutoMode"),
|
|
36
|
+
"selectAndApplyModel should have isAutoMode parameter",
|
|
37
|
+
);
|
|
38
|
+
assert.ok(
|
|
39
|
+
modelSelectionSrc.includes("isAutoMode = true"),
|
|
40
|
+
"isAutoMode should default to true (auto-mode behavior preserved)",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("routing is disabled when isAutoMode is false", () => {
|
|
45
|
+
// The code should disable routing when not in auto-mode
|
|
46
|
+
assert.ok(
|
|
47
|
+
modelSelectionSrc.includes("if (!isAutoMode)"),
|
|
48
|
+
"should check isAutoMode flag to disable routing",
|
|
49
|
+
);
|
|
50
|
+
assert.ok(
|
|
51
|
+
modelSelectionSrc.includes("routingConfig.enabled = false"),
|
|
52
|
+
"should set routingConfig.enabled = false for interactive mode",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("resolvePreferredModelConfig skips routing synthesis when isAutoMode is false", () => {
|
|
57
|
+
// resolvePreferredModelConfig should accept isAutoMode and bail early
|
|
58
|
+
// before synthesizing a routing ceiling from tier_models (#3962 codex review)
|
|
59
|
+
assert.ok(
|
|
60
|
+
modelSelectionSrc.includes("function resolvePreferredModelConfig"),
|
|
61
|
+
"resolvePreferredModelConfig should exist",
|
|
62
|
+
);
|
|
63
|
+
// The function should check isAutoMode before routing synthesis
|
|
64
|
+
const fnIdx = modelSelectionSrc.indexOf("function resolvePreferredModelConfig");
|
|
65
|
+
const fnBody = modelSelectionSrc.slice(fnIdx, fnIdx + 600);
|
|
66
|
+
assert.ok(
|
|
67
|
+
fnBody.includes("isAutoMode"),
|
|
68
|
+
"resolvePreferredModelConfig should accept isAutoMode parameter",
|
|
69
|
+
);
|
|
70
|
+
assert.ok(
|
|
71
|
+
fnBody.includes("if (!isAutoMode) return undefined"),
|
|
72
|
+
"should return undefined (skip routing synthesis) when not in auto-mode",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("selectAndApplyModel threads isAutoMode to resolvePreferredModelConfig", () => {
|
|
77
|
+
// The call to resolvePreferredModelConfig inside selectAndApplyModel
|
|
78
|
+
// should pass isAutoMode as the third argument
|
|
79
|
+
const callSite = "resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode)";
|
|
80
|
+
assert.ok(
|
|
81
|
+
modelSelectionSrc.includes(callSite),
|
|
82
|
+
"selectAndApplyModel should pass isAutoMode to resolvePreferredModelConfig",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("guided-flow passes isAutoMode=false", () => {
|
|
87
|
+
// guided-flow.ts should explicitly pass isAutoMode as false
|
|
88
|
+
assert.ok(
|
|
89
|
+
guidedFlowSrc.includes("/* isAutoMode */ false"),
|
|
90
|
+
"guided-flow should pass isAutoMode=false to selectAndApplyModel",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("auto/phases.ts does NOT pass isAutoMode=false", () => {
|
|
95
|
+
// auto/phases.ts should use the default (true) — it's auto-mode
|
|
96
|
+
const phasesSrc = readFileSync(
|
|
97
|
+
join(__dirname, "..", "auto", "phases.ts"),
|
|
98
|
+
"utf-8",
|
|
99
|
+
);
|
|
100
|
+
assert.ok(
|
|
101
|
+
!phasesSrc.includes("isAutoMode"),
|
|
102
|
+
"auto/phases.ts should use default isAutoMode=true (not pass it explicitly)",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("model downgrade notifications always visible (#3962)", () => {
|
|
108
|
+
test("downgrade notification is not gated by verbose flag", () => {
|
|
109
|
+
// The downgrade notification block should NOT be wrapped in `if (verbose)`
|
|
110
|
+
// Find the downgrade block and verify it's not behind a verbose check
|
|
111
|
+
const downgradeBlock = "if (routingResult.wasDowngraded)";
|
|
112
|
+
const downgradeIdx = modelSelectionSrc.indexOf(downgradeBlock);
|
|
113
|
+
assert.ok(downgradeIdx > 0, "downgrade block should exist");
|
|
114
|
+
|
|
115
|
+
// Extract the code between wasDowngraded check and the next routing label assignment
|
|
116
|
+
const afterDowngrade = modelSelectionSrc.slice(
|
|
117
|
+
downgradeIdx,
|
|
118
|
+
modelSelectionSrc.indexOf("routingTierLabel =", downgradeIdx),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// The notification calls should NOT be wrapped in `if (verbose)`
|
|
122
|
+
assert.ok(
|
|
123
|
+
!afterDowngrade.includes("if (verbose)"),
|
|
124
|
+
"downgrade notifications should not be gated by verbose flag",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// But the notification calls should exist
|
|
128
|
+
assert.ok(
|
|
129
|
+
afterDowngrade.includes('ctx.ui.notify('),
|
|
130
|
+
"downgrade notifications should still fire",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("tier escalation notification is not gated by verbose flag", () => {
|
|
135
|
+
// Extract the escalation block: from "if (escalated)" to its closing
|
|
136
|
+
// and verify the notification is present but `if (verbose)` is not.
|
|
137
|
+
const escalatedIdx = modelSelectionSrc.indexOf("if (escalated)");
|
|
138
|
+
assert.ok(escalatedIdx > 0, "escalation block should exist");
|
|
139
|
+
|
|
140
|
+
// Get the block from "if (escalated)" to the next closing brace pattern
|
|
141
|
+
const block = modelSelectionSrc.slice(escalatedIdx, escalatedIdx + 400);
|
|
142
|
+
assert.ok(
|
|
143
|
+
block.includes("Tier escalation:"),
|
|
144
|
+
"escalation block should contain the notification",
|
|
145
|
+
);
|
|
146
|
+
assert.ok(
|
|
147
|
+
!block.includes("if (verbose)"),
|
|
148
|
+
"escalation block should not gate notification behind verbose flag",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("auto-mode start routing banner (#3962)", () => {
|
|
154
|
+
test("auto-start shows dynamic routing status on startup", () => {
|
|
155
|
+
assert.ok(
|
|
156
|
+
autoStartSrc.includes("Dynamic routing:"),
|
|
157
|
+
"auto-start should display routing status banner",
|
|
158
|
+
);
|
|
159
|
+
assert.ok(
|
|
160
|
+
autoStartSrc.includes("resolveDynamicRoutingConfig"),
|
|
161
|
+
"auto-start should import resolveDynamicRoutingConfig",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("banner shows different messages for enabled vs disabled routing", () => {
|
|
166
|
+
assert.ok(
|
|
167
|
+
autoStartSrc.includes("Dynamic routing: enabled"),
|
|
168
|
+
"should show message when routing is enabled",
|
|
169
|
+
);
|
|
170
|
+
assert.ok(
|
|
171
|
+
autoStartSrc.includes("Dynamic routing: disabled"),
|
|
172
|
+
"should show message when routing is disabled",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("banner shows the ceiling model", () => {
|
|
177
|
+
assert.ok(
|
|
178
|
+
autoStartSrc.includes("startModelLabel"),
|
|
179
|
+
"banner should reference the start/ceiling model",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("banner accounts for flat-rate provider suppression", () => {
|
|
184
|
+
// The banner should check isFlatRateProvider to accurately reflect
|
|
185
|
+
// whether routing will actually be active at dispatch time (#3962 codex review)
|
|
186
|
+
assert.ok(
|
|
187
|
+
autoStartSrc.includes("isFlatRateProvider"),
|
|
188
|
+
"banner should check flat-rate provider status",
|
|
189
|
+
);
|
|
190
|
+
assert.ok(
|
|
191
|
+
autoStartSrc.includes("effectivelyEnabled"),
|
|
192
|
+
"banner should compute effective routing state, not just raw config",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("banner uses effective ceiling from tier_models.heavy when configured", () => {
|
|
197
|
+
// The actual ceiling may come from tier_models.heavy, not the start model
|
|
198
|
+
assert.ok(
|
|
199
|
+
autoStartSrc.includes("tier_models?.heavy"),
|
|
200
|
+
"banner should check tier_models.heavy for the effective ceiling",
|
|
201
|
+
);
|
|
202
|
+
assert.ok(
|
|
203
|
+
autoStartSrc.includes("effectiveCeiling"),
|
|
204
|
+
"banner should compute the effective ceiling model",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
File without changes
|
|
File without changes
|