@opengsd/gsd-pi 1.2.0-dev.d6c5343c → 1.2.0-dev.e8563f58
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/mcp-server.js +2 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/orchestrator.js +28 -10
- package/dist/resources/extensions/gsd/auto-model-selection.js +11 -7
- package/dist/resources/extensions/gsd/auto.js +7 -0
- package/dist/resources/extensions/gsd/blocked-models.js +28 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +26 -6
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
- package/dist/resources/extensions/gsd/consent-question.js +16 -0
- package/dist/resources/extensions/gsd/doctor-git-checks.js +2 -18
- package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
- package/dist/resources/extensions/gsd/gsd-db.js +2 -1
- package/dist/resources/extensions/gsd/milestone-closeout.js +73 -2
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
- package/dist/resources/extensions/shared/gsd-browser-cli.js +21 -2
- package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
- package/dist/resources/shared/package-manager-detection.js +1 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/update-check.d.ts +2 -0
- package/dist/update-check.js +24 -1
- package/dist/update-cmd.js +20 -3
- 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/chunks/8357.js +2 -2
- 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/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
- package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
- package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
- package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +4 -0
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +18 -18
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +99 -38
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +5 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/index.d.ts +2 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +2 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +12 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +12 -3
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
- package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +11 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +28 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +16 -7
- package/src/resources/extensions/gsd/auto.ts +8 -0
- package/src/resources/extensions/gsd/blocked-models.ts +49 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -5
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
- package/src/resources/extensions/gsd/consent-question.ts +15 -0
- package/src/resources/extensions/gsd/doctor-git-checks.ts +2 -19
- package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
- package/src/resources/extensions/gsd/gsd-db.ts +4 -3
- package/src/resources/extensions/gsd/milestone-closeout.ts +97 -2
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +97 -0
- package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/consent-question.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +273 -38
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
- package/src/resources/extensions/shared/gsd-browser-cli.ts +23 -2
- package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
- package/src/resources/shared/package-manager-detection.ts +1 -1
- /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsd/pi-coding-agent",
|
|
3
|
-
"version": "1.2.0-dev.
|
|
3
|
+
"version": "1.2.0-dev.e8563f58",
|
|
4
4
|
"description": "Coding agent CLI (vendored from earendil-works/pi)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"gsd": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"copy-assets": "node scripts/copy-assets.cjs"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@opengsd/contracts": "^1.2.0-dev.
|
|
36
|
+
"@opengsd/contracts": "^1.2.0-dev.e8563f58",
|
|
37
37
|
"@mariozechner/jiti": "^2.6.2",
|
|
38
38
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
39
39
|
"chalk": "5.6.2",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"typebox": "1.1.38",
|
|
54
54
|
"undici": "7.26.0",
|
|
55
55
|
"yaml": "2.9.0",
|
|
56
|
-
"@gsd/agent-core": "^1.2.0-dev.
|
|
57
|
-
"@gsd/native": "^1.2.0-dev.
|
|
58
|
-
"@gsd/pi-agent-core": "^1.2.0-dev.
|
|
59
|
-
"@gsd/pi-ai": "^1.2.0-dev.
|
|
60
|
-
"@gsd/pi-tui": "^1.2.0-dev.
|
|
56
|
+
"@gsd/agent-core": "^1.2.0-dev.e8563f58",
|
|
57
|
+
"@gsd/native": "^1.2.0-dev.e8563f58",
|
|
58
|
+
"@gsd/pi-agent-core": "^1.2.0-dev.e8563f58",
|
|
59
|
+
"@gsd/pi-ai": "^1.2.0-dev.e8563f58",
|
|
60
|
+
"@gsd/pi-tui": "^1.2.0-dev.e8563f58",
|
|
61
61
|
"@sinclair/typebox": "^0.34.41"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsd/pi-tui",
|
|
3
|
-
"version": "1.2.0-dev.
|
|
3
|
+
"version": "1.2.0-dev.e8563f58",
|
|
4
4
|
"description": "Terminal UI library (vendored from earendil-works/pi)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"gsd": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build": "node ../../scripts/clean-package-dist.cjs && tsc -p tsconfig.json --incremental false"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@gsd/native": "^1.2.0-dev.
|
|
24
|
+
"@gsd/native": "^1.2.0-dev.e8563f58",
|
|
25
25
|
"get-east-asian-width": "1.6.0",
|
|
26
26
|
"marked": "15.0.12",
|
|
27
27
|
"@sinclair/typebox": "^0.34.41"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/rpc-client",
|
|
3
|
-
"version": "1.2.0-dev.
|
|
3
|
+
"version": "1.2.0-dev.e8563f58",
|
|
4
4
|
"description": "Standalone RPC client SDK for GSD — zero internal dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"gsd": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"test": "node --test dist/rpc-client.test.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@opengsd/contracts": "^1.2.0-dev.
|
|
37
|
+
"@opengsd/contracts": "^1.2.0-dev.e8563f58"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=22.0.0"
|
package/pkg/package.json
CHANGED
|
@@ -38,6 +38,17 @@ describe("resolveGsdBrowserMcpLaunchConfig identity flags", () => {
|
|
|
38
38
|
assert.equal(args[args.indexOf("--identity-key") + 1], "custom-key");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it("splits GSD_BROWSER_MCP_COMMAND command lines before spawning", () => {
|
|
42
|
+
const commandLine = '"C:\\Program Files\\nodejs\\node.exe" "C:\\Users\\Test User\\AppData\\Roaming\\npm\\node_modules\\@opengsd\\gsd-browser\\bin\\gsd-browser"';
|
|
43
|
+
const { command, args } = resolveGsdBrowserMcpLaunchConfig("C:\\Users\\Test User\\project", {
|
|
44
|
+
GSD_BROWSER_MCP_COMMAND: commandLine,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(command, "C:\\Program Files\\nodejs\\node.exe");
|
|
48
|
+
assert.equal(args[0], "C:\\Users\\Test User\\AppData\\Roaming\\npm\\node_modules\\@opengsd\\gsd-browser\\bin\\gsd-browser");
|
|
49
|
+
assert.equal(args[1], "mcp");
|
|
50
|
+
});
|
|
51
|
+
|
|
41
52
|
it("uses a path-safe identity-project identifier", () => {
|
|
42
53
|
const { args } = resolveGsdBrowserMcpLaunchConfig("/tmp/example/project", {});
|
|
43
54
|
const projectId = args[args.indexOf("--identity-project") + 1];
|
|
@@ -22,7 +22,7 @@ type BlockedAdvanceResult = Extract<AutoAdvanceResult, { kind: "blocked" }>;
|
|
|
22
22
|
import { debugCount, debugLog, debugTime } from "../debug-logger.js";
|
|
23
23
|
import { reconcileBeforeDispatch } from "../state-reconciliation.js";
|
|
24
24
|
import { isLegalEdge, IllegalPhaseTransitionError } from "../state-transition-matrix.js";
|
|
25
|
-
import { resolveDispatch } from "../auto-dispatch.js";
|
|
25
|
+
import { hasPendingDeepStage, resolveDispatch } from "../auto-dispatch.js";
|
|
26
26
|
import { classifyFailure } from "../recovery-classification.js";
|
|
27
27
|
import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
28
28
|
import { invalidateAllCaches } from "../cache.js";
|
|
@@ -193,14 +193,25 @@ export async function decideOrchestratorDispatch(
|
|
|
193
193
|
): Promise<DispatchDecision> {
|
|
194
194
|
const state = input.stateSnapshot;
|
|
195
195
|
const active = state.activeMilestone;
|
|
196
|
-
if (!active) return null;
|
|
197
|
-
|
|
198
196
|
const activeSession = input.session ?? session;
|
|
199
197
|
const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
|
|
200
|
-
|
|
198
|
+
const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
|
|
199
|
+
if (!active) {
|
|
200
|
+
if (state.phase !== "pre-planning") return null;
|
|
201
|
+
if (!hasPendingDeepStage(prefs, activeDispatchBasePath)) {
|
|
202
|
+
return {
|
|
203
|
+
kind: "blocked",
|
|
204
|
+
reason: state.nextAction || "No active milestone. Run /gsd unpark <id> or create a new milestone.",
|
|
205
|
+
action: "stop",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (active && activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
|
|
201
211
|
activeSession.currentMilestoneId = active.id;
|
|
202
212
|
}
|
|
203
|
-
const
|
|
213
|
+
const dispatchMid = active?.id ?? activeSession?.currentMilestoneId ?? "";
|
|
214
|
+
const dispatchMidTitle = active?.title ?? "";
|
|
204
215
|
|
|
205
216
|
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
206
217
|
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
@@ -232,8 +243,15 @@ export async function decideOrchestratorDispatch(
|
|
|
232
243
|
? "true"
|
|
233
244
|
: "false");
|
|
234
245
|
|
|
246
|
+
// Only replay a milestone-scoped verification retry when a milestone is
|
|
247
|
+
// active. Pre-PR (#712 fix), `!active` returned null before reaching this
|
|
248
|
+
// block, so the retry was preserved for a future tick. The new
|
|
249
|
+
// pre-planning + deep-pending fall-through must keep that contract:
|
|
250
|
+
// otherwise a stale execute-task / complete-slice / complete-milestone
|
|
251
|
+
// retry whose target milestone has since been parked would preempt
|
|
252
|
+
// project-level deep rules like `discuss-project`.
|
|
235
253
|
const pendingRetry = session?.pendingVerificationRetryDispatch;
|
|
236
|
-
if (session && pendingRetry) {
|
|
254
|
+
if (session && pendingRetry && active) {
|
|
237
255
|
session.pendingVerificationRetryDispatch = null;
|
|
238
256
|
const alreadyClosedReason = getAlreadyClosedDispatchReason(
|
|
239
257
|
pendingRetry.unitType,
|
|
@@ -255,8 +273,8 @@ export async function decideOrchestratorDispatch(
|
|
|
255
273
|
|
|
256
274
|
const action = await resolveDispatch({
|
|
257
275
|
basePath: activeDispatchBasePath,
|
|
258
|
-
mid:
|
|
259
|
-
midTitle:
|
|
276
|
+
mid: dispatchMid,
|
|
277
|
+
midTitle: dispatchMidTitle,
|
|
260
278
|
state,
|
|
261
279
|
prefs,
|
|
262
280
|
session: activeSession,
|
|
@@ -300,8 +318,8 @@ export async function decideOrchestratorDispatch(
|
|
|
300
318
|
prompt: action.prompt,
|
|
301
319
|
pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
|
|
302
320
|
state,
|
|
303
|
-
mid:
|
|
304
|
-
midTitle:
|
|
321
|
+
mid: dispatchMid,
|
|
322
|
+
midTitle: dispatchMidTitle,
|
|
305
323
|
};
|
|
306
324
|
session.pendingOrchestrationDispatch = pending;
|
|
307
325
|
}
|
|
@@ -18,7 +18,7 @@ import { getSessionModelOverride } from "./session-model-override.js";
|
|
|
18
18
|
import { logWarning } from "./workflow-logger.js";
|
|
19
19
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
20
20
|
import { applyModelPolicyFilter } from "./uok/model-policy.js";
|
|
21
|
-
import { isModelBlocked } from "./blocked-models.js";
|
|
21
|
+
import { isModelBlocked, isModelTemporarilyUnavailable } from "./blocked-models.js";
|
|
22
22
|
import { getRequiredWorkflowToolsForAutoUnit, isWorkflowMcpSurfaceTool } from "./workflow-mcp.js";
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -272,6 +272,15 @@ function buildModelPolicyBlockReasons(
|
|
|
272
272
|
}];
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
function isModelUnavailable(
|
|
276
|
+
basePath: string,
|
|
277
|
+
provider: string | undefined,
|
|
278
|
+
id: string | undefined,
|
|
279
|
+
): boolean {
|
|
280
|
+
return isModelBlocked(basePath, provider, id) ||
|
|
281
|
+
isModelTemporarilyUnavailable(basePath, provider, id);
|
|
282
|
+
}
|
|
283
|
+
|
|
275
284
|
function restoreToolBaseline(pi: ExtensionAPI): void {
|
|
276
285
|
const key = pi as unknown as object;
|
|
277
286
|
const baseline = TOOL_BASELINE.get(key);
|
|
@@ -817,9 +826,9 @@ export async function selectAndApplyModel(
|
|
|
817
826
|
// (issue #4513). The block is persisted in .gsd/runtime/blocked-models.json
|
|
818
827
|
// so it survives /gsd auto restarts — without this, the same dead model
|
|
819
828
|
// gets reselected after every restart.
|
|
820
|
-
if (
|
|
829
|
+
if (isModelUnavailable(basePath, model.provider, model.id)) {
|
|
821
830
|
ctx.ui.notify(
|
|
822
|
-
`Skipping
|
|
831
|
+
`Skipping unavailable model ${model.provider}/${model.id}.`,
|
|
823
832
|
"warning",
|
|
824
833
|
);
|
|
825
834
|
continue;
|
|
@@ -896,7 +905,7 @@ export async function selectAndApplyModel(
|
|
|
896
905
|
for (const model of buildPolicyEligibleFallbackOrder(ctx, routingEligibleModels, autoModeStartModel)) {
|
|
897
906
|
const key = `${model.provider.toLowerCase()}/${model.id.toLowerCase()}`;
|
|
898
907
|
if (!policyAllowedModelKeys.has(key)) continue;
|
|
899
|
-
if (
|
|
908
|
+
if (isModelUnavailable(basePath, model.provider, model.id)) continue;
|
|
900
909
|
const ok = await pi.setModel(model, { persist: false });
|
|
901
910
|
if (!ok) continue;
|
|
902
911
|
appliedModel = model;
|
|
@@ -926,10 +935,10 @@ export async function selectAndApplyModel(
|
|
|
926
935
|
autoModeStartModel,
|
|
927
936
|
effectiveSessionModelOverride,
|
|
928
937
|
);
|
|
929
|
-
const startBlocked =
|
|
938
|
+
const startBlocked = isModelUnavailable(basePath, autoModeStartModel.provider, autoModeStartModel.id);
|
|
930
939
|
if (startBlocked) {
|
|
931
940
|
ctx.ui.notify(
|
|
932
|
-
`Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is
|
|
941
|
+
`Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is unavailable. Using current session model instead.`,
|
|
933
942
|
"warning",
|
|
934
943
|
);
|
|
935
944
|
} else {
|
|
@@ -940,7 +949,7 @@ export async function selectAndApplyModel(
|
|
|
940
949
|
const ok = await pi.setModel(startModel, { persist: false });
|
|
941
950
|
if (!ok) {
|
|
942
951
|
const byId = availableModels.find(
|
|
943
|
-
m => m.id === autoModeStartModel.id && !
|
|
952
|
+
m => m.id === autoModeStartModel.id && !isModelUnavailable(basePath, m.provider, m.id),
|
|
944
953
|
);
|
|
945
954
|
if (byId) {
|
|
946
955
|
const fallbackOk = await pi.setModel(byId, { persist: false });
|
|
@@ -924,6 +924,14 @@ export function setCurrentDispatchedModelId(model: { provider: string; id: strin
|
|
|
924
924
|
s.currentDispatchedModelId = model ? `${model.provider}/${model.id}` : null;
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
+
/**
|
|
928
|
+
* Update the active unit model after runtime recovery switches models mid-unit.
|
|
929
|
+
* The next session restore path reads this field before dispatching again.
|
|
930
|
+
*/
|
|
931
|
+
export function setCurrentUnitModelForRecovery(model: any | null): void {
|
|
932
|
+
s.currentUnitModel = model;
|
|
933
|
+
}
|
|
934
|
+
|
|
927
935
|
// Tool tracking — delegates to auto-tool-tracking.ts
|
|
928
936
|
export function markToolStart(toolCallId: string, toolName?: string): void {
|
|
929
937
|
_markToolStart(toolCallId, s.active, toolName);
|
|
@@ -23,6 +23,15 @@ interface BlockedModelsFile {
|
|
|
23
23
|
blocked: BlockedModelEntry[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
interface TemporaryBlockedModelEntry {
|
|
27
|
+
provider: string;
|
|
28
|
+
id: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
blockedUntil: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const temporaryBlockedModels = new Map<string, TemporaryBlockedModelEntry>();
|
|
34
|
+
|
|
26
35
|
function blockedModelsPath(basePath: string): string {
|
|
27
36
|
return join(gsdRoot(basePath), "runtime", "blocked-models.json");
|
|
28
37
|
}
|
|
@@ -31,6 +40,10 @@ function modelKey(provider: string, id: string): string {
|
|
|
31
40
|
return `${provider.toLowerCase()}/${id.toLowerCase()}`;
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
function temporaryModelKey(basePath: string, provider: string, id: string): string {
|
|
44
|
+
return `${basePath}:${modelKey(provider, id)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
function readFileSafe(path: string): BlockedModelsFile {
|
|
35
48
|
if (!existsSync(path)) return { version: 1, blocked: [] };
|
|
36
49
|
try {
|
|
@@ -66,6 +79,42 @@ export function isModelBlocked(
|
|
|
66
79
|
);
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
export function blockModelUntil(
|
|
83
|
+
basePath: string,
|
|
84
|
+
provider: string,
|
|
85
|
+
id: string,
|
|
86
|
+
blockedUntil: number,
|
|
87
|
+
reason: string,
|
|
88
|
+
): void {
|
|
89
|
+
const key = temporaryModelKey(basePath, provider, id);
|
|
90
|
+
if (blockedUntil <= Date.now()) {
|
|
91
|
+
temporaryBlockedModels.delete(key);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
temporaryBlockedModels.set(key, { provider, id, reason, blockedUntil });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isModelTemporarilyUnavailable(
|
|
98
|
+
basePath: string,
|
|
99
|
+
provider: string | undefined,
|
|
100
|
+
id: string | undefined,
|
|
101
|
+
now = Date.now(),
|
|
102
|
+
): boolean {
|
|
103
|
+
if (!provider || !id) return false;
|
|
104
|
+
const key = temporaryModelKey(basePath, provider, id);
|
|
105
|
+
const entry = temporaryBlockedModels.get(key);
|
|
106
|
+
if (!entry) return false;
|
|
107
|
+
if (entry.blockedUntil <= now) {
|
|
108
|
+
temporaryBlockedModels.delete(key);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function clearTemporaryModelBlocksForTest(): void {
|
|
115
|
+
temporaryBlockedModels.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
69
118
|
export function blockModel(
|
|
70
119
|
basePath: string,
|
|
71
120
|
provider: string,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
isAutoCompletionStopInProgress,
|
|
20
20
|
pauseAuto,
|
|
21
21
|
setCurrentDispatchedModelId,
|
|
22
|
+
setCurrentUnitModelForRecovery,
|
|
22
23
|
} from "../auto.js";
|
|
23
24
|
import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
|
|
24
25
|
import { pauseAutoForProviderError } from "../provider-error-pause.js";
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
isTransient,
|
|
42
43
|
type ErrorClass,
|
|
43
44
|
} from "../error-classifier.js";
|
|
44
|
-
import { blockModel, isModelBlocked } from "../blocked-models.js";
|
|
45
|
+
import { blockModel, blockModelUntil, isModelBlocked, isModelTemporarilyUnavailable } from "../blocked-models.js";
|
|
45
46
|
import { getProjectGSDPreferencesPath } from "../preferences.js";
|
|
46
47
|
import { resolveProviderErrorGuidance } from "../provider-error-guidance.js";
|
|
47
48
|
import { formatGuidance } from "../guidance.js";
|
|
@@ -143,9 +144,14 @@ async function tryProviderModelFallback(params: ProviderModelFallbackParams): Pr
|
|
|
143
144
|
const nextModelId = getNextFallbackModel(cursorModelId, modelConfig);
|
|
144
145
|
if (!nextModelId) break;
|
|
145
146
|
const candidate = resolveModelId(nextModelId, availableModels, rejectedProvider);
|
|
146
|
-
if (
|
|
147
|
+
if (
|
|
148
|
+
candidate &&
|
|
149
|
+
!isModelBlocked(basePath, candidate.provider, candidate.id) &&
|
|
150
|
+
!isModelTemporarilyUnavailable(basePath, candidate.provider, candidate.id)
|
|
151
|
+
) {
|
|
147
152
|
const ok = await pi.setModel(candidate, { persist: false });
|
|
148
153
|
if (ok) {
|
|
154
|
+
setCurrentUnitModelForRecovery(candidate);
|
|
149
155
|
setCurrentDispatchedModelId({ provider: candidate.provider, id: candidate.id });
|
|
150
156
|
switchedNotify(`${candidate.provider}/${candidate.id}`);
|
|
151
157
|
pi.sendMessage(
|
|
@@ -163,7 +169,8 @@ async function tryProviderModelFallback(params: ProviderModelFallbackParams): Pr
|
|
|
163
169
|
if (
|
|
164
170
|
sessionModel &&
|
|
165
171
|
!(sessionModel.provider === rejectedProvider && sessionModel.id === rejectedId) &&
|
|
166
|
-
!isModelBlocked(basePath, sessionModel.provider, sessionModel.id)
|
|
172
|
+
!isModelBlocked(basePath, sessionModel.provider, sessionModel.id) &&
|
|
173
|
+
!isModelTemporarilyUnavailable(basePath, sessionModel.provider, sessionModel.id)
|
|
167
174
|
) {
|
|
168
175
|
const startModel = availableModels.find(
|
|
169
176
|
(m) => m.provider === sessionModel.provider && m.id === sessionModel.id,
|
|
@@ -171,6 +178,7 @@ async function tryProviderModelFallback(params: ProviderModelFallbackParams): Pr
|
|
|
171
178
|
if (startModel) {
|
|
172
179
|
const ok = await pi.setModel(startModel, { persist: false });
|
|
173
180
|
if (ok) {
|
|
181
|
+
setCurrentUnitModelForRecovery(startModel);
|
|
174
182
|
setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
|
|
175
183
|
switchedNotify(`${startModel.provider}/${startModel.id}`);
|
|
176
184
|
pi.sendMessage(
|
|
@@ -676,6 +684,16 @@ export async function handleAgentEnd(
|
|
|
676
684
|
if (currentProvider === "openai-codex" || currentProvider === "google-gemini-cli") {
|
|
677
685
|
cls.retryAfterMs = Math.min(cls.retryAfterMs, 30_000);
|
|
678
686
|
}
|
|
687
|
+
const dash = getAutoDashboardData();
|
|
688
|
+
if (dash.basePath && ctx.model?.provider && ctx.model?.id) {
|
|
689
|
+
blockModelUntil(
|
|
690
|
+
dash.basePath,
|
|
691
|
+
ctx.model.provider,
|
|
692
|
+
ctx.model.id,
|
|
693
|
+
Date.now() + cls.retryAfterMs,
|
|
694
|
+
rawErrorMsg || displayMsg || "rate limit",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
679
697
|
}
|
|
680
698
|
|
|
681
699
|
// ── 2. Decide & Act ──────────────────────────────────────────────────
|
|
@@ -721,9 +739,14 @@ export async function handleAgentEnd(
|
|
|
721
739
|
retryState.networkRetryCount = 0;
|
|
722
740
|
retryState.currentRetryModelId = undefined;
|
|
723
741
|
const modelToSet = resolveModelId(nextModelId, availableModels, ctx.model?.provider);
|
|
724
|
-
|
|
742
|
+
const modelUnavailable = dash.basePath && modelToSet
|
|
743
|
+
? isModelBlocked(dash.basePath, modelToSet.provider, modelToSet.id) ||
|
|
744
|
+
isModelTemporarilyUnavailable(dash.basePath, modelToSet.provider, modelToSet.id)
|
|
745
|
+
: false;
|
|
746
|
+
if (modelToSet && !modelUnavailable) {
|
|
725
747
|
const ok = await pi.setModel(modelToSet, { persist: false });
|
|
726
748
|
if (ok) {
|
|
749
|
+
setCurrentUnitModelForRecovery(modelToSet);
|
|
727
750
|
setCurrentDispatchedModelId({ provider: modelToSet.provider, id: modelToSet.id });
|
|
728
751
|
ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning");
|
|
729
752
|
pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
|
|
@@ -737,11 +760,17 @@ export async function handleAgentEnd(
|
|
|
737
760
|
// Try restoring session model
|
|
738
761
|
const sessionModel = getAutoModeStartModel();
|
|
739
762
|
if (sessionModel) {
|
|
740
|
-
|
|
763
|
+
const dash = getAutoDashboardData();
|
|
764
|
+
const sessionModelUnavailable = dash.basePath
|
|
765
|
+
? isModelBlocked(dash.basePath, sessionModel.provider, sessionModel.id) ||
|
|
766
|
+
isModelTemporarilyUnavailable(dash.basePath, sessionModel.provider, sessionModel.id)
|
|
767
|
+
: false;
|
|
768
|
+
if (!sessionModelUnavailable && (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider)) {
|
|
741
769
|
const startModel = ctx.modelRegistry.getAvailable().find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
|
|
742
770
|
if (startModel) {
|
|
743
771
|
const ok = await pi.setModel(startModel, { persist: false });
|
|
744
772
|
if (ok) {
|
|
773
|
+
setCurrentUnitModelForRecovery(startModel);
|
|
745
774
|
setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
|
|
746
775
|
retryState.networkRetryCount = 0;
|
|
747
776
|
retryState.currentRetryModelId = undefined;
|
|
@@ -137,8 +137,8 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
137
137
|
parameters: Type.Object({
|
|
138
138
|
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
139
139
|
runtime: Type.Optional(
|
|
140
|
-
Type.
|
|
141
|
-
description: "Restrict to one runtime.",
|
|
140
|
+
Type.String({
|
|
141
|
+
description: "Restrict to one runtime: bash, node, or python.",
|
|
142
142
|
}),
|
|
143
143
|
),
|
|
144
144
|
failing_only: Type.Optional(Type.Boolean({ description: "Only non-zero exit codes and timeouts." })),
|
|
@@ -1,25 +1,40 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Shared closeout detection and merge actions for /gsd home and smart entry.
|
|
3
3
|
|
|
4
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
4
7
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
5
8
|
|
|
6
9
|
import type { NextAction } from "../shared/next-action-ui.js";
|
|
7
10
|
import type { GSDState } from "./types.js";
|
|
8
11
|
import { setAutoOutcomeWidget } from "./auto-dashboard.js";
|
|
9
12
|
import { invalidateAllCaches } from "./cache.js";
|
|
13
|
+
import { isDbAvailable } from "./db/engine.js";
|
|
14
|
+
import { getMilestone } from "./db/queries.js";
|
|
15
|
+
import { MILESTONE_ID_RE } from "./milestone-ids.js";
|
|
10
16
|
import { mergeCompletedMilestone } from "./parallel-merge.js";
|
|
11
17
|
import { cleanupQuickBranch, detectStrandedQuickBranch, type StrandedQuickBranch } from "./quick.js";
|
|
18
|
+
import { isClosedStatus } from "./status-guards.js";
|
|
12
19
|
import {
|
|
13
20
|
findUnmergedCompletedMilestones,
|
|
14
21
|
type UnmergedMilestoneBlocker,
|
|
15
22
|
} from "./unmerged-milestone-guard.js";
|
|
16
23
|
import { appendRequirementsBacklogToSummary } from "./requirements-backlog.js";
|
|
24
|
+
import { nativeBranchList, nativeIsRepo } from "./native-git-bridge.js";
|
|
25
|
+
import { allWorktreesDirs } from "./worktree-manager.js";
|
|
17
26
|
|
|
18
27
|
export type CloseoutActionId = "finish_quick" | "finish_milestone";
|
|
19
28
|
|
|
29
|
+
export interface IdleMilestoneResidueHint {
|
|
30
|
+
message: string;
|
|
31
|
+
milestoneIds: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
export interface CloseoutContext {
|
|
21
35
|
strandedQuick: StrandedQuickBranch | null;
|
|
22
36
|
unmergedMilestones: UnmergedMilestoneBlocker[];
|
|
37
|
+
idleResidueHint?: IdleMilestoneResidueHint | null;
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
const MILESTONE_MERGE_CLOSEOUT_COMMANDS = [
|
|
@@ -29,11 +44,90 @@ const MILESTONE_MERGE_CLOSEOUT_COMMANDS = [
|
|
|
29
44
|
"/gsd start for new work",
|
|
30
45
|
];
|
|
31
46
|
|
|
47
|
+
function listMilestoneWorktreeIds(basePath: string): string[] {
|
|
48
|
+
const ids = new Set<string>();
|
|
49
|
+
for (const wtDir of allWorktreesDirs(basePath)) {
|
|
50
|
+
if (!existsSync(wtDir)) continue;
|
|
51
|
+
for (const entry of readdirSync(wtDir)) {
|
|
52
|
+
if (!MILESTONE_ID_RE.test(entry)) continue;
|
|
53
|
+
try {
|
|
54
|
+
if (statSync(join(wtDir, entry)).isDirectory()) ids.add(entry);
|
|
55
|
+
} catch {
|
|
56
|
+
// skip unreadable entries
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...ids].sort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function listMilestoneBranchIds(basePath: string): string[] {
|
|
64
|
+
try {
|
|
65
|
+
return nativeBranchList(basePath, "milestone/*")
|
|
66
|
+
.map((branch) => branch.replace(/^milestone\//, ""))
|
|
67
|
+
.filter((id) => MILESTONE_ID_RE.test(id))
|
|
68
|
+
.sort();
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A milestone ID is "stranded residue" only when its worktree/branch artifacts
|
|
76
|
+
* exist for a milestone the DB does not consider currently in flight — i.e. the
|
|
77
|
+
* row is closed (complete/done/skipped/closed) or absent. Active, pending,
|
|
78
|
+
* blocked, parked, queued, and deferred rows describe normal in-flight or
|
|
79
|
+
* intentionally-preserved state, never residue. Returning `false` skips the ID;
|
|
80
|
+
* returning `true` keeps it in the hint.
|
|
81
|
+
*/
|
|
82
|
+
function isStrandedMilestoneId(milestoneId: string): boolean {
|
|
83
|
+
if (!isDbAvailable()) return true;
|
|
84
|
+
const row = getMilestone(milestoneId);
|
|
85
|
+
if (!row) return true;
|
|
86
|
+
return isClosedStatus(row.status);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Surface stranded milestone git residue when closeout guards did not classify it. */
|
|
90
|
+
export function detectIdleMilestoneResidueHint(basePath: string): IdleMilestoneResidueHint | null {
|
|
91
|
+
if (!nativeIsRepo(basePath)) return null;
|
|
92
|
+
|
|
93
|
+
const gsdDir = join(basePath, ".gsd");
|
|
94
|
+
const dbPath = join(gsdDir, "gsd.db");
|
|
95
|
+
if (!existsSync(gsdDir) || !existsSync(dbPath)) {
|
|
96
|
+
return {
|
|
97
|
+
milestoneIds: [],
|
|
98
|
+
message:
|
|
99
|
+
"This git repo has no local GSD workflow database (.gsd/gsd.db). " +
|
|
100
|
+
"Workflow state may live in an external worktree, or run /gsd new-project to initialize here.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const worktreeIds = listMilestoneWorktreeIds(basePath);
|
|
105
|
+
const branchIds = listMilestoneBranchIds(basePath);
|
|
106
|
+
const candidateIds = [...new Set([...worktreeIds, ...branchIds])].sort();
|
|
107
|
+
const milestoneIds = candidateIds.filter(isStrandedMilestoneId);
|
|
108
|
+
if (milestoneIds.length === 0) return null;
|
|
109
|
+
|
|
110
|
+
const listed = milestoneIds.join(", ");
|
|
111
|
+
const recovery =
|
|
112
|
+
milestoneIds.length === 1
|
|
113
|
+
? `/gsd dispatch complete-milestone ${milestoneIds[0]}`
|
|
114
|
+
: "/gsd doctor --fix";
|
|
115
|
+
return {
|
|
116
|
+
milestoneIds,
|
|
117
|
+
message:
|
|
118
|
+
`Stranded milestone git residue detected (${listed}: worktree dir and/or milestone/* branch). ` +
|
|
119
|
+
`Run ${recovery} or /gsd status to recover closeout before starting new work.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
32
123
|
export async function loadCloseoutContext(basePath: string): Promise<CloseoutContext> {
|
|
33
124
|
const unmergedMilestones = await findUnmergedCompletedMilestones(basePath);
|
|
125
|
+
const idleResidueHint =
|
|
126
|
+
unmergedMilestones.length === 0 ? detectIdleMilestoneResidueHint(basePath) : null;
|
|
34
127
|
return {
|
|
35
128
|
strandedQuick: detectStrandedQuickBranch(basePath),
|
|
36
129
|
unmergedMilestones,
|
|
130
|
+
idleResidueHint,
|
|
37
131
|
};
|
|
38
132
|
}
|
|
39
133
|
|
|
@@ -87,6 +181,14 @@ export function buildIdleMenuSummary(state: GSDState, closeout: CloseoutContext)
|
|
|
87
181
|
];
|
|
88
182
|
}
|
|
89
183
|
|
|
184
|
+
// Surface idle residue before the completion summary so smart entry shows
|
|
185
|
+
// the same recovery text /gsd home would: a closed/unknown milestone with
|
|
186
|
+
// lingering worktree/branch artifacts must not be hidden behind the
|
|
187
|
+
// "all milestones complete" message.
|
|
188
|
+
if (closeout.idleResidueHint) {
|
|
189
|
+
return [closeout.idleResidueHint.message];
|
|
190
|
+
}
|
|
191
|
+
|
|
90
192
|
if (state.phase === "complete") {
|
|
91
193
|
const last = state.lastCompletedMilestone;
|
|
92
194
|
return appendRequirementsBacklogToSummary(state, [
|