gsd-pi 2.42.0-dev.97e9e30 → 2.42.0-dev.eedc83f

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.
Files changed (167) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +15 -1
  3. package/dist/resource-loader.js +39 -6
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
  5. package/dist/resources/extensions/gsd/auto-prompts.js +1 -1
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
  7. package/dist/resources/extensions/gsd/detection.js +19 -0
  8. package/dist/resources/extensions/gsd/doctor-checks.js +31 -1
  9. package/dist/resources/extensions/gsd/doctor-providers.js +10 -0
  10. package/dist/resources/extensions/gsd/forensics.js +84 -0
  11. package/dist/resources/extensions/gsd/git-constants.js +1 -0
  12. package/dist/resources/extensions/gsd/git-service.js +68 -2
  13. package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
  14. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  15. package/dist/resources/extensions/gsd/preferences.js +59 -8
  16. package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
  17. package/dist/resources/extensions/gsd/repo-identity.js +46 -5
  18. package/dist/resources/extensions/gsd/service-tier.js +13 -4
  19. package/dist/resources/extensions/gsd/session-lock.js +2 -2
  20. package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
  21. package/dist/resources/extensions/mcp-client/index.js +2 -1
  22. package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  44. package/dist/web/standalone/.next/server/app/index.html +1 -1
  45. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  52. package/dist/web/standalone/.next/server/chunks/229.js +2 -2
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/dist/web-mode.d.ts +2 -0
  57. package/dist/web-mode.js +40 -4
  58. package/package.json +1 -1
  59. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  60. package/packages/pi-agent-core/dist/agent.js +2 -0
  61. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  62. package/packages/pi-agent-core/dist/types.d.ts +6 -0
  63. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  64. package/packages/pi-agent-core/dist/types.js.map +1 -1
  65. package/packages/pi-agent-core/src/agent.test.ts +53 -0
  66. package/packages/pi-agent-core/src/agent.ts +3 -0
  67. package/packages/pi-agent-core/src/types.ts +6 -0
  68. package/packages/pi-agent-core/tsconfig.json +1 -1
  69. package/packages/pi-ai/dist/models.d.ts +5 -3
  70. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
  72. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  73. package/packages/pi-ai/dist/models.generated.js +1135 -1588
  74. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  75. package/packages/pi-ai/dist/models.js.map +1 -1
  76. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
  78. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  79. package/packages/pi-ai/scripts/generate-models.ts +1543 -0
  80. package/packages/pi-ai/src/models.generated.ts +1140 -1593
  81. package/packages/pi-ai/src/models.ts +7 -4
  82. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
  83. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
  85. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
  87. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
  89. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
  91. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
  102. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
  104. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
  106. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
  108. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
  111. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +9 -6
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +30 -10
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  121. package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
  122. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
  123. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
  124. package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
  125. package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
  126. package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
  127. package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
  128. package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
  129. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
  130. package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
  131. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
  132. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
  133. package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
  134. package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
  135. package/src/resources/extensions/gsd/auto-prompts.ts +1 -1
  136. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
  137. package/src/resources/extensions/gsd/detection.ts +19 -0
  138. package/src/resources/extensions/gsd/doctor-checks.ts +32 -1
  139. package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
  140. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  141. package/src/resources/extensions/gsd/forensics.ts +92 -0
  142. package/src/resources/extensions/gsd/git-constants.ts +1 -0
  143. package/src/resources/extensions/gsd/git-service.ts +71 -2
  144. package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
  145. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  146. package/src/resources/extensions/gsd/preferences.ts +62 -6
  147. package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
  148. package/src/resources/extensions/gsd/repo-identity.ts +48 -5
  149. package/src/resources/extensions/gsd/service-tier.ts +17 -4
  150. package/src/resources/extensions/gsd/session-lock.ts +2 -2
  151. package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
  152. package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +48 -0
  153. package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
  154. package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
  155. package/src/resources/extensions/gsd/tests/git-service.test.ts +49 -0
  156. package/src/resources/extensions/gsd/tests/journal.test.ts +82 -127
  157. package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
  158. package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
  159. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
  160. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
  161. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
  162. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
  163. package/src/resources/extensions/gsd/worktree-resolver.ts +2 -2
  164. package/src/resources/extensions/mcp-client/index.ts +5 -1
  165. package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
  166. /package/dist/web/standalone/.next/static/{PXrI5DoWsm7rwAVnEU2rD → JUBX5FUR73jiViQU5a-Cx}/_buildManifest.js +0 -0
  167. /package/dist/web/standalone/.next/static/{PXrI5DoWsm7rwAVnEU2rD → JUBX5FUR73jiViQU5a-Cx}/_ssgManifest.js +0 -0
