@opengsd/gsd-pi 1.0.2-dev.235ebf3 → 1.0.2-dev.29398d2
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 +7 -0
- package/dist/resource-loader.js +42 -9
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/context7/index.js +12 -2
- 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/loop.js +19 -0
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- 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/auto-worktree.js +2 -54
- 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/doctor-providers.js +54 -24
- package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
- package/dist/resources/extensions/gsd/key-manager.js +45 -13
- 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/worktree-lifecycle.js +24 -3
- package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
- package/dist/resources/extensions/search-the-web/native-search.js +57 -8
- 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 +5 -5
- 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/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +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 +5 -5
- 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/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/package.json +0 -1
- package/dist/worktree-cli.d.ts +0 -2
- package/dist/worktree-cli.js +21 -9
- package/package.json +5 -2
- package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/packages/cloud-mcp-gateway/package.json +4 -3
- 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/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/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.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-mode-class-constants.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.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/bin/gsd-mcp-server.js +14 -0
- 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 +5 -4
- 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/bin/pi-ai.js +14 -0
- package/packages/pi-ai/dist/models.generated.d.ts +40 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +49 -30
- 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 +3 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
- package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
- package/packages/pi-coding-agent/package.json +8 -8
- 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/deps.js +10 -0
- 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/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/loop.ts +22 -0
- package/src/resources/extensions/gsd/auto/phases.ts +1 -1
- 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/auto-worktree.ts +2 -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/doctor-providers.ts +55 -27
- package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
- package/src/resources/extensions/gsd/key-manager.ts +57 -14
- 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/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/custom-engine-loop-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
- 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/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/worktree-post-create-hook.test.ts +141 -1
- 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/worktree-lifecycle.ts +37 -2
- package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
- package/src/resources/extensions/search-the-web/native-search.ts +60 -8
- package/src/resources/shared/package-manager-detection.ts +39 -0
- package/dist/tsconfig.extensions.tsbuildinfo +0 -1
- /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → bukT6Ux1YchPm2XqjaexX}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → bukT6Ux1YchPm2XqjaexX}/_ssgManifest.js +0 -0
|
@@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, rmSync, existsSync, writeFileSync, readFileSync
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
|
|
16
|
-
import { runWorktreePostCreateHook } from "../
|
|
16
|
+
import { runWorktreePostCreateHook } from "../worktree-post-create-hook.ts";
|
|
17
17
|
|
|
18
18
|
function makeTmpDir(): string {
|
|
19
19
|
return mkdtempSync(join(tmpdir(), "gsd-wt-hook-test-"));
|
|
@@ -45,10 +45,150 @@ function writeNodeHookScript(filePath: string, code: string): void {
|
|
|
45
45
|
test("returns null when no hook path is provided", () => {
|
|
46
46
|
const src = makeTmpDir();
|
|
47
47
|
const wt = makeTmpDir();
|
|
48
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
48
49
|
try {
|
|
50
|
+
process.env.GSD_HOME = join(src, "empty-gsd-home");
|
|
49
51
|
const result = runWorktreePostCreateHook(src, wt, undefined);
|
|
50
52
|
assert.equal(result, null);
|
|
51
53
|
} finally {
|
|
54
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
55
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
56
|
+
rmSync(src, { recursive: true, force: true });
|
|
57
|
+
rmSync(wt, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("reads git.worktree_post_create from project preferences without loading auto-worktree", () => {
|
|
62
|
+
const src = makeTmpDir();
|
|
63
|
+
const wt = makeTmpDir();
|
|
64
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
65
|
+
try {
|
|
66
|
+
process.env.GSD_HOME = join(src, "empty-gsd-home");
|
|
67
|
+
const hooksDir = join(src, ".gsd", "hooks");
|
|
68
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(src, ".gsd", "PREFERENCES.md"),
|
|
71
|
+
[
|
|
72
|
+
"---",
|
|
73
|
+
"git:",
|
|
74
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/post-create")}`,
|
|
75
|
+
"---",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
|
+
const hookFile = hookPath(join(hooksDir, "post-create"));
|
|
80
|
+
writeNodeHookScript(
|
|
81
|
+
hookFile,
|
|
82
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "ok");`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const result = runWorktreePostCreateHook(src, wt);
|
|
86
|
+
|
|
87
|
+
assert.equal(result, null);
|
|
88
|
+
assert.ok(existsSync(join(wt, "configured-hook-ran")), "configured hook should run");
|
|
89
|
+
} finally {
|
|
90
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
91
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
92
|
+
rmSync(src, { recursive: true, force: true });
|
|
93
|
+
rmSync(wt, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("prefers canonical project PREFERENCES.md over legacy lowercase preferences.md", () => {
|
|
98
|
+
const src = makeTmpDir();
|
|
99
|
+
const wt = makeTmpDir();
|
|
100
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
101
|
+
try {
|
|
102
|
+
process.env.GSD_HOME = join(src, "empty-gsd-home");
|
|
103
|
+
const hooksDir = join(src, ".gsd", "hooks");
|
|
104
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
105
|
+
writeFileSync(
|
|
106
|
+
join(src, ".gsd", "preferences.md"),
|
|
107
|
+
[
|
|
108
|
+
"---",
|
|
109
|
+
"git:",
|
|
110
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/legacy")}`,
|
|
111
|
+
"---",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
);
|
|
115
|
+
writeFileSync(
|
|
116
|
+
join(src, ".gsd", "PREFERENCES.md"),
|
|
117
|
+
[
|
|
118
|
+
"---",
|
|
119
|
+
"git:",
|
|
120
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/canonical")}`,
|
|
121
|
+
"---",
|
|
122
|
+
"",
|
|
123
|
+
].join("\n"),
|
|
124
|
+
);
|
|
125
|
+
writeNodeHookScript(
|
|
126
|
+
hookPath(join(hooksDir, "canonical")),
|
|
127
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "canonical");`,
|
|
128
|
+
);
|
|
129
|
+
writeNodeHookScript(
|
|
130
|
+
hookPath(join(hooksDir, "legacy")),
|
|
131
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "legacy");`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = runWorktreePostCreateHook(src, wt);
|
|
135
|
+
|
|
136
|
+
assert.equal(result, null);
|
|
137
|
+
assert.equal(readFileSync(join(wt, "configured-hook-ran"), "utf-8"), "canonical");
|
|
138
|
+
} finally {
|
|
139
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
140
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
141
|
+
rmSync(src, { recursive: true, force: true });
|
|
142
|
+
rmSync(wt, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("prefers canonical global PREFERENCES.md over legacy lowercase preferences.md", () => {
|
|
147
|
+
const src = makeTmpDir();
|
|
148
|
+
const wt = makeTmpDir();
|
|
149
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
150
|
+
try {
|
|
151
|
+
const gsdHomeDir = join(src, "global-gsd-home");
|
|
152
|
+
process.env.GSD_HOME = gsdHomeDir;
|
|
153
|
+
const hooksDir = join(src, ".gsd", "hooks");
|
|
154
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
155
|
+
mkdirSync(gsdHomeDir, { recursive: true });
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(gsdHomeDir, "preferences.md"),
|
|
158
|
+
[
|
|
159
|
+
"---",
|
|
160
|
+
"git:",
|
|
161
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/legacy")}`,
|
|
162
|
+
"---",
|
|
163
|
+
"",
|
|
164
|
+
].join("\n"),
|
|
165
|
+
);
|
|
166
|
+
writeFileSync(
|
|
167
|
+
join(gsdHomeDir, "PREFERENCES.md"),
|
|
168
|
+
[
|
|
169
|
+
"---",
|
|
170
|
+
"git:",
|
|
171
|
+
` worktree_post_create: ${hookPath(".gsd/hooks/canonical")}`,
|
|
172
|
+
"---",
|
|
173
|
+
"",
|
|
174
|
+
].join("\n"),
|
|
175
|
+
);
|
|
176
|
+
writeNodeHookScript(
|
|
177
|
+
hookPath(join(hooksDir, "canonical")),
|
|
178
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "canonical");`,
|
|
179
|
+
);
|
|
180
|
+
writeNodeHookScript(
|
|
181
|
+
hookPath(join(hooksDir, "legacy")),
|
|
182
|
+
`require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "legacy");`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const result = runWorktreePostCreateHook(src, wt);
|
|
186
|
+
|
|
187
|
+
assert.equal(result, null);
|
|
188
|
+
assert.equal(readFileSync(join(wt, "configured-hook-ran"), "utf-8"), "canonical");
|
|
189
|
+
} finally {
|
|
190
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
191
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
52
192
|
rmSync(src, { recursive: true, force: true });
|
|
53
193
|
rmSync(wt, { recursive: true, force: true });
|
|
54
194
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { test } from "node:test";
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
17
|
+
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
|
|
@@ -79,3 +79,47 @@ test("#2942: injected existsFn — milestones/ alone is enough", () => {
|
|
|
79
79
|
p === "/proj/.gsd" || p === "/proj/.gsd/milestones";
|
|
80
80
|
assert.equal(hasGsdBootstrapArtifacts("/proj/.gsd", existsFn), true);
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
test("bare /gsd routes zombie .gsd folders to project init before closeout/db checks", async (t) => {
|
|
84
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-zombie-bare-command-"));
|
|
85
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
86
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
87
|
+
|
|
88
|
+
const previousCwd = process.cwd();
|
|
89
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
90
|
+
const previousProjectRoot = process.env.GSD_PROJECT_ROOT;
|
|
91
|
+
try {
|
|
92
|
+
process.chdir(base);
|
|
93
|
+
process.env.GSD_HOME = join(base, ".test-gsd-home");
|
|
94
|
+
delete process.env.GSD_PROJECT_ROOT;
|
|
95
|
+
|
|
96
|
+
const notifications: string[] = [];
|
|
97
|
+
const ctx = {
|
|
98
|
+
hasUI: false,
|
|
99
|
+
ui: {
|
|
100
|
+
notify: (content: unknown) => notifications.push(String(content)),
|
|
101
|
+
setStatus: () => {},
|
|
102
|
+
setWidget: () => {},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const { handleAutoCommand } = await import("../commands/handlers/auto.ts");
|
|
106
|
+
|
|
107
|
+
await handleAutoCommand("", ctx as any, {} as any);
|
|
108
|
+
|
|
109
|
+
assert.ok(
|
|
110
|
+
notifications.some((message) => message.includes("/gsd init did not start")),
|
|
111
|
+
"bare /gsd should route unbootstrapped zombie folders to the init wizard",
|
|
112
|
+
);
|
|
113
|
+
assert.equal(
|
|
114
|
+
existsSync(join(base, ".gsd", "gsd.db")),
|
|
115
|
+
false,
|
|
116
|
+
"bare /gsd should not create the project DB before init has bootstrapped .gsd/",
|
|
117
|
+
);
|
|
118
|
+
} finally {
|
|
119
|
+
process.chdir(previousCwd);
|
|
120
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
121
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
122
|
+
if (previousProjectRoot === undefined) delete process.env.GSD_PROJECT_ROOT;
|
|
123
|
+
else process.env.GSD_PROJECT_ROOT = previousProjectRoot;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -173,6 +173,15 @@ export async function handleCompleteTask(
|
|
|
173
173
|
if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
|
|
174
174
|
return { error: "milestoneId is required and must be a non-empty string" };
|
|
175
175
|
}
|
|
176
|
+
if (!params.oneLiner || typeof params.oneLiner !== "string" || params.oneLiner.trim() === "") {
|
|
177
|
+
return { error: "oneLiner is required and must be a non-empty string" };
|
|
178
|
+
}
|
|
179
|
+
if (!params.narrative || typeof params.narrative !== "string" || params.narrative.trim() === "") {
|
|
180
|
+
return { error: "narrative is required and must be a non-empty string" };
|
|
181
|
+
}
|
|
182
|
+
if (!params.verification || typeof params.verification !== "string" || params.verification.trim() === "") {
|
|
183
|
+
return { error: "verification is required and must be a non-empty string" };
|
|
184
|
+
}
|
|
176
185
|
|
|
177
186
|
const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, params.milestoneId);
|
|
178
187
|
|
|
@@ -306,7 +306,7 @@ export interface TaskCompleteParams {
|
|
|
306
306
|
milestoneId: string;
|
|
307
307
|
oneLiner: string;
|
|
308
308
|
narrative: string;
|
|
309
|
-
verification
|
|
309
|
+
verification?: string;
|
|
310
310
|
deviations?: string;
|
|
311
311
|
knownIssues?: string;
|
|
312
312
|
keyFiles?: string[];
|
|
@@ -315,6 +315,40 @@ export interface TaskCompleteParams {
|
|
|
315
315
|
verificationEvidence?: VerificationEvidenceInput[];
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
type NormalizedVerificationEvidence = {
|
|
319
|
+
command: string;
|
|
320
|
+
exitCode: number;
|
|
321
|
+
verdict: string;
|
|
322
|
+
durationMs: number;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
function normalizeVerificationEvidence(
|
|
326
|
+
evidence: VerificationEvidenceInput[] | undefined,
|
|
327
|
+
): NormalizedVerificationEvidence[] {
|
|
328
|
+
return (evidence ?? []).map((entry) =>
|
|
329
|
+
typeof entry === "string"
|
|
330
|
+
? { command: entry, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 }
|
|
331
|
+
: entry,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function deriveVerificationSummary(
|
|
336
|
+
evidence: NormalizedVerificationEvidence[],
|
|
337
|
+
): string | null {
|
|
338
|
+
if (evidence.length === 0) return null;
|
|
339
|
+
|
|
340
|
+
const rendered = evidence.slice(0, 3).map((entry) => {
|
|
341
|
+
const command = entry.command.trim() || "(unspecified command)";
|
|
342
|
+
const verdict = entry.verdict.trim() || "recorded";
|
|
343
|
+
return `\`${command}\` exited ${entry.exitCode} (${verdict})`;
|
|
344
|
+
});
|
|
345
|
+
const suffix = evidence.length > rendered.length
|
|
346
|
+
? `; ${evidence.length - rendered.length} more check(s) recorded`
|
|
347
|
+
: "";
|
|
348
|
+
|
|
349
|
+
return `Verification evidence recorded: ${rendered.join("; ")}${suffix}.`;
|
|
350
|
+
}
|
|
351
|
+
|
|
318
352
|
export type CompleteMilestoneExecutorParams = Partial<CompleteMilestoneParams> & Record<string, unknown>;
|
|
319
353
|
export type SliceCompleteExecutorParams = CompleteSliceParams;
|
|
320
354
|
export type PlanMilestoneExecutorParams = PlanMilestoneParams;
|
|
@@ -350,9 +384,27 @@ export async function executeTaskComplete(
|
|
|
350
384
|
}
|
|
351
385
|
try {
|
|
352
386
|
const coerced = { ...params };
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
387
|
+
const verificationEvidence = normalizeVerificationEvidence(params.verificationEvidence);
|
|
388
|
+
coerced.verificationEvidence = verificationEvidence;
|
|
389
|
+
|
|
390
|
+
const verification = typeof params.verification === "string" ? params.verification.trim() : "";
|
|
391
|
+
if (verification.length === 0) {
|
|
392
|
+
const derived = deriveVerificationSummary(verificationEvidence);
|
|
393
|
+
if (derived) {
|
|
394
|
+
coerced.verification = derived;
|
|
395
|
+
} else if (params.blockerDiscovered === true) {
|
|
396
|
+
coerced.verification = "Not run: blocker discovered before verification.";
|
|
397
|
+
} else {
|
|
398
|
+
return {
|
|
399
|
+
content: [{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: "Error completing task: verification is required unless verificationEvidence is provided or blockerDiscovered is true.",
|
|
402
|
+
}],
|
|
403
|
+
details: { operation: "complete_task", error: "verification_required" },
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
356
408
|
|
|
357
409
|
const result = await handleCompleteTask(coerced as any, basePath);
|
|
358
410
|
if ("error" in result) {
|
|
@@ -216,6 +216,10 @@ export type EnterResult =
|
|
|
216
216
|
cause?: unknown;
|
|
217
217
|
};
|
|
218
218
|
|
|
219
|
+
export interface StrandedMilestoneAdoptionOptions {
|
|
220
|
+
mode: "worktree" | "branch";
|
|
221
|
+
}
|
|
222
|
+
|
|
219
223
|
export type ExitResult =
|
|
220
224
|
| { ok: true; merged: boolean; codeFilesChanged: boolean }
|
|
221
225
|
| { ok: false; reason: "merge-conflict" | "teardown-failed"; cause?: unknown };
|
|
@@ -237,6 +241,8 @@ export interface MergeContext {
|
|
|
237
241
|
*/
|
|
238
242
|
worktreeBasePath: string;
|
|
239
243
|
milestoneId: string;
|
|
244
|
+
/** Temporary override used while recovering stranded work. */
|
|
245
|
+
isolationModeOverride?: "worktree" | "branch" | "none";
|
|
240
246
|
/**
|
|
241
247
|
* When true, `mergeMilestoneStandalone` returns `{ merged: false,
|
|
242
248
|
* mode: "skipped" }` immediately (mirrors the single-loop guard). Default
|
|
@@ -533,6 +539,7 @@ export function _enterMilestoneCore(
|
|
|
533
539
|
deps: WorktreeLifecycleDeps,
|
|
534
540
|
milestoneId: string,
|
|
535
541
|
ctx: NotifyCtx,
|
|
542
|
+
opts: { modeOverride?: "worktree" | "branch" } = {},
|
|
536
543
|
): EnterResult {
|
|
537
544
|
if (!isValidMilestoneId(milestoneId)) {
|
|
538
545
|
debugLog("WorktreeLifecycle", {
|
|
@@ -653,7 +660,7 @@ export function _enterMilestoneCore(
|
|
|
653
660
|
// Handles the case where originalBasePath is falsy and basePath is itself
|
|
654
661
|
// a worktree path — prevents double-nested worktree paths (#3729).
|
|
655
662
|
const basePath = resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
|
|
656
|
-
const mode = getIsolationMode(basePath);
|
|
663
|
+
const mode = opts.modeOverride ?? getIsolationMode(basePath);
|
|
657
664
|
|
|
658
665
|
if (s.isolationDegraded) {
|
|
659
666
|
if (mode === "worktree") {
|
|
@@ -1298,7 +1305,9 @@ export function mergeMilestoneStandalone(
|
|
|
1298
1305
|
};
|
|
1299
1306
|
}
|
|
1300
1307
|
|
|
1301
|
-
const mode =
|
|
1308
|
+
const mode =
|
|
1309
|
+
mctx.isolationModeOverride ??
|
|
1310
|
+
getIsolationMode(originalBasePath || worktreeBasePath);
|
|
1302
1311
|
debugLog("WorktreeLifecycle", {
|
|
1303
1312
|
action: "mergeAndExit",
|
|
1304
1313
|
milestoneId,
|
|
@@ -1637,6 +1646,7 @@ export class WorktreeLifecycle {
|
|
|
1637
1646
|
originalBasePath: this.s.originalBasePath,
|
|
1638
1647
|
worktreeBasePath: this.s.basePath,
|
|
1639
1648
|
milestoneId,
|
|
1649
|
+
isolationModeOverride: this.s.strandedRecoveryIsolationMode ?? undefined,
|
|
1640
1650
|
isolationDegraded: this.s.isolationDegraded,
|
|
1641
1651
|
notify: ctx.notify,
|
|
1642
1652
|
});
|
|
@@ -1740,6 +1750,7 @@ export class WorktreeLifecycle {
|
|
|
1740
1750
|
// Rebuild GitService after merge (branch HEAD changed)
|
|
1741
1751
|
rebuildGitService(this.s, this.deps);
|
|
1742
1752
|
}
|
|
1753
|
+
this.s.strandedRecoveryIsolationMode = null;
|
|
1743
1754
|
return result;
|
|
1744
1755
|
}
|
|
1745
1756
|
|
|
@@ -1876,6 +1887,30 @@ export class WorktreeLifecycle {
|
|
|
1876
1887
|
this.s.basePath = resolvePausedResumeBasePath(base, persistedWorktreePath);
|
|
1877
1888
|
}
|
|
1878
1889
|
|
|
1890
|
+
/**
|
|
1891
|
+
* Adopt in-progress stranded work during bootstrap.
|
|
1892
|
+
*
|
|
1893
|
+
* Unlike completed-orphan recovery, this does not merge, delete, or commit.
|
|
1894
|
+
* It only moves the live session onto the branch/worktree proven by the
|
|
1895
|
+
* audit evidence, while preserving that mode for the eventual merge.
|
|
1896
|
+
*/
|
|
1897
|
+
adoptStrandedMilestone(
|
|
1898
|
+
milestoneId: string,
|
|
1899
|
+
base: string,
|
|
1900
|
+
ctx: NotifyCtx,
|
|
1901
|
+
opts: StrandedMilestoneAdoptionOptions,
|
|
1902
|
+
): EnterResult {
|
|
1903
|
+
this.adoptSessionRoot(base);
|
|
1904
|
+
this.s.strandedRecoveryIsolationMode = opts.mode;
|
|
1905
|
+
const result = _enterMilestoneCore(this.s, this.deps, milestoneId, ctx, {
|
|
1906
|
+
modeOverride: opts.mode,
|
|
1907
|
+
});
|
|
1908
|
+
if (!result.ok) {
|
|
1909
|
+
this.s.strandedRecoveryIsolationMode = null;
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1879
1914
|
/**
|
|
1880
1915
|
* Adopt an orphan worktree for a bootstrap-time merge (ADR-016 phase 2 / B4,
|
|
1881
1916
|
* issue #5622).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Lightweight worktree post-create hook runner.
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { isAbsolute, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
|
|
11
|
+
import { gsdHome } from "./gsd-home.js";
|
|
12
|
+
import { gsdRoot } from "./paths.js";
|
|
13
|
+
|
|
14
|
+
function readPreferencesObject(path: string): Record<string, unknown> | null {
|
|
15
|
+
if (!existsSync(path)) return null;
|
|
16
|
+
|
|
17
|
+
const content = readFileSync(path, "utf-8");
|
|
18
|
+
try {
|
|
19
|
+
const startMarker = content.startsWith("---\r\n") ? "---\r\n" : "---\n";
|
|
20
|
+
if (content.startsWith(startMarker)) {
|
|
21
|
+
const searchStart = startMarker.length;
|
|
22
|
+
const endIdx = content.indexOf("\n---", searchStart);
|
|
23
|
+
if (endIdx === -1) return null;
|
|
24
|
+
|
|
25
|
+
const parsed = parseYaml(content.slice(searchStart, endIdx).replace(/\r/g, ""));
|
|
26
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
27
|
+
? parsed as Record<string, unknown>
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const gitLines: string[] = [];
|
|
32
|
+
let inGitSection = false;
|
|
33
|
+
for (const rawLine of content.split("\n")) {
|
|
34
|
+
const line = rawLine.replace(/\r$/, "");
|
|
35
|
+
const heading = line.match(/^##\s+(.+)$/);
|
|
36
|
+
if (heading) {
|
|
37
|
+
inGitSection = heading[1].trim().toLowerCase().replace(/\s+/g, "_") === "git";
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (inGitSection && line.trim() && !line.trimStart().startsWith("#")) {
|
|
41
|
+
gitLines.push(line.replace(/^\s*-\s*/, ""));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (gitLines.length === 0) return null;
|
|
45
|
+
|
|
46
|
+
const parsed = parseYaml(gitLines.join("\n"));
|
|
47
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
48
|
+
? { git: parsed as Record<string, unknown> }
|
|
49
|
+
: null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractHookPath(preferences: Record<string, unknown> | null): string | null {
|
|
56
|
+
const git = preferences?.git;
|
|
57
|
+
if (!git || typeof git !== "object" || Array.isArray(git)) return null;
|
|
58
|
+
const hookPath = (git as Record<string, unknown>).worktree_post_create;
|
|
59
|
+
return typeof hookPath === "string" && hookPath.trim() ? hookPath : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveConfiguredHookPath(sourceDir: string): string | null {
|
|
63
|
+
const paths = [
|
|
64
|
+
join(homedir(), ".pi", "agent", "gsd-preferences.md"),
|
|
65
|
+
join(gsdHome(), "preferences.md"),
|
|
66
|
+
join(gsdHome(), "PREFERENCES.md"),
|
|
67
|
+
join(gsdRoot(sourceDir), "preferences.md"),
|
|
68
|
+
join(gsdRoot(sourceDir), "PREFERENCES.md"),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
let hookPath: string | null = null;
|
|
72
|
+
for (const path of paths) {
|
|
73
|
+
hookPath = extractHookPath(readPreferencesObject(path)) ?? hookPath;
|
|
74
|
+
}
|
|
75
|
+
return hookPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run the user-configured post-create hook script after worktree creation.
|
|
80
|
+
* The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
|
|
81
|
+
* Failure is non-fatal -- returns the error message or null on success.
|
|
82
|
+
*
|
|
83
|
+
* Reads git.worktree_post_create from effective global/project preferences
|
|
84
|
+
* unless hookPath is provided directly.
|
|
85
|
+
*/
|
|
86
|
+
export function runWorktreePostCreateHook(
|
|
87
|
+
sourceDir: string,
|
|
88
|
+
worktreeDir: string,
|
|
89
|
+
hookPath?: string,
|
|
90
|
+
): string | null {
|
|
91
|
+
if (hookPath === undefined) {
|
|
92
|
+
hookPath = resolveConfiguredHookPath(sourceDir) ?? undefined;
|
|
93
|
+
}
|
|
94
|
+
if (!hookPath) return null;
|
|
95
|
+
|
|
96
|
+
let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
|
|
97
|
+
if (!existsSync(resolved)) {
|
|
98
|
+
return `Worktree post-create hook not found: ${resolved}`;
|
|
99
|
+
}
|
|
100
|
+
if (process.platform === "win32") {
|
|
101
|
+
try {
|
|
102
|
+
resolved = realpathSync.native(resolved);
|
|
103
|
+
} catch {
|
|
104
|
+
// Keep the original path; the exec error below will include the failure.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
|
|
110
|
+
execFileSync(resolved, [], {
|
|
111
|
+
cwd: worktreeDir,
|
|
112
|
+
env: {
|
|
113
|
+
...process.env,
|
|
114
|
+
SOURCE_DIR: sourceDir,
|
|
115
|
+
WORKTREE_DIR: worktreeDir,
|
|
116
|
+
},
|
|
117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
118
|
+
encoding: "utf-8",
|
|
119
|
+
timeout: 30_000,
|
|
120
|
+
shell: needsShell,
|
|
121
|
+
});
|
|
122
|
+
return null;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
return `Worktree post-create hook failed: ${msg}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -16,6 +16,47 @@ export const CUSTOM_SEARCH_TOOL_NAMES = ["search-the-web", "search_and_read", "g
|
|
|
16
16
|
|
|
17
17
|
/** Thinking block types that require signature validation by the API */
|
|
18
18
|
const THINKING_TYPES = new Set(["thinking", "redacted_thinking"]);
|
|
19
|
+
const NATIVE_SERVER_TOOL_USE_TYPES = new Set([
|
|
20
|
+
"server_tool_use",
|
|
21
|
+
"serverToolUse",
|
|
22
|
+
]);
|
|
23
|
+
const NATIVE_WEB_SEARCH_RESULT_TYPES = new Set([
|
|
24
|
+
"web_search_tool_result",
|
|
25
|
+
"webSearchResult",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function nativeServerToolId(block: any): string | undefined {
|
|
29
|
+
if (!NATIVE_SERVER_TOOL_USE_TYPES.has(block?.type)) return undefined;
|
|
30
|
+
return typeof block.id === "string" ? block.id : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nativeWebSearchResultId(block: any): string | undefined {
|
|
34
|
+
if (!NATIVE_WEB_SEARCH_RESULT_TYPES.has(block?.type)) return undefined;
|
|
35
|
+
const id = block.type === "webSearchResult" ? block.toolUseId : block.tool_use_id;
|
|
36
|
+
return typeof id === "string" ? id : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasCompleteNativeServerToolReplay(content: any[]): boolean {
|
|
40
|
+
const pendingToolUseIds = new Set<string>();
|
|
41
|
+
let sawNativeServerToolUse = false;
|
|
42
|
+
|
|
43
|
+
for (const block of content) {
|
|
44
|
+
const toolUseId = nativeServerToolId(block);
|
|
45
|
+
if (toolUseId !== undefined) {
|
|
46
|
+
if (pendingToolUseIds.has(toolUseId)) return false;
|
|
47
|
+
sawNativeServerToolUse = true;
|
|
48
|
+
pendingToolUseIds.add(toolUseId);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resultId = nativeWebSearchResultId(block);
|
|
53
|
+
if (resultId !== undefined) {
|
|
54
|
+
if (!pendingToolUseIds.delete(resultId)) return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return sawNativeServerToolUse && pendingToolUseIds.size === 0;
|
|
59
|
+
}
|
|
19
60
|
|
|
20
61
|
/**
|
|
21
62
|
* Providers whose Anthropic-Messages endpoint is known to accept the native
|
|
@@ -36,6 +77,11 @@ const NATIVE_WEB_SEARCH_PROVIDERS = new Set([
|
|
|
36
77
|
"vercel-ai-gateway",
|
|
37
78
|
]);
|
|
38
79
|
|
|
80
|
+
function looksLikeAnthropicModelName(modelName: string): boolean {
|
|
81
|
+
const normalized = modelName.trim().toLowerCase();
|
|
82
|
+
return normalized.startsWith("claude-") || normalized.startsWith("anthropic/claude-");
|
|
83
|
+
}
|
|
84
|
+
|
|
39
85
|
/**
|
|
40
86
|
* True when the model is an Anthropic-shaped transport AND the provider is
|
|
41
87
|
* known to accept the native `web_search_20250305` tool. Gate both on api
|
|
@@ -89,11 +135,10 @@ export interface NativeSearchPI {
|
|
|
89
135
|
* those blocks. The Anthropic API detects the modification and rejects the
|
|
90
136
|
* request with "thinking blocks cannot be modified."
|
|
91
137
|
*
|
|
92
|
-
* Fix: Remove thinking blocks from
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* current turn regardless.
|
|
138
|
+
* Fix: Remove thinking blocks only from assistant messages that do not carry
|
|
139
|
+
* native server-tool blocks. Complete native server-tool histories can be
|
|
140
|
+
* replayed as-is; stripping thinking from those messages is itself a latest
|
|
141
|
+
* assistant message modification.
|
|
97
142
|
*/
|
|
98
143
|
export function stripThinkingFromHistory(
|
|
99
144
|
messages: Array<Record<string, unknown>>
|
|
@@ -103,6 +148,9 @@ export function stripThinkingFromHistory(
|
|
|
103
148
|
|
|
104
149
|
const content = msg.content;
|
|
105
150
|
if (!Array.isArray(content)) continue;
|
|
151
|
+
if (hasCompleteNativeServerToolReplay(content)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
106
154
|
|
|
107
155
|
msg.content = content.filter(
|
|
108
156
|
(block: any) => !THINKING_TYPES.has(block?.type)
|
|
@@ -180,6 +228,8 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
|
|
|
180
228
|
// The model name heuristic is needed for session restores where
|
|
181
229
|
// modelsAreEqual suppresses model_select AND the SDK doesn't pass model.
|
|
182
230
|
const eventModel = event.model as { provider?: string; api?: string } | undefined;
|
|
231
|
+
const payloadModelName = typeof payload.model === "string" ? payload.model : "";
|
|
232
|
+
const payloadLooksAnthropic = payloadModelName ? looksLikeAnthropicModelName(payloadModelName) : undefined;
|
|
183
233
|
let isAnthropic: boolean;
|
|
184
234
|
if (eventModel?.api || eventModel?.provider) {
|
|
185
235
|
// Preferred path: gate on api shape + provider allowlist. Both fields
|
|
@@ -188,12 +238,14 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
|
|
|
188
238
|
// (#444 regression) or minimax-served Claude-compat as Anthropic (#4492).
|
|
189
239
|
isAnthropic = supportsNativeWebSearch(eventModel);
|
|
190
240
|
} else if (modelSelectFired) {
|
|
191
|
-
|
|
241
|
+
// The model_select flag can be stale if the next request omits event.model
|
|
242
|
+
// after a provider switch. A concrete non-Claude payload must win so an
|
|
243
|
+
// Anthropic-only tool never leaks into OpenAI Responses requests.
|
|
244
|
+
isAnthropic = isAnthropicProvider && payloadLooksAnthropic !== false;
|
|
192
245
|
} else {
|
|
193
246
|
// Last resort: session-restore paths where the SDK doesn't pass model.
|
|
194
247
|
// The model-name prefix is best-effort and assumes direct Anthropic.
|
|
195
|
-
|
|
196
|
-
isAnthropic = modelName.startsWith("claude-");
|
|
248
|
+
isAnthropic = payloadLooksAnthropic === true;
|
|
197
249
|
}
|
|
198
250
|
if (!isAnthropic) return;
|
|
199
251
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join, resolve as resolvePath, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
function hasPnpmPath(value: string | undefined): boolean {
|
|
5
|
+
if (!value) return false
|
|
6
|
+
const normalized = value.replace(/\\/g, '/').toLowerCase()
|
|
7
|
+
return (
|
|
8
|
+
normalized.includes('/.pnpm/') ||
|
|
9
|
+
normalized.endsWith('/pnpm') ||
|
|
10
|
+
normalized.endsWith('/pnpm.cjs') ||
|
|
11
|
+
normalized.endsWith('/pnpm.js')
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pathStartsWith(pathValue: string | undefined, dir: string): boolean {
|
|
16
|
+
if (!pathValue) return false
|
|
17
|
+
const resolvedPath = resolvePath(pathValue)
|
|
18
|
+
const resolvedDir = resolvePath(dir)
|
|
19
|
+
return resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Shared by update-check.ts and gsd command handlers. The JS installer keeps a
|
|
23
|
+
// parallel copy because it runs before TypeScript output exists.
|
|
24
|
+
export function isPnpmInstall(
|
|
25
|
+
argv1: string | undefined = process.argv[1],
|
|
26
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
27
|
+
): boolean {
|
|
28
|
+
if (env.npm_config_user_agent?.startsWith('pnpm/')) return true
|
|
29
|
+
if (hasPnpmPath(env.npm_execpath)) return true
|
|
30
|
+
if (hasPnpmPath(argv1)) return true
|
|
31
|
+
if (!argv1) return false
|
|
32
|
+
|
|
33
|
+
const pnpmBinDirs: string[] = []
|
|
34
|
+
if (env.PNPM_HOME) pnpmBinDirs.push(env.PNPM_HOME)
|
|
35
|
+
pnpmBinDirs.push(join(homedir(), 'Library', 'pnpm'))
|
|
36
|
+
pnpmBinDirs.push(join(homedir(), '.local', 'share', 'pnpm'))
|
|
37
|
+
|
|
38
|
+
return pnpmBinDirs.some((dir) => pathStartsWith(argv1, dir) || pathStartsWith(env.npm_execpath, dir))
|
|
39
|
+
}
|