gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc

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 (217) hide show
  1. package/README.md +15 -11
  2. package/dist/app-paths.js +1 -1
  3. package/dist/extension-registry.js +2 -2
  4. package/dist/remote-questions-config.js +2 -2
  5. package/dist/resource-loader.js +34 -1
  6. package/dist/resources/extensions/browser-tools/index.js +3 -1
  7. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  8. package/dist/resources/extensions/env-utils.js +29 -0
  9. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  10. package/dist/resources/extensions/github-sync/cli.js +284 -0
  11. package/dist/resources/extensions/github-sync/index.js +73 -0
  12. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  13. package/dist/resources/extensions/github-sync/sync.js +424 -0
  14. package/dist/resources/extensions/github-sync/templates.js +118 -0
  15. package/dist/resources/extensions/github-sync/types.js +7 -0
  16. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  18. package/dist/resources/extensions/gsd/auto-loop.js +636 -594
  19. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  20. package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
  21. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
  23. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  24. package/dist/resources/extensions/gsd/auto.js +143 -96
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  27. package/dist/resources/extensions/gsd/commands.js +4 -2
  28. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  29. package/dist/resources/extensions/gsd/detection.js +1 -2
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  31. package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
  32. package/dist/resources/extensions/gsd/doctor.js +20 -1
  33. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +48 -9
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/git-service.js +30 -12
  38. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  39. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  40. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  41. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  42. package/dist/resources/extensions/gsd/index.js +24 -20
  43. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  44. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  45. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  46. package/dist/resources/extensions/gsd/paths.js +3 -0
  47. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  49. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  50. package/dist/resources/extensions/gsd/preferences.js +22 -11
  51. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  52. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  54. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  55. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
  56. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  57. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  59. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  62. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  63. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  64. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  68. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  69. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  70. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  71. package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
  72. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  73. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  74. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  75. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  76. package/dist/resources/extensions/gsd/state.js +42 -23
  77. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  78. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  79. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  80. package/dist/resources/extensions/mcp-client/index.js +14 -1
  81. package/dist/resources/extensions/remote-questions/status.js +4 -1
  82. package/dist/resources/extensions/remote-questions/store.js +4 -1
  83. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  84. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  85. package/dist/resources/extensions/subagent/isolation.js +2 -1
  86. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  87. package/package.json +1 -1
  88. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  89. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  90. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  91. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  93. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  95. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  97. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  99. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/index.js +1 -1
  101. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  102. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  103. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  104. package/packages/pi-coding-agent/src/index.ts +1 -0
  105. package/src/resources/extensions/browser-tools/index.ts +3 -0
  106. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  107. package/src/resources/extensions/env-utils.ts +31 -0
  108. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  109. package/src/resources/extensions/github-sync/cli.ts +364 -0
  110. package/src/resources/extensions/github-sync/index.ts +93 -0
  111. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  112. package/src/resources/extensions/github-sync/sync.ts +556 -0
  113. package/src/resources/extensions/github-sync/templates.ts +183 -0
  114. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  115. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  116. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  117. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  118. package/src/resources/extensions/github-sync/types.ts +47 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  120. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  121. package/src/resources/extensions/gsd/auto-loop.ts +526 -545
  122. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  123. package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
  124. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  125. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
  126. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  127. package/src/resources/extensions/gsd/auto.ts +139 -101
  128. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  129. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  130. package/src/resources/extensions/gsd/commands.ts +5 -3
  131. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  132. package/src/resources/extensions/gsd/detection.ts +2 -2
  133. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  134. package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
  135. package/src/resources/extensions/gsd/doctor.ts +22 -1
  136. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  137. package/src/resources/extensions/gsd/export.ts +1 -1
  138. package/src/resources/extensions/gsd/files.ts +51 -11
  139. package/src/resources/extensions/gsd/forensics.ts +1 -1
  140. package/src/resources/extensions/gsd/git-service.ts +44 -10
  141. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  142. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  143. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  144. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  145. package/src/resources/extensions/gsd/index.ts +24 -17
  146. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  147. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  149. package/src/resources/extensions/gsd/paths.ts +4 -0
  150. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  151. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  152. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  153. package/src/resources/extensions/gsd/preferences.ts +25 -11
  154. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  155. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  156. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  157. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  158. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
  159. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  160. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  161. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  162. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  165. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  166. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  170. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  174. package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
  175. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  176. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  177. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  178. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  179. package/src/resources/extensions/gsd/state.ts +39 -21
  180. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  181. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  182. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  183. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  184. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  186. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  187. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  188. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  189. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  190. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  191. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  193. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  194. package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
  195. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  196. package/src/resources/extensions/gsd/types.ts +18 -1
  197. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  198. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  199. package/src/resources/extensions/mcp-client/index.ts +17 -1
  200. package/src/resources/extensions/remote-questions/status.ts +5 -1
  201. package/src/resources/extensions/remote-questions/store.ts +5 -1
  202. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  203. package/src/resources/extensions/shared/frontmatter.ts +1 -1
  204. package/src/resources/extensions/subagent/isolation.ts +3 -1
  205. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  206. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  207. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  208. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  209. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  210. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  211. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  212. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  213. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  214. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  215. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  216. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  217. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