@@ -7,7 +7,7 @@
7
7
  * rather than hard-coding package.json / src/ only.
8
8
  */
9
9
 
10
- import test from "node:test";
10
+ import { describe, test, beforeEach, afterEach } from "node:test";
11
11
  import assert from "node:assert/strict";
12
12
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
13
13
  import { join } from "node:path";
@@ -67,112 +67,69 @@ test("PROJECT_FILES is exported and contains expected multi-ecosystem entries",
67
67
  assert.ok(PROJECT_FILES.includes("Package.swift"), "includes Swift marker");
68
68
  });
69
69
 
70
- test("health check passes for Rust project (Cargo.toml, no package.json)", () => {
71
- const dir = createGitRepo();
72
- try {
70
+ describe("health check with git repo", () => {
71
+ let dir: string;
72
+ beforeEach(() => { dir = createGitRepo(); });
73
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
74
+
75
+ test("health check passes for Rust project (Cargo.toml, no package.json)", () => {
73
76
  writeFileSync(join(dir, "Cargo.toml"), "[package]\nname = \"test\"\n");
74
77
  mkdirSync(join(dir, "crates"), { recursive: true });
75
78
  assert.ok(wouldPassHealthCheck(dir, existsSync), "Rust project should pass health check");
76
- } finally {
77
- rmSync(dir, { recursive: true, force: true });
78
- }
79
- });
79
+ });
80
80
 
81
- test("health check passes for Go project (go.mod, no package.json)", () => {
82
- const dir = createGitRepo();
83
- try {
81
+ test("health check passes for Go project (go.mod, no package.json)", () => {
84
82
  writeFileSync(join(dir, "go.mod"), "module example.com/test\n\ngo 1.21\n");
85
83
  assert.ok(wouldPassHealthCheck(dir, existsSync), "Go project should pass health check");
86
- } finally {
87
- rmSync(dir, { recursive: true, force: true });
88
- }
89
- });
84
+ });
90
85
 
91
- test("health check passes for Python project (pyproject.toml, no package.json)", () => {
92
- const dir = createGitRepo();
93
- try {
86
+ test("health check passes for Python project (pyproject.toml, no package.json)", () => {
94
87
  writeFileSync(join(dir, "pyproject.toml"), "[project]\nname = \"test\"\n");
95
88
  assert.ok(wouldPassHealthCheck(dir, existsSync), "Python project should pass health check");
96
- } finally {
97
- rmSync(dir, { recursive: true, force: true });
98
- }
99
- });
89
+ });
100
90
 
101
- test("health check passes for Java project (pom.xml, no package.json)", () => {
102
- const dir = createGitRepo();
103
- try {
91
+ test("health check passes for Java project (pom.xml, no package.json)", () => {
104
92
  writeFileSync(join(dir, "pom.xml"), "<project></project>\n");
105
93
  assert.ok(wouldPassHealthCheck(dir, existsSync), "Java project should pass health check");
106
- } finally {
107
- rmSync(dir, { recursive: true, force: true });
108
- }
109
- });
94
+ });
110
95
 
111
- test("health check passes for Swift project (Package.swift, no package.json)", () => {
112
- const dir = createGitRepo();
113
- try {
96
+ test("health check passes for Swift project (Package.swift, no package.json)", () => {
114
97
  writeFileSync(join(dir, "Package.swift"), "// swift-tools-version:5.7\n");
115
98
  assert.ok(wouldPassHealthCheck(dir, existsSync), "Swift project should pass health check");
116
- } finally {
117
- rmSync(dir, { recursive: true, force: true });
118
- }
119
- });
99
+ });
120
100
 
121
- test("health check passes for C/C++ project (CMakeLists.txt, no package.json)", () => {
122
- const dir = createGitRepo();
123
- try {
101
+ test("health check passes for C/C++ project (CMakeLists.txt, no package.json)", () => {
124
102
  writeFileSync(join(dir, "CMakeLists.txt"), "cmake_minimum_required(VERSION 3.20)\n");
125
103
  assert.ok(wouldPassHealthCheck(dir, existsSync), "C/C++ project should pass health check");
126
- } finally {
127
- rmSync(dir, { recursive: true, force: true });
128
- }
129
- });
104
+ });
130
105
 
131
- test("health check passes for Elixir project (mix.exs, no package.json)", () => {
132
- const dir = createGitRepo();
133
- try {
106
+ test("health check passes for Elixir project (mix.exs, no package.json)", () => {
134
107
  writeFileSync(join(dir, "mix.exs"), "defmodule Test.MixProject do\nend\n");
135
108
  assert.ok(wouldPassHealthCheck(dir, existsSync), "Elixir project should pass health check");
136
- } finally {
137
- rmSync(dir, { recursive: true, force: true });
138
- }
139
- });
109
+ });
140
110
 
141
- test("health check passes for JS project (package.json, backward compat)", () => {
142
- const dir = createGitRepo();
143
- try {
111
+ test("health check passes for JS project (package.json, backward compat)", () => {
144
112
  writeFileSync(join(dir, "package.json"), '{"name":"test"}\n');
145
113
  assert.ok(wouldPassHealthCheck(dir, existsSync), "JS project should pass health check");
146
- } finally {
147
- rmSync(dir, { recursive: true, force: true });
148
- }
149
- });
114
+ });
150
115
 
151
- test("health check passes for src/-only project (backward compat)", () => {
152
- const dir = createGitRepo();
153
- try {
116
+ test("health check passes for src/-only project (backward compat)", () => {
154
117
  mkdirSync(join(dir, "src"), { recursive: true });
155
118
  assert.ok(wouldPassHealthCheck(dir, existsSync), "src/-only project should pass health check");
156
- } finally {
157
- rmSync(dir, { recursive: true, force: true });
158
- }
119
+ });
120
+
121
+ test("health check fails for empty git repo with no project files", () => {
122
+ assert.ok(!wouldPassHealthCheck(dir, existsSync), "empty git repo should fail health check");
123
+ });
159
124
  });
160
125
 
161
- test("health check fails for directory with no .git", () => {
162
- const dir = mkdtempSync(join(tmpdir(), "wt-dispatch-test-nogit-"));
163
- try {
126
+ describe("health check without git repo", () => {
127
+ let dir: string;
128
+ beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "wt-dispatch-test-nogit-")); });
129
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
130
+
131
+ test("health check fails for directory with no .git", () => {
164
132
  writeFileSync(join(dir, "Cargo.toml"), "[package]\nname = \"test\"\n");
165
133
  assert.ok(!wouldPassHealthCheck(dir, existsSync), "no-git directory should fail health check");
166
- } finally {
167
- rmSync(dir, { recursive: true, force: true });
168
- }
169
- });
170
-
171
- test("health check fails for empty git repo with no project files", () => {
172
- const dir = createGitRepo();
173
- try {
174
- assert.ok(!wouldPassHealthCheck(dir, existsSync), "empty git repo should fail health check");
175
- } finally {
176
- rmSync(dir, { recursive: true, force: true });
177
- }
134
+ });
178
135
  });
