@opengsd/gsd-pi 1.0.2-dev.5961fbf → 1.0.2-dev.5f7864c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -12
- package/dist/onboarding.js +22 -3
- package/dist/resource-loader.d.ts +2 -0
- package/dist/resource-loader.js +18 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/context7/index.js +12 -2
- package/dist/resources/extensions/get-secrets-from-user.js +16 -16
- package/dist/resources/extensions/google-cli/index.js +30 -0
- package/dist/resources/extensions/google-cli/models.js +55 -0
- package/dist/resources/extensions/google-cli/package.json +11 -0
- package/dist/resources/extensions/google-cli/readiness.js +12 -0
- package/dist/resources/extensions/google-cli/stream-adapter.js +191 -0
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-start.js +232 -49
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
- package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
- package/dist/resources/extensions/gsd/commands-usage.js +105 -1
- package/dist/resources/extensions/gsd/config-overlay.js +20 -14
- package/dist/resources/extensions/gsd/context-overlay.js +22 -16
- package/dist/resources/extensions/gsd/dashboard-overlay.js +10 -23
- package/dist/resources/extensions/gsd/doctor-providers.js +54 -24
- package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/key-manager.js +45 -13
- package/dist/resources/extensions/gsd/notification-overlay.js +8 -9
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +15 -13
- package/dist/resources/extensions/gsd/prompt-loader.js +2 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.js +28 -18
- package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
- package/dist/resources/extensions/gsd/tui/render-kit.js +51 -0
- package/dist/resources/extensions/gsd/vision-ask.js +22 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.js +8 -36
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
- package/dist/resources/extensions/search-the-web/native-search.js +57 -8
- package/dist/resources/extensions/shared/confirm-ui.js +9 -6
- package/dist/resources/extensions/shared/dialog-frame.js +42 -0
- package/dist/resources/extensions/shared/interview-ui.js +42 -30
- package/dist/resources/extensions/shared/next-action-ui.js +6 -6
- package/dist/resources/shared/package-manager-detection.js +36 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +7 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
- 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/api/update/route.js +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 +6 -6
- package/dist/web/standalone/.next/server/chunks/1834.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/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/dist/modes/interactive/components/dialog-container.d.ts +12 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts.map +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js +45 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js.map +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts +3 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js +11 -11
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts +3 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js +13 -11
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts +3 -3
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js +12 -10
- package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +2 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts +6 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js +9 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts +3 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +144 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js +2 -14
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +13 -13
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +57 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +64 -28
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +50 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -0
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.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 +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install/detect-existing.js +17 -3
- package/scripts/install/npm-global.js +103 -33
- package/scripts/install.js +1 -0
- package/src/resources/extensions/context7/index.ts +15 -2
- package/src/resources/extensions/get-secrets-from-user.ts +17 -16
- package/src/resources/extensions/google-cli/index.ts +34 -0
- package/src/resources/extensions/google-cli/models.ts +57 -0
- package/src/resources/extensions/google-cli/package.json +11 -0
- package/src/resources/extensions/google-cli/readiness.ts +15 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +245 -0
- package/src/resources/extensions/gsd/auto/session.ts +3 -0
- package/src/resources/extensions/gsd/auto-start.ts +307 -56
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
- package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
- package/src/resources/extensions/gsd/commands-usage.ts +110 -5
- package/src/resources/extensions/gsd/config-overlay.ts +19 -16
- package/src/resources/extensions/gsd/context-overlay.ts +24 -19
- package/src/resources/extensions/gsd/dashboard-overlay.ts +14 -27
- package/src/resources/extensions/gsd/doctor-providers.ts +55 -27
- package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/key-manager.ts +57 -14
- package/src/resources/extensions/gsd/notification-overlay.ts +12 -11
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +16 -12
- package/src/resources/extensions/gsd/prompt-loader.ts +2 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +29 -20
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +5 -3
- package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
- package/src/resources/extensions/gsd/tests/commands-usage.test.ts +97 -0
- package/src/resources/extensions/gsd/tests/context-chart.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
- package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/show-config-command.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
- package/src/resources/extensions/gsd/tests/tui-border-assertions.ts +28 -0
- package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/vision-ask.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
- package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
- package/src/resources/extensions/gsd/tui/render-kit.ts +82 -0
- package/src/resources/extensions/gsd/vision-ask.ts +28 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +12 -40
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
- package/src/resources/extensions/search-the-web/native-search.ts +60 -8
- package/src/resources/extensions/shared/confirm-ui.ts +8 -12
- package/src/resources/extensions/shared/dialog-frame.ts +71 -0
- package/src/resources/extensions/shared/interview-ui.ts +43 -42
- package/src/resources/extensions/shared/next-action-ui.ts +6 -6
- package/src/resources/extensions/shared/tests/confirm-ui.test.ts +57 -0
- package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +163 -0
- package/src/resources/extensions/shared/tests/next-action-ui-hasui.test.ts +55 -0
- package/src/resources/shared/package-manager-detection.ts +39 -0
- /package/dist/web/standalone/.next/static/{spUYLkQXoHJyxYOMH9VQy → IjxvcC7sl_MHNKXsUZrAy}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{spUYLkQXoHJyxYOMH9VQy → IjxvcC7sl_MHNKXsUZrAy}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createAssistantMessageEventStream } from "@gsd/pi-ai";
|
|
3
|
+
const ZERO_USAGE = {
|
|
4
|
+
input: 0,
|
|
5
|
+
output: 0,
|
|
6
|
+
cacheRead: 0,
|
|
7
|
+
cacheWrite: 0,
|
|
8
|
+
totalTokens: 0,
|
|
9
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
10
|
+
};
|
|
11
|
+
function textBlocks(content) {
|
|
12
|
+
return content
|
|
13
|
+
.map((block) => block.type === "text" ? block.text : `[${block.type} omitted]`)
|
|
14
|
+
.join("\n");
|
|
15
|
+
}
|
|
16
|
+
function messageToText(message) {
|
|
17
|
+
if (message.role === "user") {
|
|
18
|
+
const content = typeof message.content === "string" ? message.content : textBlocks(message.content);
|
|
19
|
+
return `User:\n${content}`;
|
|
20
|
+
}
|
|
21
|
+
if (message.role === "assistant") {
|
|
22
|
+
const text = message.content
|
|
23
|
+
.map((block) => {
|
|
24
|
+
if (block.type === "text")
|
|
25
|
+
return block.text;
|
|
26
|
+
if (block.type === "thinking")
|
|
27
|
+
return `[thinking omitted]`;
|
|
28
|
+
if (block.type === "toolCall")
|
|
29
|
+
return `[tool call: ${block.name}]`;
|
|
30
|
+
if (block.type === "serverToolUse")
|
|
31
|
+
return `[server tool: ${block.name}]`;
|
|
32
|
+
if (block.type === "webSearchResult")
|
|
33
|
+
return `[web search result omitted]`;
|
|
34
|
+
return `[${block.type} omitted]`;
|
|
35
|
+
})
|
|
36
|
+
.join("\n");
|
|
37
|
+
return `Assistant:\n${text}`;
|
|
38
|
+
}
|
|
39
|
+
return `Tool result (${message.toolName}):\n${textBlocks(message.content)}`;
|
|
40
|
+
}
|
|
41
|
+
export function buildGoogleCliPrompt(context) {
|
|
42
|
+
const parts = [];
|
|
43
|
+
if (context.systemPrompt?.trim()) {
|
|
44
|
+
parts.push(`System instructions:\n${context.systemPrompt.trim()}`);
|
|
45
|
+
}
|
|
46
|
+
if (context.messages.length > 0) {
|
|
47
|
+
parts.push(context.messages.map(messageToText).join("\n\n"));
|
|
48
|
+
}
|
|
49
|
+
if (context.tools?.length) {
|
|
50
|
+
const names = context.tools.map((tool) => tool.name).join(", ");
|
|
51
|
+
parts.push(`Available local GSD tools were not forwarded to this external CLI bridge. ` +
|
|
52
|
+
`If you need to act, use the CLI's own tools or ask the user to switch to a provider with native tool-call support. ` +
|
|
53
|
+
`Requested GSD tools: ${names}`);
|
|
54
|
+
}
|
|
55
|
+
return parts.join("\n\n").trim();
|
|
56
|
+
}
|
|
57
|
+
function buildAssistantMessage(model, text, stopReason = "stop", errorMessage) {
|
|
58
|
+
return {
|
|
59
|
+
role: "assistant",
|
|
60
|
+
content: text ? [{ type: "text", text }] : [],
|
|
61
|
+
api: model.api,
|
|
62
|
+
provider: model.provider,
|
|
63
|
+
model: model.id,
|
|
64
|
+
usage: { ...ZERO_USAGE, cost: { ...ZERO_USAGE.cost } },
|
|
65
|
+
stopReason,
|
|
66
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function extractGeminiJsonResponse(raw) {
|
|
71
|
+
const trimmed = raw.trim();
|
|
72
|
+
if (!trimmed)
|
|
73
|
+
return "";
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(trimmed);
|
|
76
|
+
for (const key of ["response", "text", "content", "message"]) {
|
|
77
|
+
const value = parsed[key];
|
|
78
|
+
if (typeof value === "string")
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(parsed, null, 2);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return trimmed;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function commandForProvider(provider) {
|
|
88
|
+
return provider === "google-gemini-cli" ? "gemini" : "agy";
|
|
89
|
+
}
|
|
90
|
+
function argsForProvider(provider, model, prompt) {
|
|
91
|
+
if (provider === "google-gemini-cli") {
|
|
92
|
+
const args = ["-p", prompt, "--output-format", "json"];
|
|
93
|
+
if (model.id !== "default")
|
|
94
|
+
args.unshift("-m", model.id);
|
|
95
|
+
return args;
|
|
96
|
+
}
|
|
97
|
+
const args = ["-p", prompt];
|
|
98
|
+
if (model.id !== "default")
|
|
99
|
+
args.unshift("-m", model.id);
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
export function buildGoogleCliSpawnInvocation(command, args, platform = process.platform) {
|
|
103
|
+
if (platform === "win32") {
|
|
104
|
+
return { command: "cmd", args: ["/c", command, ...args] };
|
|
105
|
+
}
|
|
106
|
+
return { command, args };
|
|
107
|
+
}
|
|
108
|
+
function runCli(command, args, options) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const invocation = buildGoogleCliSpawnInvocation(command, args);
|
|
111
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
112
|
+
cwd: options?.cwd || process.cwd(),
|
|
113
|
+
env: process.env,
|
|
114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
115
|
+
});
|
|
116
|
+
let stdout = "";
|
|
117
|
+
let stderr = "";
|
|
118
|
+
let settled = false;
|
|
119
|
+
const settle = (fn) => {
|
|
120
|
+
if (settled)
|
|
121
|
+
return;
|
|
122
|
+
settled = true;
|
|
123
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
124
|
+
fn();
|
|
125
|
+
};
|
|
126
|
+
const onAbort = () => {
|
|
127
|
+
child.kill("SIGTERM");
|
|
128
|
+
settle(() => reject(new Error("Request was aborted")));
|
|
129
|
+
};
|
|
130
|
+
if (options?.signal?.aborted) {
|
|
131
|
+
onAbort();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
options?.signal?.addEventListener("abort", onAbort);
|
|
135
|
+
child.stdout.setEncoding("utf8");
|
|
136
|
+
child.stderr.setEncoding("utf8");
|
|
137
|
+
child.stdout.on("data", (chunk) => {
|
|
138
|
+
stdout += chunk;
|
|
139
|
+
});
|
|
140
|
+
child.stderr.on("data", (chunk) => {
|
|
141
|
+
stderr += chunk;
|
|
142
|
+
});
|
|
143
|
+
child.on("error", (error) => {
|
|
144
|
+
settle(() => reject(error));
|
|
145
|
+
});
|
|
146
|
+
child.on("close", (code, signal) => {
|
|
147
|
+
settle(() => resolve({ stdout, stderr, code, signal }));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function emitText(stream, message, text) {
|
|
152
|
+
stream.push({ type: "start", partial: { ...message, content: [] } });
|
|
153
|
+
if (text) {
|
|
154
|
+
stream.push({ type: "text_start", contentIndex: 0, partial: message });
|
|
155
|
+
stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial: message });
|
|
156
|
+
stream.push({ type: "text_end", contentIndex: 0, content: text, partial: message });
|
|
157
|
+
}
|
|
158
|
+
stream.push({ type: "done", reason: "stop", message });
|
|
159
|
+
stream.end(message);
|
|
160
|
+
}
|
|
161
|
+
function emitError(stream, model, error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
const output = buildAssistantMessage(model, "", "error", message);
|
|
164
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
165
|
+
stream.end(output);
|
|
166
|
+
}
|
|
167
|
+
export function streamViaGoogleCli(model, context, options) {
|
|
168
|
+
const stream = createAssistantMessageEventStream();
|
|
169
|
+
const provider = model.provider;
|
|
170
|
+
queueMicrotask(async () => {
|
|
171
|
+
try {
|
|
172
|
+
const prompt = buildGoogleCliPrompt(context);
|
|
173
|
+
const command = commandForProvider(provider);
|
|
174
|
+
const args = argsForProvider(provider, model, prompt);
|
|
175
|
+
const result = await runCli(command, args, options);
|
|
176
|
+
if (result.code !== 0) {
|
|
177
|
+
const detail = (result.stderr || result.stdout || `CLI exited with code ${result.code}`).trim();
|
|
178
|
+
throw new Error(detail);
|
|
179
|
+
}
|
|
180
|
+
const text = provider === "google-gemini-cli"
|
|
181
|
+
? extractGeminiJsonResponse(result.stdout)
|
|
182
|
+
: result.stdout.trim();
|
|
183
|
+
const message = buildAssistantMessage(model, text);
|
|
184
|
+
emitText(stream, message, text);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
emitError(stream, model, error);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return stream;
|
|
191
|
+
}
|
|
@@ -132,6 +132,8 @@ export class AutoSession {
|
|
|
132
132
|
// ── Isolation degradation ────────────────────────────────────────────
|
|
133
133
|
/** Set to true when worktree creation fails; prevents merge of nonexistent branch. */
|
|
134
134
|
isolationDegraded = false;
|
|
135
|
+
/** Temporary recovery mode for stranded work adopted from physical git evidence. */
|
|
136
|
+
strandedRecoveryIsolationMode = null;
|
|
135
137
|
/** Project-root dirty snapshot captured before an isolated worktree unit runs. */
|
|
136
138
|
rootWriteBaseline = null;
|
|
137
139
|
// ── Merge guard ──────────────────────────────────────────────────────
|
|
@@ -285,6 +287,7 @@ export class AutoSession {
|
|
|
285
287
|
this.lastGitActionFailure = null;
|
|
286
288
|
this.lastGitActionStatus = null;
|
|
287
289
|
this.isolationDegraded = false;
|
|
290
|
+
this.strandedRecoveryIsolationMode = null;
|
|
288
291
|
this.rootWriteBaseline = null;
|
|
289
292
|
this.milestoneMergedInPhases = false;
|
|
290
293
|
this.milestoneStartShas = new Map();
|
|
@@ -21,7 +21,7 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
21
21
|
import { writeLock, clearLock, readCrashLock, isLockProcessAlive } from "./crash-recovery.js";
|
|
22
22
|
import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
23
23
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
24
|
-
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
|
|
24
|
+
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, nativeHasChanges, } from "./native-git-bridge.js";
|
|
25
25
|
import { GitServiceImpl } from "./git-service.js";
|
|
26
26
|
import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
|
|
27
27
|
import { getAutoWorktreePath, checkoutBranchWithStashGuard } from "./auto-worktree.js";
|
|
@@ -126,17 +126,68 @@ export function resolveSurvivorRecoveryIsolationMode(isolationMode, phase) {
|
|
|
126
126
|
return "branch";
|
|
127
127
|
return isolationMode;
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
function isBlockingStrandedWorkAction(action) {
|
|
130
|
+
return action.kind === "in-progress-stranded-work" && action.blocksAuto;
|
|
131
|
+
}
|
|
132
|
+
function detectWorktreeEvidence(basePath, milestoneId, hasChanges) {
|
|
133
|
+
const wtDir = getWorktreeDir(basePath, milestoneId);
|
|
134
|
+
const wtPath = getAutoWorktreePath(basePath, milestoneId);
|
|
135
|
+
let dirty = false;
|
|
136
|
+
if (wtPath) {
|
|
137
|
+
try {
|
|
138
|
+
dirty = hasChanges(wtPath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
dirty = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
path: wtPath,
|
|
146
|
+
dirExists: existsSync(wtDir),
|
|
147
|
+
dirty,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function strandedWorkMessage(args) {
|
|
151
|
+
const evidence = [];
|
|
152
|
+
if (args.branch && args.commitsAhead > 0) {
|
|
153
|
+
evidence.push(`branch ${args.branch} has ${args.commitsAhead} commit(s) ahead of ${args.mainBranch}`);
|
|
154
|
+
}
|
|
155
|
+
if (args.dirtyWorktree) {
|
|
156
|
+
evidence.push("the worktree has uncommitted changes");
|
|
157
|
+
}
|
|
158
|
+
if (evidence.length === 0) {
|
|
159
|
+
evidence.push("physical git evidence exists");
|
|
160
|
+
}
|
|
161
|
+
const wtSuffix = args.worktreeDirExists
|
|
162
|
+
? ` Worktree directory at .gsd/worktrees/${args.milestoneId}/ holds live work.`
|
|
163
|
+
: "";
|
|
164
|
+
const recovery = args.recoveryMode === "worktree"
|
|
165
|
+
? "Recovering will adopt the existing worktree."
|
|
166
|
+
: "Recovering will adopt the milestone branch.";
|
|
167
|
+
return (`Stranded work for in-progress milestone ${args.milestoneId}: ${evidence.join("; ")}.` +
|
|
168
|
+
wtSuffix +
|
|
169
|
+
` ${recovery} Park or discard explicitly if abandoning.`);
|
|
170
|
+
}
|
|
171
|
+
export function auditOrphanedMilestoneBranches(basePath, _isolationMode, gitDeps = {}) {
|
|
130
172
|
const recovered = [];
|
|
131
173
|
const warnings = [];
|
|
174
|
+
const actions = [];
|
|
132
175
|
const branchList = gitDeps.branchList ?? nativeBranchList;
|
|
133
176
|
const branchExists = gitDeps.branchExists ?? nativeBranchExists;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
177
|
+
const hasChanges = gitDeps.hasChanges ?? nativeHasChanges;
|
|
178
|
+
const pushAction = (action) => {
|
|
179
|
+
actions.push(action);
|
|
180
|
+
if (action.severity === "info") {
|
|
181
|
+
recovered.push(action.message);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
warnings.push(action.message);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
137
187
|
// Skip if DB not available — can't determine completion status
|
|
138
|
-
if (!isDbAvailable())
|
|
139
|
-
return { recovered, warnings };
|
|
188
|
+
if (!isDbAvailable()) {
|
|
189
|
+
return { recovered, warnings, actions, blockingStrandedWork: null };
|
|
190
|
+
}
|
|
140
191
|
let milestoneBranches;
|
|
141
192
|
let milestoneBranchListAvailable = true;
|
|
142
193
|
try {
|
|
@@ -170,6 +221,7 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
170
221
|
if (!milestone)
|
|
171
222
|
continue;
|
|
172
223
|
const isMerged = mergedBranches.has(branch);
|
|
224
|
+
const worktreeEvidence = detectWorktreeEvidence(basePath, milestoneId, hasChanges);
|
|
173
225
|
// #4762 — in-progress milestone branch with unmerged commits ahead of
|
|
174
226
|
// main. This is the pre-completion orphan case: auto-mode exited without
|
|
175
227
|
// completing the milestone (pause, stop, crash, merge error, blocker) and
|
|
@@ -181,32 +233,45 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
181
233
|
// Parked/other closed statuses go through the legacy complete/unmerged
|
|
182
234
|
// path below where appropriate.
|
|
183
235
|
if (!isClosedStatus(milestone.status)) {
|
|
184
|
-
if (isMerged)
|
|
185
|
-
continue; // nothing to recover
|
|
186
236
|
let commitsAhead = 0;
|
|
187
237
|
try {
|
|
188
238
|
commitsAhead = nativeCommitCountBetween(basePath, mainBranch, branch);
|
|
189
239
|
}
|
|
190
240
|
catch {
|
|
191
|
-
|
|
192
|
-
continue;
|
|
241
|
+
commitsAhead = 0;
|
|
193
242
|
}
|
|
194
|
-
if (commitsAhead === 0)
|
|
243
|
+
if ((isMerged || commitsAhead === 0) && !worktreeEvidence.dirty)
|
|
195
244
|
continue;
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
245
|
+
const recoveryMode = worktreeEvidence.path
|
|
246
|
+
? "worktree"
|
|
247
|
+
: "branch";
|
|
248
|
+
const message = strandedWorkMessage({
|
|
249
|
+
milestoneId,
|
|
250
|
+
branch,
|
|
251
|
+
commitsAhead,
|
|
252
|
+
mainBranch,
|
|
253
|
+
dirtyWorktree: worktreeEvidence.dirty,
|
|
254
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
255
|
+
recoveryMode,
|
|
256
|
+
});
|
|
257
|
+
pushAction({
|
|
258
|
+
kind: "in-progress-stranded-work",
|
|
259
|
+
milestoneId,
|
|
260
|
+
branch,
|
|
261
|
+
commitsAhead,
|
|
262
|
+
dirtyWorktree: worktreeEvidence.dirty,
|
|
263
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
264
|
+
recoveryMode,
|
|
265
|
+
message,
|
|
266
|
+
severity: "warning",
|
|
267
|
+
blocksAuto: true,
|
|
268
|
+
});
|
|
204
269
|
// #4764 telemetry
|
|
205
270
|
try {
|
|
206
271
|
emitWorktreeOrphaned(basePath, milestoneId, {
|
|
207
272
|
reason: "in-progress-unmerged",
|
|
208
273
|
commitsAhead,
|
|
209
|
-
worktreeDirExists:
|
|
274
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
210
275
|
});
|
|
211
276
|
}
|
|
212
277
|
catch (err) {
|
|
@@ -223,7 +288,14 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
223
288
|
// Branch is merged — safe to delete branch and clean up worktree dir
|
|
224
289
|
try {
|
|
225
290
|
nativeBranchDelete(basePath, branch, true);
|
|
226
|
-
|
|
291
|
+
pushAction({
|
|
292
|
+
kind: "complete-merged-branch",
|
|
293
|
+
milestoneId,
|
|
294
|
+
branch,
|
|
295
|
+
message: `Deleted merged branch ${branch} for completed milestone ${milestoneId}.`,
|
|
296
|
+
severity: "info",
|
|
297
|
+
blocksAuto: false,
|
|
298
|
+
});
|
|
227
299
|
}
|
|
228
300
|
catch (err) {
|
|
229
301
|
warnings.push(`Failed to delete merged branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -246,7 +318,15 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
246
318
|
if (isInsideWorktreesDir(basePath, wtDir)) {
|
|
247
319
|
try {
|
|
248
320
|
rmSync(wtDir, { recursive: true, force: true });
|
|
249
|
-
|
|
321
|
+
pushAction({
|
|
322
|
+
kind: "complete-merged-worktree",
|
|
323
|
+
milestoneId,
|
|
324
|
+
branch,
|
|
325
|
+
worktreeDirExists: true,
|
|
326
|
+
message: `Removed orphaned worktree directory for ${milestoneId}.`,
|
|
327
|
+
severity: "info",
|
|
328
|
+
blocksAuto: false,
|
|
329
|
+
});
|
|
250
330
|
}
|
|
251
331
|
catch (err2) {
|
|
252
332
|
warnings.push(`Failed to remove worktree directory for ${milestoneId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
@@ -257,14 +337,30 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
257
337
|
}
|
|
258
338
|
}
|
|
259
339
|
else {
|
|
260
|
-
|
|
340
|
+
pushAction({
|
|
341
|
+
kind: "complete-merged-worktree",
|
|
342
|
+
milestoneId,
|
|
343
|
+
branch,
|
|
344
|
+
worktreeDirExists: true,
|
|
345
|
+
message: `Removed orphaned worktree directory for ${milestoneId}.`,
|
|
346
|
+
severity: "info",
|
|
347
|
+
blocksAuto: false,
|
|
348
|
+
});
|
|
261
349
|
}
|
|
262
350
|
}
|
|
263
351
|
}
|
|
264
352
|
else {
|
|
265
353
|
// Branch is NOT merged — preserve for safety, warn the user
|
|
266
|
-
|
|
267
|
-
|
|
354
|
+
pushAction({
|
|
355
|
+
kind: "complete-unmerged-branch",
|
|
356
|
+
milestoneId,
|
|
357
|
+
branch,
|
|
358
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
359
|
+
message: `Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
|
|
360
|
+
`This may contain unmerged work. Merge manually or run \`/gsd doctor fix\` to resolve.`,
|
|
361
|
+
severity: "warning",
|
|
362
|
+
blocksAuto: false,
|
|
363
|
+
});
|
|
268
364
|
// #4764 telemetry
|
|
269
365
|
try {
|
|
270
366
|
emitWorktreeOrphaned(basePath, milestoneId, {
|
|
@@ -300,6 +396,43 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
300
396
|
completedMilestones = [];
|
|
301
397
|
}
|
|
302
398
|
for (const m of completedMilestones) {
|
|
399
|
+
if (!isClosedStatus(m.status)) {
|
|
400
|
+
if (seenMilestoneIds.has(m.id))
|
|
401
|
+
continue;
|
|
402
|
+
const worktreeEvidence = detectWorktreeEvidence(basePath, m.id, hasChanges);
|
|
403
|
+
if (!worktreeEvidence.dirty)
|
|
404
|
+
continue;
|
|
405
|
+
const message = strandedWorkMessage({
|
|
406
|
+
milestoneId: m.id,
|
|
407
|
+
commitsAhead: 0,
|
|
408
|
+
mainBranch,
|
|
409
|
+
dirtyWorktree: true,
|
|
410
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
411
|
+
recoveryMode: "worktree",
|
|
412
|
+
});
|
|
413
|
+
pushAction({
|
|
414
|
+
kind: "in-progress-stranded-work",
|
|
415
|
+
milestoneId: m.id,
|
|
416
|
+
commitsAhead: 0,
|
|
417
|
+
dirtyWorktree: true,
|
|
418
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
419
|
+
recoveryMode: "worktree",
|
|
420
|
+
message,
|
|
421
|
+
severity: "warning",
|
|
422
|
+
blocksAuto: true,
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
emitWorktreeOrphaned(basePath, m.id, {
|
|
426
|
+
reason: "in-progress-unmerged",
|
|
427
|
+
commitsAhead: 0,
|
|
428
|
+
worktreeDirExists: worktreeEvidence.dirExists,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
logWarning("engine", `worktree-orphaned telemetry failed for ${m.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
433
|
+
}
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
303
436
|
if (m.status !== "complete")
|
|
304
437
|
continue;
|
|
305
438
|
if (seenMilestoneIds.has(m.id))
|
|
@@ -332,17 +465,36 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode, gitDeps
|
|
|
332
465
|
if (existsSync(wtDir)) {
|
|
333
466
|
try {
|
|
334
467
|
rmSync(wtDir, { recursive: true, force: true });
|
|
335
|
-
|
|
468
|
+
pushAction({
|
|
469
|
+
kind: "complete-branchless-worktree",
|
|
470
|
+
milestoneId: m.id,
|
|
471
|
+
worktreeDirExists: true,
|
|
472
|
+
message: `Removed orphaned worktree directory for ${m.id} (branch already deleted).`,
|
|
473
|
+
severity: "info",
|
|
474
|
+
blocksAuto: false,
|
|
475
|
+
});
|
|
336
476
|
}
|
|
337
477
|
catch (err) {
|
|
338
478
|
warnings.push(`Failed to remove orphaned worktree directory for ${m.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
339
479
|
}
|
|
340
480
|
}
|
|
341
481
|
else {
|
|
342
|
-
|
|
482
|
+
pushAction({
|
|
483
|
+
kind: "complete-branchless-worktree",
|
|
484
|
+
milestoneId: m.id,
|
|
485
|
+
worktreeDirExists: true,
|
|
486
|
+
message: `Removed orphaned worktree directory for ${m.id} (branch already deleted).`,
|
|
487
|
+
severity: "info",
|
|
488
|
+
blocksAuto: false,
|
|
489
|
+
});
|
|
343
490
|
}
|
|
344
491
|
}
|
|
345
|
-
return {
|
|
492
|
+
return {
|
|
493
|
+
recovered,
|
|
494
|
+
warnings,
|
|
495
|
+
actions,
|
|
496
|
+
blockingStrandedWork: actions.find(isBlockingStrandedWorkAction) ?? null,
|
|
497
|
+
};
|
|
346
498
|
}
|
|
347
499
|
/**
|
|
348
500
|
* Pure decision function for picking which orphan milestone the auto-loop
|
|
@@ -679,17 +831,27 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
679
831
|
// was lost due to session ending between completion and teardown.
|
|
680
832
|
// Must run after DB open and before worktree entry.
|
|
681
833
|
let orphanAuditRecovered = false;
|
|
834
|
+
let strandedRecoveryActions = [];
|
|
835
|
+
let strandedRecoveryAction = null;
|
|
682
836
|
try {
|
|
683
837
|
const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode(base));
|
|
838
|
+
strandedRecoveryActions = auditResult.actions.filter(isBlockingStrandedWorkAction);
|
|
839
|
+
strandedRecoveryAction = strandedRecoveryActions[0] ?? null;
|
|
684
840
|
for (const msg of auditResult.recovered) {
|
|
685
841
|
ctx.ui.notify(`Orphan audit: ${msg}`, "info");
|
|
686
842
|
}
|
|
687
843
|
for (const msg of auditResult.warnings) {
|
|
688
|
-
|
|
844
|
+
const prefix = msg.startsWith("Stranded work") ? "" : "Orphan audit: ";
|
|
845
|
+
ctx.ui.notify(`${prefix}${msg}`, "warning");
|
|
689
846
|
}
|
|
690
847
|
if (auditResult.recovered.length > 0) {
|
|
691
848
|
orphanAuditRecovered = true;
|
|
692
|
-
debugLog("orphan-audit", {
|
|
849
|
+
debugLog("orphan-audit", {
|
|
850
|
+
recovered: auditResult.recovered,
|
|
851
|
+
warnings: auditResult.warnings,
|
|
852
|
+
strandedRecoveryAction,
|
|
853
|
+
strandedRecoveryActions,
|
|
854
|
+
});
|
|
693
855
|
}
|
|
694
856
|
}
|
|
695
857
|
catch (err) {
|
|
@@ -724,13 +886,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
724
886
|
logWarning("bootstrap", `orphaned preflight-stash audit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
725
887
|
}
|
|
726
888
|
let state = await deriveState(base);
|
|
727
|
-
if (process.env.GSD_HEADLESS === "1" &&
|
|
728
|
-
orphanAuditRecovered &&
|
|
729
|
-
!state.activeMilestone &&
|
|
730
|
-
state.phase === "complete") {
|
|
731
|
-
ctx.ui.notify("Auto-mode stopped (Recovered completed milestone cleanup; all milestones complete).", "info");
|
|
732
|
-
return releaseLockAndReturn();
|
|
733
|
-
}
|
|
734
889
|
// Stale worktree state recovery (#654)
|
|
735
890
|
if (state.activeMilestone &&
|
|
736
891
|
shouldUseWorktreeIsolation(base) &&
|
|
@@ -740,6 +895,28 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
740
895
|
state = await deriveState(wtPath);
|
|
741
896
|
}
|
|
742
897
|
}
|
|
898
|
+
const blockingStrandedRecoveryAction = state.activeMilestone
|
|
899
|
+
? strandedRecoveryActions.find((action) => action.milestoneId !== state.activeMilestone?.id) ?? strandedRecoveryAction
|
|
900
|
+
: strandedRecoveryAction;
|
|
901
|
+
if (blockingStrandedRecoveryAction) {
|
|
902
|
+
if (!state.activeMilestone) {
|
|
903
|
+
ctx.ui.notify(`Stranded work for ${blockingStrandedRecoveryAction.milestoneId} blocks auto-mode, but that milestone is not active in project state. Park or discard it explicitly before continuing.`, "error");
|
|
904
|
+
return releaseLockAndReturn();
|
|
905
|
+
}
|
|
906
|
+
if (state.activeMilestone.id !== blockingStrandedRecoveryAction.milestoneId) {
|
|
907
|
+
ctx.ui.notify(`Stranded work for ${blockingStrandedRecoveryAction.milestoneId} blocks auto-mode before ${state.activeMilestone.id}. Recover, park, or discard ${blockingStrandedRecoveryAction.milestoneId} explicitly before continuing.`, "error");
|
|
908
|
+
return releaseLockAndReturn();
|
|
909
|
+
}
|
|
910
|
+
strandedRecoveryAction = blockingStrandedRecoveryAction;
|
|
911
|
+
ctx.ui.notify(`Recovering stranded work for ${strandedRecoveryAction.milestoneId} before dispatching new units.`, "info");
|
|
912
|
+
}
|
|
913
|
+
if (process.env.GSD_HEADLESS === "1" &&
|
|
914
|
+
orphanAuditRecovered &&
|
|
915
|
+
!state.activeMilestone &&
|
|
916
|
+
state.phase === "complete") {
|
|
917
|
+
ctx.ui.notify("Auto-mode stopped (Recovered completed milestone cleanup; all milestones complete).", "info");
|
|
918
|
+
return releaseLockAndReturn();
|
|
919
|
+
}
|
|
743
920
|
// Milestone branch recovery (#601, #2358)
|
|
744
921
|
// Detect survivor milestone branches in both pre-planning and complete phases.
|
|
745
922
|
// In phase=complete, the milestone artifacts exist but finalization (merge,
|
|
@@ -768,7 +945,8 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
768
945
|
// The worktree/branch was created but the milestone only has CONTEXT-DRAFT.md.
|
|
769
946
|
// Route to the interactive discussion handler instead of falling through to
|
|
770
947
|
// auto-mode, which would immediately stop with "needs discussion".
|
|
771
|
-
if (
|
|
948
|
+
if (!strandedRecoveryAction &&
|
|
949
|
+
decideSurvivorAction(hasSurvivorBranch, state.phase) === "discuss") {
|
|
772
950
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
773
951
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
774
952
|
invalidateAllCaches();
|
|
@@ -848,14 +1026,14 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
848
1026
|
const effectivePrefs = loadEffectiveGSDPreferences(base)?.preferences;
|
|
849
1027
|
const { shouldRunDeepProjectSetup } = await import("./auto-dispatch.js");
|
|
850
1028
|
const deepProjectStagePending = shouldRunDeepProjectSetup(state, effectivePrefs, base, { hasSurvivorBranch });
|
|
851
|
-
if (deepProjectStagePending) {
|
|
1029
|
+
if (deepProjectStagePending && !strandedRecoveryAction) {
|
|
852
1030
|
// Deep project-level setup runs before the first milestone exists. Let
|
|
853
1031
|
// the auto loop dispatch workflow-preferences / project / requirements
|
|
854
1032
|
// units instead of recursing back through showSmartEntry while this
|
|
855
1033
|
// bootstrap still holds the session lock.
|
|
856
1034
|
s.currentMilestoneId = null;
|
|
857
1035
|
}
|
|
858
|
-
if (!hasSurvivorBranch && !deepProjectStagePending) {
|
|
1036
|
+
if (!hasSurvivorBranch && !deepProjectStagePending && !strandedRecoveryAction) {
|
|
859
1037
|
// No active work — start a new milestone via discuss flow
|
|
860
1038
|
if (!state.activeMilestone || state.phase === "complete") {
|
|
861
1039
|
// Guard against recursive dialog loop (#1348):
|
|
@@ -917,7 +1095,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
917
1095
|
}
|
|
918
1096
|
}
|
|
919
1097
|
// Unreachable safety check
|
|
920
|
-
if (!state.activeMilestone && !deepProjectStagePending) {
|
|
1098
|
+
if (!state.activeMilestone && !deepProjectStagePending && !strandedRecoveryAction) {
|
|
921
1099
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
922
1100
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
923
1101
|
return releaseLockAndReturn();
|
|
@@ -952,7 +1130,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
952
1130
|
s.resourceVersionOnStart = readResourceVersion();
|
|
953
1131
|
s.pendingQuickTasks = [];
|
|
954
1132
|
s.currentUnit = null;
|
|
955
|
-
s.currentMilestoneId ??=
|
|
1133
|
+
s.currentMilestoneId ??=
|
|
1134
|
+
strandedRecoveryAction?.milestoneId ??
|
|
1135
|
+
(deepProjectStagePending ? null : state.activeMilestone?.id ?? null);
|
|
956
1136
|
s.originalModelId = startModelSnapshot?.id ?? ctx.model?.id ?? null;
|
|
957
1137
|
s.originalModelProvider = startModelSnapshot?.provider ?? ctx.model?.provider ?? null;
|
|
958
1138
|
s.originalThinkingLevel = startThinkingSnapshot ?? null;
|
|
@@ -960,7 +1140,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
960
1140
|
registerSigtermHandler(base);
|
|
961
1141
|
// Capture integration branch
|
|
962
1142
|
if (s.currentMilestoneId) {
|
|
963
|
-
if (getIsolationMode(base) !== "none") {
|
|
1143
|
+
if (getIsolationMode(base) !== "none" || strandedRecoveryAction) {
|
|
964
1144
|
captureIntegrationBranch(base, s.currentMilestoneId);
|
|
965
1145
|
}
|
|
966
1146
|
setActiveMilestoneId(base, s.currentMilestoneId);
|
|
@@ -970,7 +1150,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
970
1150
|
// milestone/<MID>. Auto-checkout back to the integration branch.
|
|
971
1151
|
const isolationMode = getIsolationMode(base);
|
|
972
1152
|
const isRepo = nativeIsRepo(base);
|
|
973
|
-
if (isolationMode === "none" && isRepo) {
|
|
1153
|
+
if (isolationMode === "none" && isRepo && !strandedRecoveryAction) {
|
|
974
1154
|
try {
|
|
975
1155
|
const currentBranch = nativeGetCurrentBranch(base);
|
|
976
1156
|
const integrationBranch = nativeDetectMainBranch(base);
|
|
@@ -1001,12 +1181,15 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
1001
1181
|
return symlinkRe.test(p);
|
|
1002
1182
|
};
|
|
1003
1183
|
if (s.currentMilestoneId &&
|
|
1004
|
-
getIsolationMode(base) !== "none" &&
|
|
1184
|
+
(getIsolationMode(base) !== "none" || strandedRecoveryAction?.recoveryMode) &&
|
|
1005
1185
|
!detectWorktreeName(base) &&
|
|
1006
1186
|
!isUnderGsdWorktrees(base)) {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1187
|
+
const lifecycle = buildLifecycle();
|
|
1188
|
+
const enterResult = strandedRecoveryAction?.recoveryMode
|
|
1189
|
+
? lifecycle.adoptStrandedMilestone(s.currentMilestoneId, base, { notify: ctx.ui.notify.bind(ctx.ui) }, { mode: strandedRecoveryAction.recoveryMode })
|
|
1190
|
+
: lifecycle.enterMilestone(s.currentMilestoneId, {
|
|
1191
|
+
notify: ctx.ui.notify.bind(ctx.ui),
|
|
1192
|
+
});
|
|
1010
1193
|
if (!enterResult.ok) {
|
|
1011
1194
|
s.active = false;
|
|
1012
1195
|
if (enterResult.reason === "lease-conflict") {
|