@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
package/dist/mcp-server.js
CHANGED
|
@@ -29,6 +29,7 @@ function isPlainObject(value) {
|
|
|
29
29
|
// built via a template string so TypeScript's NodeNext resolver treats them as
|
|
30
30
|
// `any` and skips static checking.
|
|
31
31
|
const MCP_PKG = '@modelcontextprotocol/sdk';
|
|
32
|
+
import { sanitizeSchemaForMoonshot } from '@gsd/pi-ai';
|
|
32
33
|
export function mcpSdkSpecifier(subpath) {
|
|
33
34
|
return `${MCP_PKG}/${subpath}.js`;
|
|
34
35
|
}
|
|
@@ -65,7 +66,7 @@ export async function startMcpServer(options) {
|
|
|
65
66
|
tools: tools.map((t) => ({
|
|
66
67
|
name: t.name,
|
|
67
68
|
description: t.description,
|
|
68
|
-
inputSchema: t.parameters,
|
|
69
|
+
inputSchema: sanitizeSchemaForMoonshot(t.parameters),
|
|
69
70
|
})),
|
|
70
71
|
}));
|
|
71
72
|
// tools/call — execute the requested tool and return content blocks.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
47400075b94e1392
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { debugCount, debugLog, debugTime } from "../debug-logger.js";
|
|
13
13
|
import { reconcileBeforeDispatch } from "../state-reconciliation.js";
|
|
14
14
|
import { isLegalEdge, IllegalPhaseTransitionError } from "../state-transition-matrix.js";
|
|
15
|
-
import { resolveDispatch } from "../auto-dispatch.js";
|
|
15
|
+
import { hasPendingDeepStage, resolveDispatch } from "../auto-dispatch.js";
|
|
16
16
|
import { classifyFailure } from "../recovery-classification.js";
|
|
17
17
|
import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
18
18
|
import { invalidateAllCaches } from "../cache.js";
|
|
@@ -111,14 +111,25 @@ function shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath
|
|
|
111
111
|
export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, session, input) {
|
|
112
112
|
const state = input.stateSnapshot;
|
|
113
113
|
const active = state.activeMilestone;
|
|
114
|
-
if (!active)
|
|
115
|
-
return null;
|
|
116
114
|
const activeSession = input.session ?? session;
|
|
117
115
|
const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
|
|
118
|
-
|
|
116
|
+
const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
|
|
117
|
+
if (!active) {
|
|
118
|
+
if (state.phase !== "pre-planning")
|
|
119
|
+
return null;
|
|
120
|
+
if (!hasPendingDeepStage(prefs, activeDispatchBasePath)) {
|
|
121
|
+
return {
|
|
122
|
+
kind: "blocked",
|
|
123
|
+
reason: state.nextAction || "No active milestone. Run /gsd unpark <id> or create a new milestone.",
|
|
124
|
+
action: "stop",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (active && activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
|
|
119
129
|
activeSession.currentMilestoneId = active.id;
|
|
120
130
|
}
|
|
121
|
-
const
|
|
131
|
+
const dispatchMid = active?.id ?? activeSession?.currentMilestoneId ?? "";
|
|
132
|
+
const dispatchMidTitle = active?.title ?? "";
|
|
122
133
|
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
123
134
|
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
124
135
|
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
@@ -146,8 +157,15 @@ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, sess
|
|
|
146
157
|
})
|
|
147
158
|
? "true"
|
|
148
159
|
: "false");
|
|
160
|
+
// Only replay a milestone-scoped verification retry when a milestone is
|
|
161
|
+
// active. Pre-PR (#712 fix), `!active` returned null before reaching this
|
|
162
|
+
// block, so the retry was preserved for a future tick. The new
|
|
163
|
+
// pre-planning + deep-pending fall-through must keep that contract:
|
|
164
|
+
// otherwise a stale execute-task / complete-slice / complete-milestone
|
|
165
|
+
// retry whose target milestone has since been parked would preempt
|
|
166
|
+
// project-level deep rules like `discuss-project`.
|
|
149
167
|
const pendingRetry = session?.pendingVerificationRetryDispatch;
|
|
150
|
-
if (session && pendingRetry) {
|
|
168
|
+
if (session && pendingRetry && active) {
|
|
151
169
|
session.pendingVerificationRetryDispatch = null;
|
|
152
170
|
const alreadyClosedReason = getAlreadyClosedDispatchReason(pendingRetry.unitType, pendingRetry.unitId);
|
|
153
171
|
if (alreadyClosedReason) {
|
|
@@ -165,8 +183,8 @@ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, sess
|
|
|
165
183
|
}
|
|
166
184
|
const action = await resolveDispatch({
|
|
167
185
|
basePath: activeDispatchBasePath,
|
|
168
|
-
mid:
|
|
169
|
-
midTitle:
|
|
186
|
+
mid: dispatchMid,
|
|
187
|
+
midTitle: dispatchMidTitle,
|
|
170
188
|
state,
|
|
171
189
|
prefs,
|
|
172
190
|
session: activeSession,
|
|
@@ -211,8 +229,8 @@ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, sess
|
|
|
211
229
|
prompt: action.prompt,
|
|
212
230
|
pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
|
|
213
231
|
state,
|
|
214
|
-
mid:
|
|
215
|
-
midTitle:
|
|
232
|
+
mid: dispatchMid,
|
|
233
|
+
midTitle: dispatchMidTitle,
|
|
216
234
|
};
|
|
217
235
|
session.pendingOrchestrationDispatch = pending;
|
|
218
236
|
}
|
|
@@ -13,7 +13,7 @@ import { getSessionModelOverride } from "./session-model-override.js";
|
|
|
13
13
|
import { logWarning } from "./workflow-logger.js";
|
|
14
14
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
15
15
|
import { applyModelPolicyFilter } from "./uok/model-policy.js";
|
|
16
|
-
import { isModelBlocked } from "./blocked-models.js";
|
|
16
|
+
import { isModelBlocked, isModelTemporarilyUnavailable } from "./blocked-models.js";
|
|
17
17
|
import { getRequiredWorkflowToolsForAutoUnit, isWorkflowMcpSurfaceTool } from "./workflow-mcp.js";
|
|
18
18
|
/**
|
|
19
19
|
* Thrown when the model-policy gate rejects every candidate model for a unit
|
|
@@ -218,6 +218,10 @@ function buildModelPolicyBlockReasons(policyDenyReasons, availableModels, routin
|
|
|
218
218
|
reason: `configured model(s) did not resolve against policy-eligible registry [${eligibleSummary}]`,
|
|
219
219
|
}];
|
|
220
220
|
}
|
|
221
|
+
function isModelUnavailable(basePath, provider, id) {
|
|
222
|
+
return isModelBlocked(basePath, provider, id) ||
|
|
223
|
+
isModelTemporarilyUnavailable(basePath, provider, id);
|
|
224
|
+
}
|
|
221
225
|
function restoreToolBaseline(pi) {
|
|
222
226
|
const key = pi;
|
|
223
227
|
const baseline = TOOL_BASELINE.get(key);
|
|
@@ -657,8 +661,8 @@ autoModeStartThinkingLevel) {
|
|
|
657
661
|
// (issue #4513). The block is persisted in .gsd/runtime/blocked-models.json
|
|
658
662
|
// so it survives /gsd auto restarts — without this, the same dead model
|
|
659
663
|
// gets reselected after every restart.
|
|
660
|
-
if (
|
|
661
|
-
ctx.ui.notify(`Skipping
|
|
664
|
+
if (isModelUnavailable(basePath, model.provider, model.id)) {
|
|
665
|
+
ctx.ui.notify(`Skipping unavailable model ${model.provider}/${model.id}.`, "warning");
|
|
662
666
|
continue;
|
|
663
667
|
}
|
|
664
668
|
// Warn if the ID is ambiguous across providers
|
|
@@ -724,7 +728,7 @@ autoModeStartThinkingLevel) {
|
|
|
724
728
|
const key = `${model.provider.toLowerCase()}/${model.id.toLowerCase()}`;
|
|
725
729
|
if (!policyAllowedModelKeys.has(key))
|
|
726
730
|
continue;
|
|
727
|
-
if (
|
|
731
|
+
if (isModelUnavailable(basePath, model.provider, model.id))
|
|
728
732
|
continue;
|
|
729
733
|
const ok = await pi.setModel(model, { persist: false });
|
|
730
734
|
if (!ok)
|
|
@@ -746,16 +750,16 @@ autoModeStartThinkingLevel) {
|
|
|
746
750
|
// No model preference for this unit type — re-apply the model captured
|
|
747
751
|
// at auto-mode start to prevent bleed from shared global settings.json (#650).
|
|
748
752
|
const availableModels = buildModelPolicyCandidates(ctx, autoModeStartModel, effectiveSessionModelOverride);
|
|
749
|
-
const startBlocked =
|
|
753
|
+
const startBlocked = isModelUnavailable(basePath, autoModeStartModel.provider, autoModeStartModel.id);
|
|
750
754
|
if (startBlocked) {
|
|
751
|
-
ctx.ui.notify(`Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is
|
|
755
|
+
ctx.ui.notify(`Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is unavailable. Using current session model instead.`, "warning");
|
|
752
756
|
}
|
|
753
757
|
else {
|
|
754
758
|
const startModel = availableModels.find(m => m.provider === autoModeStartModel.provider && m.id === autoModeStartModel.id);
|
|
755
759
|
if (startModel) {
|
|
756
760
|
const ok = await pi.setModel(startModel, { persist: false });
|
|
757
761
|
if (!ok) {
|
|
758
|
-
const byId = availableModels.find(m => m.id === autoModeStartModel.id && !
|
|
762
|
+
const byId = availableModels.find(m => m.id === autoModeStartModel.id && !isModelUnavailable(basePath, m.provider, m.id));
|
|
759
763
|
if (byId) {
|
|
760
764
|
const fallbackOk = await pi.setModel(byId, { persist: false });
|
|
761
765
|
if (fallbackOk) {
|
|
@@ -557,6 +557,13 @@ export function getAutoModeStartModel() {
|
|
|
557
557
|
export function setCurrentDispatchedModelId(model) {
|
|
558
558
|
s.currentDispatchedModelId = model ? `${model.provider}/${model.id}` : null;
|
|
559
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* Update the active unit model after runtime recovery switches models mid-unit.
|
|
562
|
+
* The next session restore path reads this field before dispatching again.
|
|
563
|
+
*/
|
|
564
|
+
export function setCurrentUnitModelForRecovery(model) {
|
|
565
|
+
s.currentUnitModel = model;
|
|
566
|
+
}
|
|
560
567
|
// Tool tracking — delegates to auto-tool-tracking.ts
|
|
561
568
|
export function markToolStart(toolCallId, toolName) {
|
|
562
569
|
_markToolStart(toolCallId, s.active, toolName);
|
|
@@ -9,12 +9,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { gsdRoot } from "./paths.js";
|
|
11
11
|
import { withFileLockSync } from "./file-lock.js";
|
|
12
|
+
const temporaryBlockedModels = new Map();
|
|
12
13
|
function blockedModelsPath(basePath) {
|
|
13
14
|
return join(gsdRoot(basePath), "runtime", "blocked-models.json");
|
|
14
15
|
}
|
|
15
16
|
function modelKey(provider, id) {
|
|
16
17
|
return `${provider.toLowerCase()}/${id.toLowerCase()}`;
|
|
17
18
|
}
|
|
19
|
+
function temporaryModelKey(basePath, provider, id) {
|
|
20
|
+
return `${basePath}:${modelKey(provider, id)}`;
|
|
21
|
+
}
|
|
18
22
|
function readFileSafe(path) {
|
|
19
23
|
if (!existsSync(path))
|
|
20
24
|
return { version: 1, blocked: [] };
|
|
@@ -41,6 +45,30 @@ export function isModelBlocked(basePath, provider, id) {
|
|
|
41
45
|
const target = modelKey(provider, id);
|
|
42
46
|
return loadBlockedModels(basePath).some((e) => modelKey(e.provider, e.id) === target);
|
|
43
47
|
}
|
|
48
|
+
export function blockModelUntil(basePath, provider, id, blockedUntil, reason) {
|
|
49
|
+
const key = temporaryModelKey(basePath, provider, id);
|
|
50
|
+
if (blockedUntil <= Date.now()) {
|
|
51
|
+
temporaryBlockedModels.delete(key);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
temporaryBlockedModels.set(key, { provider, id, reason, blockedUntil });
|
|
55
|
+
}
|
|
56
|
+
export function isModelTemporarilyUnavailable(basePath, provider, id, now = Date.now()) {
|
|
57
|
+
if (!provider || !id)
|
|
58
|
+
return false;
|
|
59
|
+
const key = temporaryModelKey(basePath, provider, id);
|
|
60
|
+
const entry = temporaryBlockedModels.get(key);
|
|
61
|
+
if (!entry)
|
|
62
|
+
return false;
|
|
63
|
+
if (entry.blockedUntil <= now) {
|
|
64
|
+
temporaryBlockedModels.delete(key);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
export function clearTemporaryModelBlocksForTest() {
|
|
70
|
+
temporaryBlockedModels.clear();
|
|
71
|
+
}
|
|
44
72
|
export function blockModel(basePath, provider, id, reason) {
|
|
45
73
|
const path = blockedModelsPath(basePath);
|
|
46
74
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { logWarning } from "../workflow-logger.js";
|
|
3
3
|
import { checkDeepProjectSetupAfterTurn, checkAutoStartAfterDiscuss, maybeHandleReadyPhraseWithoutFiles, maybeHandleEmptyIntentTurn, resetEmptyTurnCounter, } from "../guided-flow.js";
|
|
4
4
|
import { clearPathCache } from "../paths.js";
|
|
5
|
-
import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, isAutoCompletionStopInProgress, pauseAuto, setCurrentDispatchedModelId, } from "../auto.js";
|
|
5
|
+
import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, isAutoCompletionStopInProgress, pauseAuto, setCurrentDispatchedModelId, setCurrentUnitModelForRecovery, } from "../auto.js";
|
|
6
6
|
import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
|
|
7
7
|
import { pauseAutoForProviderError } from "../provider-error-pause.js";
|
|
8
8
|
import { isSessionSwitchAbortGraceActive, isSessionSwitchInFlight, resolveAgentEnd, resolveAgentEndCancelled, } from "../auto/resolve.js";
|
|
@@ -13,7 +13,7 @@ import { clearDiscussionFlowState } from "./write-gate.js";
|
|
|
13
13
|
import { clearGuidedUnitContext } from "../guided-unit-context.js";
|
|
14
14
|
import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
|
|
15
15
|
import { classifyError, createRetryState, resetRetryState, isTransient, } from "../error-classifier.js";
|
|
16
|
-
import { blockModel, isModelBlocked } from "../blocked-models.js";
|
|
16
|
+
import { blockModel, blockModelUntil, isModelBlocked, isModelTemporarilyUnavailable } from "../blocked-models.js";
|
|
17
17
|
import { getProjectGSDPreferencesPath } from "../preferences.js";
|
|
18
18
|
import { resolveProviderErrorGuidance } from "../provider-error-guidance.js";
|
|
19
19
|
import { formatGuidance } from "../guidance.js";
|
|
@@ -96,9 +96,12 @@ async function tryProviderModelFallback(params) {
|
|
|
96
96
|
if (!nextModelId)
|
|
97
97
|
break;
|
|
98
98
|
const candidate = resolveModelId(nextModelId, availableModels, rejectedProvider);
|
|
99
|
-
if (candidate &&
|
|
99
|
+
if (candidate &&
|
|
100
|
+
!isModelBlocked(basePath, candidate.provider, candidate.id) &&
|
|
101
|
+
!isModelTemporarilyUnavailable(basePath, candidate.provider, candidate.id)) {
|
|
100
102
|
const ok = await pi.setModel(candidate, { persist: false });
|
|
101
103
|
if (ok) {
|
|
104
|
+
setCurrentUnitModelForRecovery(candidate);
|
|
102
105
|
setCurrentDispatchedModelId({ provider: candidate.provider, id: candidate.id });
|
|
103
106
|
switchedNotify(`${candidate.provider}/${candidate.id}`);
|
|
104
107
|
pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
|
|
@@ -111,11 +114,13 @@ async function tryProviderModelFallback(params) {
|
|
|
111
114
|
const sessionModel = getAutoModeStartModel();
|
|
112
115
|
if (sessionModel &&
|
|
113
116
|
!(sessionModel.provider === rejectedProvider && sessionModel.id === rejectedId) &&
|
|
114
|
-
!isModelBlocked(basePath, sessionModel.provider, sessionModel.id)
|
|
117
|
+
!isModelBlocked(basePath, sessionModel.provider, sessionModel.id) &&
|
|
118
|
+
!isModelTemporarilyUnavailable(basePath, sessionModel.provider, sessionModel.id)) {
|
|
115
119
|
const startModel = availableModels.find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
|
|
116
120
|
if (startModel) {
|
|
117
121
|
const ok = await pi.setModel(startModel, { persist: false });
|
|
118
122
|
if (ok) {
|
|
123
|
+
setCurrentUnitModelForRecovery(startModel);
|
|
119
124
|
setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
|
|
120
125
|
switchedNotify(`${startModel.provider}/${startModel.id}`);
|
|
121
126
|
pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
|
|
@@ -533,6 +538,10 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
533
538
|
if (currentProvider === "openai-codex" || currentProvider === "google-gemini-cli") {
|
|
534
539
|
cls.retryAfterMs = Math.min(cls.retryAfterMs, 30_000);
|
|
535
540
|
}
|
|
541
|
+
const dash = getAutoDashboardData();
|
|
542
|
+
if (dash.basePath && ctx.model?.provider && ctx.model?.id) {
|
|
543
|
+
blockModelUntil(dash.basePath, ctx.model.provider, ctx.model.id, Date.now() + cls.retryAfterMs, rawErrorMsg || displayMsg || "rate limit");
|
|
544
|
+
}
|
|
536
545
|
}
|
|
537
546
|
// ── 2. Decide & Act ──────────────────────────────────────────────────
|
|
538
547
|
// --- Network errors: same-model retry with backoff ---
|
|
@@ -572,9 +581,14 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
572
581
|
retryState.networkRetryCount = 0;
|
|
573
582
|
retryState.currentRetryModelId = undefined;
|
|
574
583
|
const modelToSet = resolveModelId(nextModelId, availableModels, ctx.model?.provider);
|
|
575
|
-
|
|
584
|
+
const modelUnavailable = dash.basePath && modelToSet
|
|
585
|
+
? isModelBlocked(dash.basePath, modelToSet.provider, modelToSet.id) ||
|
|
586
|
+
isModelTemporarilyUnavailable(dash.basePath, modelToSet.provider, modelToSet.id)
|
|
587
|
+
: false;
|
|
588
|
+
if (modelToSet && !modelUnavailable) {
|
|
576
589
|
const ok = await pi.setModel(modelToSet, { persist: false });
|
|
577
590
|
if (ok) {
|
|
591
|
+
setCurrentUnitModelForRecovery(modelToSet);
|
|
578
592
|
setCurrentDispatchedModelId({ provider: modelToSet.provider, id: modelToSet.id });
|
|
579
593
|
ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning");
|
|
580
594
|
pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
|
|
@@ -587,11 +601,17 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
587
601
|
// Try restoring session model
|
|
588
602
|
const sessionModel = getAutoModeStartModel();
|
|
589
603
|
if (sessionModel) {
|
|
590
|
-
|
|
604
|
+
const dash = getAutoDashboardData();
|
|
605
|
+
const sessionModelUnavailable = dash.basePath
|
|
606
|
+
? isModelBlocked(dash.basePath, sessionModel.provider, sessionModel.id) ||
|
|
607
|
+
isModelTemporarilyUnavailable(dash.basePath, sessionModel.provider, sessionModel.id)
|
|
608
|
+
: false;
|
|
609
|
+
if (!sessionModelUnavailable && (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider)) {
|
|
591
610
|
const startModel = ctx.modelRegistry.getAvailable().find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
|
|
592
611
|
if (startModel) {
|
|
593
612
|
const ok = await pi.setModel(startModel, { persist: false });
|
|
594
613
|
if (ok) {
|
|
614
|
+
setCurrentUnitModelForRecovery(startModel);
|
|
595
615
|
setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
|
|
596
616
|
retryState.networkRetryCount = 0;
|
|
597
617
|
retryState.currentRetryModelId = undefined;
|
|
@@ -114,8 +114,8 @@ export function registerExecTools(pi) {
|
|
|
114
114
|
],
|
|
115
115
|
parameters: Type.Object({
|
|
116
116
|
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
117
|
-
runtime: Type.Optional(Type.
|
|
118
|
-
description: "Restrict to one runtime.",
|
|
117
|
+
runtime: Type.Optional(Type.String({
|
|
118
|
+
description: "Restrict to one runtime: bash, node, or python.",
|
|
119
119
|
})),
|
|
120
120
|
failing_only: Type.Optional(Type.Boolean({ description: "Only non-zero exit codes and timeouts." })),
|
|
121
121
|
limit: Type.Optional(Type.Number({ description: "Max results (default 20, cap 200)", minimum: 1, maximum: 200 })),
|
|
@@ -1,22 +1,107 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Shared closeout detection and merge actions for /gsd home and smart entry.
|
|
3
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { setAutoOutcomeWidget } from "./auto-dashboard.js";
|
|
4
6
|
import { invalidateAllCaches } from "./cache.js";
|
|
7
|
+
import { isDbAvailable } from "./db/engine.js";
|
|
8
|
+
import { getMilestone } from "./db/queries.js";
|
|
9
|
+
import { MILESTONE_ID_RE } from "./milestone-ids.js";
|
|
5
10
|
import { mergeCompletedMilestone } from "./parallel-merge.js";
|
|
6
11
|
import { cleanupQuickBranch, detectStrandedQuickBranch } from "./quick.js";
|
|
12
|
+
import { isClosedStatus } from "./status-guards.js";
|
|
7
13
|
import { findUnmergedCompletedMilestones, } from "./unmerged-milestone-guard.js";
|
|
8
14
|
import { appendRequirementsBacklogToSummary } from "./requirements-backlog.js";
|
|
15
|
+
import { nativeBranchList, nativeIsRepo } from "./native-git-bridge.js";
|
|
16
|
+
import { allWorktreesDirs } from "./worktree-manager.js";
|
|
9
17
|
const MILESTONE_MERGE_CLOSEOUT_COMMANDS = [
|
|
10
18
|
"/gsd status for overview",
|
|
11
19
|
"/gsd visualize to inspect",
|
|
12
20
|
"/gsd notifications for history",
|
|
13
21
|
"/gsd start for new work",
|
|
14
22
|
];
|
|
23
|
+
function listMilestoneWorktreeIds(basePath) {
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
for (const wtDir of allWorktreesDirs(basePath)) {
|
|
26
|
+
if (!existsSync(wtDir))
|
|
27
|
+
continue;
|
|
28
|
+
for (const entry of readdirSync(wtDir)) {
|
|
29
|
+
if (!MILESTONE_ID_RE.test(entry))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
if (statSync(join(wtDir, entry)).isDirectory())
|
|
33
|
+
ids.add(entry);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// skip unreadable entries
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...ids].sort();
|
|
41
|
+
}
|
|
42
|
+
function listMilestoneBranchIds(basePath) {
|
|
43
|
+
try {
|
|
44
|
+
return nativeBranchList(basePath, "milestone/*")
|
|
45
|
+
.map((branch) => branch.replace(/^milestone\//, ""))
|
|
46
|
+
.filter((id) => MILESTONE_ID_RE.test(id))
|
|
47
|
+
.sort();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A milestone ID is "stranded residue" only when its worktree/branch artifacts
|
|
55
|
+
* exist for a milestone the DB does not consider currently in flight — i.e. the
|
|
56
|
+
* row is closed (complete/done/skipped/closed) or absent. Active, pending,
|
|
57
|
+
* blocked, parked, queued, and deferred rows describe normal in-flight or
|
|
58
|
+
* intentionally-preserved state, never residue. Returning `false` skips the ID;
|
|
59
|
+
* returning `true` keeps it in the hint.
|
|
60
|
+
*/
|
|
61
|
+
function isStrandedMilestoneId(milestoneId) {
|
|
62
|
+
if (!isDbAvailable())
|
|
63
|
+
return true;
|
|
64
|
+
const row = getMilestone(milestoneId);
|
|
65
|
+
if (!row)
|
|
66
|
+
return true;
|
|
67
|
+
return isClosedStatus(row.status);
|
|
68
|
+
}
|
|
69
|
+
/** Surface stranded milestone git residue when closeout guards did not classify it. */
|
|
70
|
+
export function detectIdleMilestoneResidueHint(basePath) {
|
|
71
|
+
if (!nativeIsRepo(basePath))
|
|
72
|
+
return null;
|
|
73
|
+
const gsdDir = join(basePath, ".gsd");
|
|
74
|
+
const dbPath = join(gsdDir, "gsd.db");
|
|
75
|
+
if (!existsSync(gsdDir) || !existsSync(dbPath)) {
|
|
76
|
+
return {
|
|
77
|
+
milestoneIds: [],
|
|
78
|
+
message: "This git repo has no local GSD workflow database (.gsd/gsd.db). " +
|
|
79
|
+
"Workflow state may live in an external worktree, or run /gsd new-project to initialize here.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const worktreeIds = listMilestoneWorktreeIds(basePath);
|
|
83
|
+
const branchIds = listMilestoneBranchIds(basePath);
|
|
84
|
+
const candidateIds = [...new Set([...worktreeIds, ...branchIds])].sort();
|
|
85
|
+
const milestoneIds = candidateIds.filter(isStrandedMilestoneId);
|
|
86
|
+
if (milestoneIds.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
const listed = milestoneIds.join(", ");
|
|
89
|
+
const recovery = milestoneIds.length === 1
|
|
90
|
+
? `/gsd dispatch complete-milestone ${milestoneIds[0]}`
|
|
91
|
+
: "/gsd doctor --fix";
|
|
92
|
+
return {
|
|
93
|
+
milestoneIds,
|
|
94
|
+
message: `Stranded milestone git residue detected (${listed}: worktree dir and/or milestone/* branch). ` +
|
|
95
|
+
`Run ${recovery} or /gsd status to recover closeout before starting new work.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
15
98
|
export async function loadCloseoutContext(basePath) {
|
|
16
99
|
const unmergedMilestones = await findUnmergedCompletedMilestones(basePath);
|
|
100
|
+
const idleResidueHint = unmergedMilestones.length === 0 ? detectIdleMilestoneResidueHint(basePath) : null;
|
|
17
101
|
return {
|
|
18
102
|
strandedQuick: detectStrandedQuickBranch(basePath),
|
|
19
103
|
unmergedMilestones,
|
|
104
|
+
idleResidueHint,
|
|
20
105
|
};
|
|
21
106
|
}
|
|
22
107
|
export function getPrimaryCloseoutRecommendation(closeout) {
|
|
@@ -62,6 +147,13 @@ export function buildIdleMenuSummary(state, closeout) {
|
|
|
62
147
|
`${blocker.milestoneId} is complete but not merged into ${blocker.integrationBranch}.`,
|
|
63
148
|
];
|
|
64
149
|
}
|
|
150
|
+
// Surface idle residue before the completion summary so smart entry shows
|
|
151
|
+
// the same recovery text /gsd home would: a closed/unknown milestone with
|
|
152
|
+
// lingering worktree/branch artifacts must not be hidden behind the
|
|
153
|
+
// "all milestones complete" message.
|
|
154
|
+
if (closeout.idleResidueHint) {
|
|
155
|
+
return [closeout.idleResidueHint.message];
|
|
156
|
+
}
|
|
65
157
|
if (state.phase === "complete") {
|
|
66
158
|
const last = state.lastCompletedMilestone;
|
|
67
159
|
return appendRequirementsBacklogToSummary(state, [
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
-
import { join, resolve as resolvePath, sep } from "node:path";
|
|
10
|
+
import { join, resolve as resolvePath, sep, win32 as pathWin32 } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { deriveState } from "./state.js";
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
@@ -20,6 +20,7 @@ import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
|
20
20
|
import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
|
|
21
21
|
import { loadPrompt } from "./prompt-loader.js";
|
|
22
22
|
import { buildClaudeRuntimeFloorAdvisory } from "../../shared/claude-runtime-floor.js";
|
|
23
|
+
import { reconcileGsdBrowserPathAfterInstall } from "../../shared/gsd-browser-path-sync.js";
|
|
23
24
|
import { isPnpmInstall } from "../../shared/package-manager-detection.js";
|
|
24
25
|
import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
|
|
25
26
|
import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
|
|
@@ -51,8 +52,31 @@ function resolveInstallCommand(pkg) {
|
|
|
51
52
|
return `bun add -g ${pkg}`;
|
|
52
53
|
if (isPnpmInstall())
|
|
53
54
|
return `pnpm add -g ${pkg}`;
|
|
55
|
+
const npmPrefix = resolveWindowsNpmGlobalPrefix();
|
|
56
|
+
if (npmPrefix)
|
|
57
|
+
return `npm --prefix ${quoteWindowsArg(npmPrefix)} install -g ${pkg}`;
|
|
54
58
|
return `npm install -g ${pkg}`;
|
|
55
59
|
}
|
|
60
|
+
function resolveWindowsNpmGlobalPrefix(argv1 = process.argv[1], platform = process.platform) {
|
|
61
|
+
if (platform !== "win32" || !argv1)
|
|
62
|
+
return null;
|
|
63
|
+
const normalized = pathWin32.normalize(argv1);
|
|
64
|
+
const marker = `${pathWin32.sep}node_modules${pathWin32.sep}`;
|
|
65
|
+
const index = normalized.toLowerCase().lastIndexOf(marker);
|
|
66
|
+
if (index <= 0)
|
|
67
|
+
return null;
|
|
68
|
+
const prefix = normalized.slice(0, index);
|
|
69
|
+
// Verify this is a real npm global prefix: such a directory always contains
|
|
70
|
+
// npm's own bin shim (`npm.cmd`) as a sibling of `node_modules/`. Local
|
|
71
|
+
// project `node_modules/`, npx caches, and other non-global layouts do not,
|
|
72
|
+
// so without this check `--prefix` would target the wrong directory.
|
|
73
|
+
if (!existsSync(pathWin32.join(prefix, "npm.cmd")))
|
|
74
|
+
return null;
|
|
75
|
+
return prefix;
|
|
76
|
+
}
|
|
77
|
+
function quoteWindowsArg(value) {
|
|
78
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
79
|
+
}
|
|
56
80
|
function notifyClaudeRuntimeFloorAdvisory(ctx) {
|
|
57
81
|
let advisory = null;
|
|
58
82
|
try {
|
|
@@ -484,11 +508,30 @@ export async function handleUpdate(ctx, args = "") {
|
|
|
484
508
|
execSync(installCmd, {
|
|
485
509
|
stdio: ["ignore", "pipe", "ignore"],
|
|
486
510
|
});
|
|
511
|
+
let reconcile = null;
|
|
512
|
+
if (browserUpdate) {
|
|
513
|
+
try {
|
|
514
|
+
reconcile = reconcileGsdBrowserPathAfterInstall({
|
|
515
|
+
latestVersion: latest,
|
|
516
|
+
compareSemver: compareSemverLocal,
|
|
517
|
+
resolvePathVersion: resolveGsdBrowserPathVersionForCommand,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// Reconciliation is best-effort: the install above already succeeded,
|
|
522
|
+
// so a reconcile failure must not flip the result to "Update failed".
|
|
523
|
+
reconcile = null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
487
526
|
const newPathVersion = browserUpdate ? resolveGsdBrowserPathVersionForCommand() : null;
|
|
488
|
-
const
|
|
527
|
+
const pathNote = browserUpdate && !(newPathVersion && compareSemverLocal(newPathVersion, latest) >= 0)
|
|
528
|
+
? (reconcile?.message
|
|
529
|
+
?? "Ensure the npm global bin directory is on your PATH so MCP automation uses the updated binary.")
|
|
530
|
+
: "";
|
|
489
531
|
ctx.ui.notify(browserUpdate
|
|
490
532
|
? `Updated gsd-browser to v${latest}. Restart your GSD session to use the new browser automation version.` +
|
|
491
|
-
(
|
|
533
|
+
(reconcile?.action === "synced" && reconcile.message ? `\n${reconcile.message}` : "") +
|
|
534
|
+
(pathNote ? `\nNote: ${pathNote}` : "")
|
|
492
535
|
: `Updated to v${latest}. Restart your GSD session to use the new version.`, "info");
|
|
493
536
|
if (!browserUpdate)
|
|
494
537
|
notifyClaudeRuntimeFloorAdvisory(ctx);
|
|
@@ -62,6 +62,20 @@ export function hasApprovalQuestion(text) {
|
|
|
62
62
|
export function hasResearchDecisionQuestion(text) {
|
|
63
63
|
return hasQuestionMatching(text, [RESEARCH_DECISION_QUESTION_RE]);
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Detect a plain-text "Next steps:" menu — numbered options with an "Other"
|
|
67
|
+
* choice — emitted as prose instead of a structured ask_user_questions call.
|
|
68
|
+
* Without this, auto-mode treats the menu as informational and loops on its
|
|
69
|
+
* own turn until tokens are exhausted (#454).
|
|
70
|
+
*/
|
|
71
|
+
export function hasPlainTextNextStepsMenu(lines) {
|
|
72
|
+
const nextStepsIndex = lines.findIndex((line) => /^next steps\s*:?$/i.test(line));
|
|
73
|
+
if (nextStepsIndex < 0)
|
|
74
|
+
return false;
|
|
75
|
+
const menuLines = lines.slice(nextStepsIndex + 1);
|
|
76
|
+
const numberedOptions = menuLines.filter((line) => /^\d+[.)]\s+\S/.test(line));
|
|
77
|
+
return numberedOptions.length >= 2 && numberedOptions.some((line) => /\bother\b/i.test(line));
|
|
78
|
+
}
|
|
65
79
|
// ── Message text extraction (moved from user-input-boundary) ────────────────
|
|
66
80
|
function extractMessageText(msg, includeThinking) {
|
|
67
81
|
if (!msg || typeof msg !== "object")
|
|
@@ -273,6 +287,8 @@ export function isAwaitingUserInput(messages) {
|
|
|
273
287
|
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
274
288
|
if (lines.some((line) => line.endsWith("?")))
|
|
275
289
|
return true;
|
|
290
|
+
if (hasPlainTextNextStepsMenu(lines))
|
|
291
|
+
return true;
|
|
276
292
|
return hasApprovalQuestion(text);
|
|
277
293
|
}
|
|
278
294
|
export function isAwaitingApprovalBoundary(messages) {
|
|
@@ -3,10 +3,9 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { loadFile } from "./files.js";
|
|
6
|
-
import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
|
|
7
|
-
import { isDbAvailable, getMilestone } from "./gsd-db.js";
|
|
8
6
|
import { resolveMilestoneFile } from "./paths.js";
|
|
9
|
-
import {
|
|
7
|
+
import { isCompletedMilestoneTerminal } from "./milestone-closeout.js";
|
|
8
|
+
import { deriveState } from "./state.js";
|
|
10
9
|
import { allWorktreesDirs, createWorktree, listWorktrees, resolveGitDir } from "./worktree-manager.js";
|
|
11
10
|
import { abortAndReset } from "./git-self-heal.js";
|
|
12
11
|
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
|
|
@@ -141,21 +140,6 @@ function getSnapshotDiffCheckFailure(basePath) {
|
|
|
141
140
|
}
|
|
142
141
|
return failures.length > 0 ? failures.join("\n") : null;
|
|
143
142
|
}
|
|
144
|
-
async function isCompletedMilestoneTerminal(basePath, milestoneId) {
|
|
145
|
-
const summaryPath = resolveMilestoneFile(basePath, milestoneId, "SUMMARY");
|
|
146
|
-
if (!summaryPath)
|
|
147
|
-
return false;
|
|
148
|
-
if (isDbAvailable()) {
|
|
149
|
-
const milestone = getMilestone(milestoneId);
|
|
150
|
-
return !!milestone && milestone.status === "complete";
|
|
151
|
-
}
|
|
152
|
-
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
153
|
-
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
154
|
-
if (!roadmapContent)
|
|
155
|
-
return false;
|
|
156
|
-
const roadmap = parseLegacyRoadmap(roadmapContent);
|
|
157
|
-
return isMilestoneComplete(roadmap);
|
|
158
|
-
}
|
|
159
143
|
export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode = "none") {
|
|
160
144
|
// Degrade gracefully if not a git repo
|
|
161
145
|
if (!nativeIsRepo(basePath)) {
|