@@ -1,4 +1,4 @@
1
- import test from "node:test";
1
+ import { describe, test, beforeEach, afterEach } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -73,9 +73,12 @@ test("worktreeBranchName formats branch name", () => {
73
73
 
74
74
  // ─── createWorktree ───────────────────────────────────────────────────────────
75
75
 
76
- test("createWorktree creates worktree with correct metadata", () => {
77
- const base = makeBaseRepo();
78
- try {
76
+ describe("createWorktree", () => {
77
+ let base: string;
78
+ beforeEach(() => { base = makeBaseRepo(); });
79
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
80
+
81
+ test("creates worktree with correct metadata", () => {
79
82
  const info = createWorktree(base, "feature-x");
80
83
  assert.strictEqual(info.name, "feature-x", "name should match");
81
84
  assert.strictEqual(info.branch, "worktree/feature-x", "branch should be prefixed");
@@ -88,80 +91,82 @@ test("createWorktree creates worktree with correct metadata", () => {
88
91
  );
89
92
  const branches = run("git branch", base);
90
93
  assert.ok(branches.includes("worktree/feature-x"), "branch should be created in base repo");
91
- } finally {
92
- rmSync(base, { recursive: true, force: true });
93
- }
94
- });
94
+ });
95
95
 
96
- test("createWorktree rejects duplicate name", () => {
97
- const { base } = makeRepoWithWorktree("feature-x");
98
- try {
96
+ test("rejects invalid name", () => {
99
97
  assert.throws(
100
- () => createWorktree(base, "feature-x"),
98
+ () => createWorktree(base, "bad name!"),
101
99
  (err: Error) => {
102
100
  assert.ok(
103
- err.message.includes("already exists"),
104
- `expected "already exists" in error, got: ${err.message}`,
101
+ err.message.includes("Invalid worktree name"),
102
+ `expected "Invalid worktree name" in error, got: ${err.message}`,
105
103
  );
106
104
  return true;
107
105
  },
108
- "should throw on duplicate worktree name",
106
+ "should throw on invalid worktree name",
109
107
  );
110
- } finally {
111
- rmSync(base, { recursive: true, force: true });
112
- }
108
+ });
113
109
  });
114
110
 
115
- test("createWorktree rejects invalid name", () => {
116
- const base = makeBaseRepo();
117
- try {
111
+ describe("createWorktree duplicate rejection", () => {
112
+ let base: string;
113
+ beforeEach(() => {
114
+ const repo = makeRepoWithWorktree("feature-x");
115
+ base = repo.base;
116
+ });
117
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
118
+
119
+ test("rejects duplicate name", () => {
118
120
  assert.throws(
119
- () => createWorktree(base, "bad name!"),
121
+ () => createWorktree(base, "feature-x"),
120
122
  (err: Error) => {
121
123
  assert.ok(
122
- err.message.includes("Invalid worktree name"),
123
- `expected "Invalid worktree name" in error, got: ${err.message}`,
124
+ err.message.includes("already exists"),
125
+ `expected "already exists" in error, got: ${err.message}`,
124
126
  );
125
127
  return true;
126
128
  },
127
- "should throw on invalid worktree name",
129
+ "should throw on duplicate worktree name",
128
130
  );
129
- } finally {
130
- rmSync(base, { recursive: true, force: true });
131
- }
131
+ });
132
132
  });
133
133
 
134
134
  // ─── listWorktrees ────────────────────────────────────────────────────────────
135
135
 
136
- test("listWorktrees returns active worktrees", () => {
137
- const { base } = makeRepoWithWorktree("feature-x");
138
- try {
136
+ describe("listWorktrees", () => {
137
+ let base: string;
138
+ beforeEach(() => {
139
+ const repo = makeRepoWithWorktree("feature-x");
140
+ base = repo.base;
141
+ });
142
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
143
+
144
+ test("returns active worktrees", () => {
139
145
  const list = listWorktrees(base);
140
146
  assert.strictEqual(list.length, 1, "should list exactly one worktree");
141
147
  assert.strictEqual(list[0]!.name, "feature-x", "name should match");
142
148
  assert.strictEqual(list[0]!.branch, "worktree/feature-x", "branch should match");
143
149
  assert.ok(list[0]!.exists, "exists flag should be true");
144
- } finally {
145
- rmSync(base, { recursive: true, force: true });
146
- }
147
- });
150
+ });
148
151
 
149
- test("listWorktrees returns empty after removal", () => {
150
- const { base } = makeRepoWithWorktree("feature-x");
151
- try {
152
+ test("returns empty after removal", () => {
152
153
  removeWorktree(base, "feature-x");
153
154
  const list = listWorktrees(base);
154
155
  assert.strictEqual(list.length, 0, "should have no worktrees after removal");
155
- } finally {
156
- rmSync(base, { recursive: true, force: true });
157
- }
156
+ });
158
157
  });
159
158
 
160
159
  // ─── diffWorktreeGSD ─────────────────────────────────────────────────────────
161
160
 
162
- test("diffWorktreeGSD detects added and modified GSD files", () => {
163
- const { base } = makeRepoWithChanges("feature-x");
164
- try {
161
+ describe("diffWorktreeGSD and getWorktreeGSDDiff", () => {
162
+ let base: string;
163
+ beforeEach(() => {
164
+ const repo = makeRepoWithChanges("feature-x");
165
+ base = repo.base;
166
+ });
167
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
168
+
169
+ test("detects added and modified GSD files", () => {
165
170
  const diff = diffWorktreeGSD(base, "feature-x");
166
171
  assert.ok(diff.added.length > 0, "should have added files");
167
172
  assert.ok(
@@ -174,58 +179,60 @@ test("diffWorktreeGSD detects added and modified GSD files", () => {
174
179
  "M001 roadmap should be in modified files",
175
180
  );
176
181
  assert.strictEqual(diff.removed.length, 0, "should have no removed files");
177
- } finally {
178
- rmSync(base, { recursive: true, force: true });
179
- }
180
- });
181
-
182
- // ─── getWorktreeGSDDiff ───────────────────────────────────────────────────────
182
+ });
183
183
 
184
- test("getWorktreeGSDDiff returns patch content", () => {
185
- const { base } = makeRepoWithChanges("feature-x");
186
- try {
184
+ test("returns patch content", () => {
187
185
  const fullDiff = getWorktreeGSDDiff(base, "feature-x");
188
186
  assert.ok(fullDiff.includes("M002"), "diff should mention M002");
189
187
  assert.ok(fullDiff.includes("updated"), "diff should mention the update");
190
- } finally {
191
- rmSync(base, { recursive: true, force: true });
192
- }
188
+ });
193
189
  });
194
190
 
195
191
  // ─── getWorktreeLog ───────────────────────────────────────────────────────────
196
192
 
197
- test("getWorktreeLog shows commits", () => {
198
- const { base } = makeRepoWithChanges("feature-x");
199
- try {
193
+ describe("getWorktreeLog", () => {
194
+ let base: string;
195
+ beforeEach(() => {
196
+ const repo = makeRepoWithChanges("feature-x");
197
+ base = repo.base;
198
+ });
199
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
200
+
201
+ test("shows commits", () => {
200
202
  const log = getWorktreeLog(base, "feature-x");
201
203
  assert.ok(log.includes("add M002"), "log should include the commit message");
202
- } finally {
203
- rmSync(base, { recursive: true, force: true });
204
- }
204
+ });
205
205
  });
206
206
 
207
207
  // ─── removeWorktree ───────────────────────────────────────────────────────────
208
208
 
209
- test("removeWorktree removes directory and branch", () => {
210
- const { base, wtPath } = makeRepoWithWorktree("feature-x");
211
- try {
209
+ describe("removeWorktree", () => {
210
+ let base: string;
211
+ let wtPath: string;
212
+ beforeEach(() => {
213
+ const repo = makeRepoWithWorktree("feature-x");
214
+ base = repo.base;
215
+ wtPath = repo.wtPath;
216
+ });
217
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
218
+
219
+ test("removes directory and branch", () => {
212
220
  removeWorktree(base, "feature-x", { deleteBranch: true });
213
221
  assert.ok(!existsSync(wtPath), "worktree directory should be gone");
214
222
  const branches = run("git branch", base);
215
223
  assert.ok(!branches.includes("worktree/feature-x"), "branch should be deleted");
216
- } finally {
217
- rmSync(base, { recursive: true, force: true });
218
- }
224
+ });
219
225
  });
220
226
 
221
- test("removeWorktree on missing worktree does not throw", () => {
222
- const base = makeBaseRepo();
223
- try {
227
+ describe("removeWorktree missing worktree", () => {
228
+ let base: string;
229
+ beforeEach(() => { base = makeBaseRepo(); });
230
+ afterEach(() => { rmSync(base, { recursive: true, force: true }); });
231
+
232
+ test("on missing worktree does not throw", () => {
224
233
  assert.doesNotThrow(
225
234
  () => removeWorktree(base, "nonexistent"),
226
235
  "should not throw when worktree does not exist",
227
236
  );
228
- } finally {
229
- rmSync(base, { recursive: true, force: true });
230
- }
237
+ });
231
238
  });
@@ -410,10 +410,10 @@ export class WorktreeResolver {
410
410
  });
411
411
  // Surface a clear, actionable error. The worktree and milestone branch are
412
412
  // intentionally preserved — nothing has been deleted. The user can retry
413
- // /complete-milestone or merge manually once the underlying issue is fixed
413
+ // /gsd dispatch complete-milestone or merge manually once the underlying issue is fixed
414
414
  // (e.g. checkout to wrong branch, unresolved conflicts). (#1668)
415
415
  ctx.notify(
416
- `Milestone merge failed: ${msg}. Your worktree and milestone branch are preserved — retry /complete-milestone or merge manually.`,
416
+ `Milestone merge failed: ${msg}. Your worktree and milestone branch are preserved — retry /gsd dispatch complete-milestone or merge manually.`,
417
417
  "warning",
418
418
  );
419
419
 
@@ -149,7 +149,11 @@ async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client>
149
149
  stderr: "pipe",
150
150
  });
151
151
  } else if (config.transport === "http" && config.url) {
152
- transport = new StreamableHTTPClientTransport(new URL(config.url));
152
+ const resolvedUrl = config.url.replace(
153
+ /\$\{([^}]+)\}/g,
154
+ (_, name) => process.env[name] ?? "",
155
+ );
156
+ transport = new StreamableHTTPClientTransport(new URL(resolvedUrl));
153
157
  } else {
154
158
  throw new Error(`Server "${name}" has unsupported transport: ${config.transport}`);
155
159
  }
@@ -398,16 +398,16 @@ export function registerSearchTool(pi: ExtensionAPI) {
398
398
  // with brief interruptions every MAX_CONSECUTIVE_DUPES+1 calls.
399
399
  if (cacheKey === lastSearchKey) {
400
400
  consecutiveDupeCount++;
401
- if (consecutiveDupeCount >= MAX_CONSECUTIVE_DUPES) {
401
+ if (consecutiveDupeCount > MAX_CONSECUTIVE_DUPES) {
402
402
  return {
403
- content: [{ type: "text" as const, text: `⚠️ Search loop detected: the query "${params.query}" has been searched ${consecutiveDupeCount + 1} times consecutively with identical results. The information you need is already in the previous search results above. Stop searching and use those results to proceed with your task.` }],
403
+ content: [{ type: "text" as const, text: `⚠️ Search loop detected: the query "${params.query}" has been searched ${consecutiveDupeCount} times consecutively with identical results. The information you need is already in the previous search results above. Stop searching and use those results to proceed with your task.` }],
404
404
  isError: true,
405
405
  details: { errorKind: "search_loop", error: "Consecutive duplicate search detected" } satisfies Partial<SearchDetails>,
406
406
  };
407
407
  }
408
408
  } else {
409
409
  lastSearchKey = cacheKey;
410
- consecutiveDupeCount = 0;
410
+ consecutiveDupeCount = 1;
411
411
  }
412
412
 
413
413
  const cached = searchCache.get(cacheKey);