package/README.md CHANGED
@@ -24,21 +24,25 @@ One command. Walk away. Come back to a built project with clean git history.
24
24
 
25
25
  ---
26
26
 
27
- ## What's New in v2.37
27
+ ## What's New in v2.38
28
28
 
29
- - **cmux integration** — sidebar status, progress bars, and notifications for [cmux](https://cmux.com) terminal multiplexer users
30
- - **Redesigned dashboard** — two-column layout with redesigned widget
31
- - **Search budget enforcement** — session-level search budget prevents unbounded native web search
29
+ - **Reactive task execution (ADR-004)** — graph-derived parallel task dispatch within slices. When enabled, GSD derives a dependency graph from IO annotations in task plans and dispatches multiple non-conflicting tasks in parallel via subagents. Backward compatible — disabled by default. Enable with `reactive_execution: true` in preferences.
30
+ - **Anthropic Vertex AI provider** — run Claude models (Opus 4.6, Sonnet 4.6, Haiku 4.5) through Google Vertex AI. Set `ANTHROPIC_VERTEX_PROJECT_ID` to activate.
31
+ - **CI optimization** — GitHub Actions minutes reduced ~60-70% (~10k ~3-4k/month)
32
+ - **Reactive batch verification** — dependency-based carry-forward for verification results across parallel task batches
33
+ - **Backtick file path enforcement** — task plan IO sections now require backtick-wrapped paths for reliable parsing
34
+
35
+ See the full [Changelog](./CHANGELOG.md) for details.
36
+
37
+ ### Previous highlights (v2.34–v2.37)
38
+
39
+ - **cmux integration** — sidebar status, progress bars, and notifications for cmux terminal multiplexer users
40
+ - **Redesigned dashboard** — two-column layout with 4 widget modes (full → small → min → off)
32
41
  - **AGENTS.md support** — deprecated `agent-instructions.md` in favor of standard `AGENTS.md` / `CLAUDE.md`
33
42
  - **AI-powered triage** — automated issue and PR triage via Claude Haiku
34
- - **Auto-generated OpenRouter registry** — model registry built from OpenRouter API for always-current model support
35
- - **Extension manifest system** — user-managed enable/disable for bundled extensions
36
- - **Pipeline simplification (ADR-003)** — merged research into planning, mechanical completion
37
- - **Workflow templates** — right-sized workflows for every task type
38
- - **Health widget** — always-on environment health checks with progress scoring
43
+ - **Auto-generated OpenRouter registry** — model registry built from OpenRouter API
39
44
  - **`/gsd changelog`** — LLM-summarized release notes for any version
40
-
41
- See the full [Changelog](./CHANGELOG.md) for details.
45
+ - **Search budget enforcement** — session-level cap prevents unbounded web search
42
46
 
43
47
  ---
44
48
 
package/dist/app-paths.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from 'os';
2
2
  import { join } from 'path';
3
- export const appRoot = join(homedir(), '.gsd');
3
+ export const appRoot = process.env.GSD_HOME || join(homedir(), '.gsd');
4
4
  export const agentDir = join(appRoot, 'agent');
5
5
  export const sessionsDir = join(appRoot, 'sessions');
6
6
  export const authFilePath = join(agentDir, 'auth.json');
@@ -6,7 +6,7 @@
6
6
  * The only way an extension stops loading is an explicit `gsd extensions disable <id>`.
7
7
  */
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
- import { homedir } from "node:os";
9
+ import { appRoot } from "./app-paths.js";
10
10
  import { dirname, join } from "node:path";
11
11
  // ─── Validation ─────────────────────────────────────────────────────────────
12
12
  function isRegistry(data) {
@@ -26,7 +26,7 @@ function isManifest(data) {
26
26
  }
27
27
  // ─── Registry Path ──────────────────────────────────────────────────────────
28
28
  export function getRegistryPath() {
29
- return join(homedir(), ".gsd", "extensions", "registry.json");
29
+ return join(appRoot, "extensions", "registry.json");
30
30
  }
31
31
  // ─── Registry I/O ───────────────────────────────────────────────────────────
32
32
  function defaultRegistry() {
@@ -9,12 +9,12 @@
9
9
  */
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
11
  import { dirname, join } from "node:path";
12
- import { homedir } from "node:os";
12
+ import { appRoot } from "./app-paths.js";
13
13
  // Inlined from preferences.ts to avoid crossing the compiled/uncompiled
14
14
  // boundary — this file is compiled by tsc, but preferences.ts is loaded
15
15
  // via jiti at runtime. Importing it as .js fails because no .js exists
16
16
  // in dist/. See #592, #1110.
17
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
17
+ const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md");
18
18
  export function saveRemoteQuestionsConfig(channel, channelId) {
19
19
  const prefsPath = GLOBAL_PREFERENCES_PATH;
20
20
  const block = [
@@ -1,7 +1,7 @@
1
1
  import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
- import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, join, relative, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { compareSemver } from './update-check.js';
@@ -217,6 +217,35 @@ function copyDirRecursive(src, dest) {
217
217
  }
218
218
  }
219
219
  }
220
+ /**
221
+ * Creates (or updates) a symlink at agentDir/node_modules pointing to GSD's
222
+ * own node_modules directory.
223
+ *
224
+ * Native ESM `import()` ignores NODE_PATH — it resolves packages by walking
225
+ * up the directory tree from the importing file. Extension files synced to
226
+ * ~/.gsd/agent/extensions/ have no ancestor node_modules, so imports of
227
+ * @gsd/* packages fail. The symlink makes Node's standard resolution find
228
+ * them without requiring every call site to use jiti.
229
+ */
230
+ function ensureNodeModulesSymlink(agentDir) {
231
+ const agentNodeModules = join(agentDir, 'node_modules');
232
+ const gsdNodeModules = join(packageRoot, 'node_modules');
233
+ try {
234
+ const existing = readlinkSync(agentNodeModules);
235
+ if (existing === gsdNodeModules)
236
+ return; // already correct
237
+ unlinkSync(agentNodeModules);
238
+ }
239
+ catch {
240
+ // readlinkSync throws if path doesn't exist or isn't a symlink — both are fine
241
+ }
242
+ try {
243
+ symlinkSync(gsdNodeModules, agentNodeModules, 'junction');
244
+ }
245
+ catch {
246
+ // Non-fatal — worst case, extensions fall back to NODE_PATH via jiti
247
+ }
248
+ }
220
249
  /**
221
250
  * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
222
251
  *
@@ -262,6 +291,10 @@ export function initResources(agentDir) {
262
291
  // Ensure all newly copied files are owner-writable so the next run can
263
292
  // overwrite them (covers extensions, agents, and skills in one walk).
264
293
  makeTreeWritable(agentDir);
294
+ // Ensure ~/.gsd/agent/node_modules symlinks to GSD's node_modules so that
295
+ // native ESM import() calls from synced extension files can resolve @gsd/*
296
+ // packages via ancestor directory lookup. NODE_PATH only applies to CJS/jiti.
297
+ ensureNodeModulesSymlink(agentDir);
265
298
  writeManagedResourceManifest(agentDir);
266
299
  ensureRegistryEntries(join(agentDir, 'extensions'));
267
300
  }
@@ -4,7 +4,7 @@ let registrationPromise = null;
4
4
  async function registerBrowserTools(pi) {
5
5
  if (!registrationPromise) {
6
6
  registrationPromise = (async () => {
7
- const [lifecycle, capture, settle, refs, utils, navigation, screenshot, interaction, inspection, session, assertions, refTools, wait, pages, forms, intent, pdf, statePersistence, networkMock, device, extract, visualDiff, zoom, codegen, actionCache, injectionDetection,] = await Promise.all([
7
+ const [lifecycle, capture, settle, refs, utils, navigation, screenshot, interaction, inspection, session, assertions, refTools, wait, pages, forms, intent, pdf, statePersistence, networkMock, device, extract, visualDiff, zoom, codegen, actionCache, injectionDetection, verify,] = await Promise.all([
8
8
  importExtensionModule(import.meta.url, "./lifecycle.js"),
9
9
  importExtensionModule(import.meta.url, "./capture.js"),
10
10
  importExtensionModule(import.meta.url, "./settle.js"),
@@ -31,6 +31,7 @@ async function registerBrowserTools(pi) {
31
31
  importExtensionModule(import.meta.url, "./tools/codegen.js"),
32
32
  importExtensionModule(import.meta.url, "./tools/action-cache.js"),
33
33
  importExtensionModule(import.meta.url, "./tools/injection-detect.js"),
34
+ importExtensionModule(import.meta.url, "./tools/verify.js"),
34
35
  ]);
35
36
  const deps = {
36
37
  ensureBrowser: lifecycle.ensureBrowser,
@@ -99,6 +100,7 @@ async function registerBrowserTools(pi) {
99
100
  codegen.registerCodegenTools(pi, deps);
100
101
  actionCache.registerActionCacheTools(pi, deps);
101
102
  injectionDetection.registerInjectionDetectionTools(pi, deps);
103
+ verify.registerVerifyTools(pi, deps);
102
104
  })().catch((error) => {
103
105
  registrationPromise = null;
104
106
  throw error;
@@ -0,0 +1,97 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ export function registerVerifyTools(pi, deps) {
3
+ pi.registerTool({
4
+ name: "browser_verify",
5
+ label: "Browser Verify",
6
+ description: "Run a structured browser verification flow: navigate to a URL, run checks (element visibility, text content), capture screenshots as evidence, and return structured pass/fail results.",
7
+ promptGuidelines: [
8
+ "Use browser_verify for UAT verification flows that need structured evidence.",
9
+ "Each check produces a pass/fail result with captured evidence.",
10
+ "Prefer this over manual navigation + assertion sequences for verification tasks.",
11
+ ],
12
+ parameters: Type.Object({
13
+ url: Type.String({ description: "URL to navigate to" }),
14
+ checks: Type.Array(Type.Object({
15
+ description: Type.String({ description: "What this check verifies" }),
16
+ selector: Type.Optional(Type.String({ description: "CSS selector to check" })),
17
+ expectedText: Type.Optional(Type.String({ description: "Expected text content" })),
18
+ expectedVisible: Type.Optional(Type.Boolean({ description: "Whether element should be visible" })),
19
+ screenshot: Type.Optional(Type.Boolean({ description: "Capture screenshot as evidence" })),
20
+ }), { description: "Verification checks to run" }),
21
+ timeout: Type.Optional(Type.Number({ description: "Navigation timeout in ms", default: 10000 })),
22
+ }),
23
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
24
+ const startTime = Date.now();
25
+ const { page } = await deps.ensureBrowser();
26
+ const timeout = params.timeout ?? 10000;
27
+ try {
28
+ await page.goto(params.url, { waitUntil: "domcontentloaded", timeout });
29
+ }
30
+ catch (navErr) {
31
+ const msg = navErr instanceof Error ? navErr.message : String(navErr);
32
+ return {
33
+ content: [{ type: "text", text: `Navigation failed: ${msg}` }],
34
+ details: {
35
+ url: params.url,
36
+ passed: false,
37
+ checks: params.checks.map((c) => ({ description: c.description, passed: false, error: msg })),
38
+ duration: Date.now() - startTime,
39
+ },
40
+ };
41
+ }
42
+ const results = [];
43
+ for (const check of params.checks) {
44
+ try {
45
+ let passed = true;
46
+ let actual;
47
+ let evidence;
48
+ if (check.selector) {
49
+ const element = await page.$(check.selector);
50
+ if (check.expectedVisible !== undefined) {
51
+ const isVisible = element ? await element.isVisible() : false;
52
+ passed = isVisible === check.expectedVisible;
53
+ actual = `visible=${isVisible}`;
54
+ }
55
+ if (check.expectedText !== undefined && element) {
56
+ const text = await element.textContent();
57
+ passed = passed && (text?.includes(check.expectedText) ?? false);
58
+ actual = `text="${text?.slice(0, 200)}"`;
59
+ }
60
+ if (!element && (check.expectedVisible === true || check.expectedText)) {
61
+ passed = false;
62
+ actual = "element not found";
63
+ }
64
+ }
65
+ if (check.screenshot) {
66
+ try {
67
+ const buf = await page.screenshot({ type: "png" });
68
+ evidence = `screenshot captured (${buf.length} bytes)`;
69
+ }
70
+ catch {
71
+ evidence = "screenshot failed";
72
+ }
73
+ }
74
+ results.push({ description: check.description, passed, actual, evidence });
75
+ }
76
+ catch (checkErr) {
77
+ results.push({
78
+ description: check.description,
79
+ passed: false,
80
+ error: checkErr instanceof Error ? checkErr.message : String(checkErr),
81
+ });
82
+ }
83
+ }
84
+ const allPassed = results.every((r) => r.passed);
85
+ const summary = results.map((r) => `${r.passed ? "PASS" : "FAIL"}: ${r.description}${r.actual ? ` (${r.actual})` : ""}${r.error ? ` — ${r.error}` : ""}`).join("\n");
86
+ return {
87
+ content: [{ type: "text", text: `Verification ${allPassed ? "PASSED" : "FAILED"} (${results.filter(r => r.passed).length}/${results.length})\n\n${summary}` }],
88
+ details: {
89
+ url: params.url,
90
+ passed: allPassed,
91
+ checks: results,
92
+ duration: Date.now() - startTime,
93
+ },
94
+ };
95
+ },
96
+ });
97
+ }
@@ -0,0 +1,29 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+ import { readFile } from "node:fs/promises";
8
+ /**
9
+ * Check which keys already exist in a .env file or process.env.
10
+ * Returns the subset of `keys` that are already set.
11
+ */
12
+ export async function checkExistingEnvKeys(keys, envFilePath) {
13
+ let fileContent = "";
14
+ try {
15
+ fileContent = await readFile(envFilePath, "utf8");
16
+ }
17
+ catch {
18
+ // ENOENT or other read error — proceed with empty content
19
+ }
20
+ const existing = [];
21
+ for (const key of keys) {
22
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
24
+ if (regex.test(fileContent) || key in process.env) {
25
+ existing.push(key);
26
+ }
27
+ }
28
+ return existing;
29
+ }
@@ -46,30 +46,11 @@ async function writeEnvKey(filePath, key, value) {
46
46
  await writeFile(filePath, content, "utf8");
47
47
  }
48
48
  // ─── Exported utilities ───────────────────────────────────────────────────────
49
- /**
50
- * Check which keys already exist in the .env file or process.env.
51
- * Returns the subset of `keys` that are already set.
52
- * Handles ENOENT gracefully (still checks process.env).
53
- * Empty-string values count as existing.
54
- */
55
- export async function checkExistingEnvKeys(keys, envFilePath) {
56
- let fileContent = "";
57
- try {
58
- fileContent = await readFile(envFilePath, "utf8");
59
- }
60
- catch {
61
- // ENOENT or other read error — proceed with empty content
62
- }
63
- const existing = [];
64
- for (const key of keys) {
65
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
67
- if (regex.test(fileContent) || key in process.env) {
68
- existing.push(key);
69
- }
70
- }
71
- return existing;
72
- }
49
+ // Re-export from env-utils.ts so existing consumers still work.
50
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
51
+ // into modules that only need env-checking (e.g. files.ts during reports).
52
+ import { checkExistingEnvKeys } from "./env-utils.js";
53
+ export { checkExistingEnvKeys };
73
54
  /**
74
55
  * Detect the write destination based on project files in basePath.
75
56
  * Priority: vercel.json → convex/ dir → fallback "dotenv".
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Thin wrapper around the `gh` CLI.
3
+ *
4
+ * Every public function returns `GhResult<T>` — never throws.
5
+ * Uses `execFileSync` (not `execSync`) for safety.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ function ok(data) {
9
+ return { ok: true, data };
10
+ }
11
+ function fail(error) {
12
+ return { ok: false, error };
13
+ }
14
+ // ─── gh Availability ────────────────────────────────────────────────────────
15
+ let _ghAvailable = null;
16
+ export function ghIsAvailable() {
17
+ if (_ghAvailable !== null)
18
+ return _ghAvailable;
19
+ try {
20
+ execFileSync("gh", ["--version"], {
21
+ encoding: "utf-8",
22
+ stdio: ["ignore", "pipe", "ignore"],
23
+ timeout: 5_000,
24
+ });
25
+ _ghAvailable = true;
26
+ }
27
+ catch {
28
+ _ghAvailable = false;
29
+ }
30
+ return _ghAvailable;
31
+ }
32
+ /** Reset cached availability (for testing). */
33
+ export function _resetGhCache() {
34
+ _ghAvailable = null;
35
+ }
36
+ // ─── Rate Limit Check ───────────────────────────────────────────────────────
37
+ let _rateLimitCheckedAt = 0;
38
+ let _rateLimitOk = true;
39
+ const RATE_LIMIT_CHECK_INTERVAL_MS = 300_000; // 5 minutes
40
+ export function ghHasRateLimit(cwd) {
41
+ const now = Date.now();
42
+ if (now - _rateLimitCheckedAt < RATE_LIMIT_CHECK_INTERVAL_MS)
43
+ return _rateLimitOk;
44
+ _rateLimitCheckedAt = now;
45
+ try {
46
+ const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".rate.remaining"], {
47
+ cwd,
48
+ encoding: "utf-8",
49
+ stdio: ["ignore", "pipe", "ignore"],
50
+ timeout: 10_000,
51
+ }).trim();
52
+ const remaining = parseInt(raw, 10);
53
+ _rateLimitOk = Number.isFinite(remaining) && remaining >= 100;
54
+ }
55
+ catch {
56
+ // Can't check — assume OK so we don't silently disable sync
57
+ _rateLimitOk = true;
58
+ }
59
+ return _rateLimitOk;
60
+ }
61
+ // ─── Helpers ────────────────────────────────────────────────────────────────
62
+ const GH_TIMEOUT = 15_000;
63
+ const MAX_BODY_LENGTH = 65_000;
64
+ function truncateBody(body) {
65
+ if (body.length <= MAX_BODY_LENGTH)
66
+ return body;
67
+ return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n*Body truncated (exceeded 65K characters)*";
68
+ }
69
+ function runGh(args, cwd) {
70
+ try {
71
+ const stdout = execFileSync("gh", args, {
72
+ cwd,
73
+ encoding: "utf-8",
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ timeout: GH_TIMEOUT,
76
+ }).trim();
77
+ return ok(stdout);
78
+ }
79
+ catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ return fail(msg);
82
+ }
83
+ }
84
+ function runGhJson(args, cwd) {
85
+ const result = runGh(args, cwd);
86
+ if (!result.ok)
87
+ return fail(result.error);
88
+ try {
89
+ return ok(JSON.parse(result.data));
90
+ }
91
+ catch {
92
+ return fail(`Failed to parse JSON: ${result.data}`);
93
+ }
94
+ }
95
+ // ─── Repo Detection ─────────────────────────────────────────────────────────
96
+ export function ghDetectRepo(cwd) {
97
+ const result = runGh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], cwd);
98
+ if (!result.ok)
99
+ return fail(result.error);
100
+ const repo = result.data.trim();
101
+ if (!repo || !repo.includes("/"))
102
+ return fail("Could not detect repo");
103
+ return ok(repo);
104
+ }
105
+ export function ghCreateIssue(cwd, opts) {
106
+ const args = [
107
+ "issue", "create",
108
+ "--repo", opts.repo,
109
+ "--title", opts.title,
110
+ "--body", truncateBody(opts.body),
111
+ ];
112
+ if (opts.labels?.length) {
113
+ args.push("--label", opts.labels.join(","));
114
+ }
115
+ if (opts.milestone) {
116
+ args.push("--milestone", String(opts.milestone));
117
+ }
118
+ const result = runGh(args, cwd);
119
+ if (!result.ok)
120
+ return fail(result.error);
121
+ // gh issue create returns the URL; extract issue number
122
+ const match = result.data.match(/\/issues\/(\d+)/);
123
+ if (!match)
124
+ return fail(`Could not parse issue number from: ${result.data}`);
125
+ const issueNumber = parseInt(match[1], 10);
126
+ // If parent specified, add as sub-issue via GraphQL
127
+ if (opts.parentIssue) {
128
+ ghAddSubIssue(cwd, opts.repo, opts.parentIssue, issueNumber);
129
+ }
130
+ return ok(issueNumber);
131
+ }
132
+ export function ghCloseIssue(cwd, repo, issueNumber, comment) {
133
+ if (comment) {
134
+ ghAddComment(cwd, repo, issueNumber, comment);
135
+ }
136
+ const result = runGh(["issue", "close", String(issueNumber), "--repo", repo], cwd);
137
+ if (!result.ok)
138
+ return fail(result.error);
139
+ return ok(undefined);
140
+ }
141
+ export function ghAddComment(cwd, repo, issueNumber, body) {
142
+ const result = runGh(["issue", "comment", String(issueNumber), "--repo", repo, "--body", truncateBody(body)], cwd);
143
+ if (!result.ok)
144
+ return fail(result.error);
145
+ return ok(undefined);
146
+ }
147
+ // ─── Sub-Issues (GraphQL) ───────────────────────────────────────────────────
148
+ function ghAddSubIssue(cwd, repo, parentNumber, childNumber) {
149
+ // Get node IDs for both issues
150
+ const parentResult = runGhJson(["api", `repos/${repo}/issues/${parentNumber}`, "--jq", "{id: .node_id}"], cwd);
151
+ const childResult = runGhJson(["api", `repos/${repo}/issues/${childNumber}`, "--jq", "{id: .node_id}"], cwd);
152
+ if (!parentResult.ok || !childResult.ok) {
153
+ return fail("Could not resolve issue node IDs for sub-issue linking");
154
+ }
155
+ const mutation = `mutation { addSubIssue(input: { issueId: "${parentResult.data.id}", subIssueId: "${childResult.data.id}" }) { issue { id } } }`;
156
+ return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd);
157
+ }
158
+ // ─── Milestones ─────────────────────────────────────────────────────────────
159
+ export function ghCreateMilestone(cwd, repo, title, description) {
160
+ const result = runGhJson([
161
+ "api", `repos/${repo}/milestones`,
162
+ "-X", "POST",
163
+ "-f", `title=${title}`,
164
+ "-f", `description=${truncateBody(description)}`,
165
+ "-f", "state=open",
166
+ "--jq", "{number: .number}",
167
+ ], cwd);
168
+ if (!result.ok)
169
+ return fail(result.error);
170
+ return ok(result.data.number);
171
+ }
172
+ export function ghCloseMilestone(cwd, repo, milestoneNumber) {
173
+ const result = runGh([
174
+ "api", `repos/${repo}/milestones/${milestoneNumber}`,
175
+ "-X", "PATCH",
176
+ "-f", "state=closed",
177
+ ], cwd);
178
+ if (!result.ok)
179
+ return fail(result.error);
180
+ return ok(undefined);
181
+ }
182
+ export function ghCreatePR(cwd, opts) {
183
+ const args = [
184
+ "pr", "create",
185
+ "--repo", opts.repo,
186
+ "--base", opts.base,
187
+ "--head", opts.head,
188
+ "--title", opts.title,
189
+ "--body", truncateBody(opts.body),
190
+ ];
191
+ if (opts.draft)
192
+ args.push("--draft");
193
+ const result = runGh(args, cwd);
194
+ if (!result.ok)
195
+ return fail(result.error);
196
+ const match = result.data.match(/\/pull\/(\d+)/);
197
+ if (!match)
198
+ return fail(`Could not parse PR number from: ${result.data}`);
199
+ return ok(parseInt(match[1], 10));
200
+ }
201
+ export function ghMarkPRReady(cwd, repo, prNumber) {
202
+ const result = runGh(["pr", "ready", String(prNumber), "--repo", repo], cwd);
203
+ if (!result.ok)
204
+ return fail(result.error);
205
+ return ok(undefined);
206
+ }
207
+ export function ghMergePR(cwd, repo, prNumber, strategy = "squash") {
208
+ const args = [
209
+ "pr", "merge", String(prNumber),
210
+ "--repo", repo,
211
+ strategy === "squash" ? "--squash" : "--merge",
212
+ "--delete-branch",
213
+ ];
214
+ const result = runGh(args, cwd);
215
+ if (!result.ok)
216
+ return fail(result.error);
217
+ return ok(undefined);
218
+ }
219
+ // ─── Projects v2 ────────────────────────────────────────────────────────────
220
+ export function ghAddToProject(cwd, repo, projectNumber, issueNumber) {
221
+ // Get the issue's node ID first
222
+ const issueResult = runGhJson(["api", `repos/${repo}/issues/${issueNumber}`, "--jq", "{id: .node_id}"], cwd);
223
+ if (!issueResult.ok)
224
+ return fail(issueResult.error);
225
+ // Get the project's node ID
226
+ const [owner] = repo.split("/");
227
+ const projectResult = runGhJson([
228
+ "api", "graphql",
229
+ "-f", `query=query { user(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
230
+ "--jq", ".data.user.projectV2.id",
231
+ ], cwd);
232
+ // Try org if user fails
233
+ let projectId;
234
+ if (projectResult.ok && projectResult.data?.id) {
235
+ projectId = projectResult.data.id;
236
+ }
237
+ else {
238
+ const orgResult = runGhJson([
239
+ "api", "graphql",
240
+ "-f", `query=query { organization(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
241
+ "--jq", ".data.organization.projectV2.id",
242
+ ], cwd);
243
+ if (orgResult.ok)
244
+ projectId = orgResult.data?.id;
245
+ }
246
+ if (!projectId)
247
+ return fail("Could not find project");
248
+ const mutation = `mutation { addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${issueResult.data.id}" }) { item { id } } }`;
249
+ return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd);
250
+ }
251
+ // ─── Branch Operations ──────────────────────────────────────────────────────
252
+ export function ghPushBranch(cwd, branch, setUpstream = true) {
253
+ const args = ["git", "push"];
254
+ if (setUpstream)
255
+ args.push("-u", "origin", branch);
256
+ else
257
+ args.push("origin", branch);
258
+ try {
259
+ execFileSync(args[0], args.slice(1), {
260
+ cwd,
261
+ encoding: "utf-8",
262
+ stdio: ["ignore", "pipe", "pipe"],
263
+ timeout: 30_000,
264
+ });
265
+ return ok(undefined);
266
+ }
267
+ catch (err) {
268
+ return fail(err instanceof Error ? err.message : String(err));
269
+ }
270
+ }
271
+ export function ghCreateBranch(cwd, branch, from) {
272
+ try {
273
+ execFileSync("git", ["branch", branch, from], {
274
+ cwd,
275
+ encoding: "utf-8",
276
+ stdio: ["ignore", "pipe", "pipe"],
277
+ timeout: 10_000,
278
+ });
279
+ return ok(undefined);
280
+ }
281
+ catch (err) {
282
+ return fail(err instanceof Error ? err.message : String(err));
283
+ }
284
+ }