gsd-pi 2.38.0-dev.eeb3520 → 2.39.0-dev.64cd3ed

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 (255) hide show
  1. package/README.md +15 -11
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/remote-questions-config.js +2 -2
  8. package/dist/resource-loader.js +34 -1
  9. package/dist/resources/extensions/async-jobs/index.js +10 -0
  10. package/dist/resources/extensions/browser-tools/index.js +3 -1
  11. package/dist/resources/extensions/browser-tools/package.json +3 -1
  12. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  13. package/dist/resources/extensions/cmux/index.js +55 -1
  14. package/dist/resources/extensions/context7/package.json +1 -1
  15. package/dist/resources/extensions/env-utils.js +29 -0
  16. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  17. package/dist/resources/extensions/github-sync/cli.js +284 -0
  18. package/dist/resources/extensions/github-sync/index.js +73 -0
  19. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  20. package/dist/resources/extensions/github-sync/sync.js +424 -0
  21. package/dist/resources/extensions/github-sync/templates.js +118 -0
  22. package/dist/resources/extensions/github-sync/types.js +7 -0
  23. package/dist/resources/extensions/google-search/package.json +3 -1
  24. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  25. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  26. package/dist/resources/extensions/gsd/auto-loop.js +650 -588
  27. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  28. package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
  29. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  30. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  31. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  32. package/dist/resources/extensions/gsd/auto.js +143 -96
  33. package/dist/resources/extensions/gsd/captures.js +9 -1
  34. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  35. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  36. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  37. package/dist/resources/extensions/gsd/commands.js +24 -3
  38. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  39. package/dist/resources/extensions/gsd/detection.js +1 -2
  40. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  41. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  42. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  43. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  44. package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
  45. package/dist/resources/extensions/gsd/doctor.js +204 -12
  46. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  47. package/dist/resources/extensions/gsd/export.js +1 -1
  48. package/dist/resources/extensions/gsd/files.js +48 -9
  49. package/dist/resources/extensions/gsd/forensics.js +1 -1
  50. package/dist/resources/extensions/gsd/git-service.js +30 -12
  51. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  52. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  53. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  54. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  55. package/dist/resources/extensions/gsd/index.js +24 -20
  56. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  57. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  58. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  59. package/dist/resources/extensions/gsd/package.json +1 -1
  60. package/dist/resources/extensions/gsd/paths.js +3 -0
  61. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  62. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  63. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  64. package/dist/resources/extensions/gsd/preferences.js +22 -11
  65. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  66. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  67. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  68. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  69. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
  70. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  71. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  72. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  73. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  74. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  75. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  76. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  77. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  78. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  79. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  80. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  81. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  82. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  83. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  84. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  85. package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
  86. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  87. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  88. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  89. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  90. package/dist/resources/extensions/gsd/state.js +42 -23
  91. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  92. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  93. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  94. package/dist/resources/extensions/gsd/worktree.js +35 -16
  95. package/dist/resources/extensions/mcp-client/index.js +14 -1
  96. package/dist/resources/extensions/remote-questions/status.js +4 -1
  97. package/dist/resources/extensions/remote-questions/store.js +4 -1
  98. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  99. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  100. package/dist/resources/extensions/subagent/index.js +12 -3
  101. package/dist/resources/extensions/subagent/isolation.js +2 -1
  102. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  103. package/dist/resources/extensions/universal-config/package.json +1 -1
  104. package/dist/welcome-screen.d.ts +12 -0
  105. package/dist/welcome-screen.js +53 -0
  106. package/package.json +1 -1
  107. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  108. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  109. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  110. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  112. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  115. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  117. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  119. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  121. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/index.js +1 -1
  123. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  124. package/packages/pi-coding-agent/package.json +1 -1
  125. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  126. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  127. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  128. package/packages/pi-coding-agent/src/index.ts +1 -0
  129. package/pkg/package.json +1 -1
  130. package/src/resources/extensions/async-jobs/index.ts +11 -0
  131. package/src/resources/extensions/browser-tools/index.ts +3 -0
  132. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  133. package/src/resources/extensions/cmux/index.ts +57 -1
  134. package/src/resources/extensions/env-utils.ts +31 -0
  135. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  136. package/src/resources/extensions/github-sync/cli.ts +364 -0
  137. package/src/resources/extensions/github-sync/index.ts +93 -0
  138. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  139. package/src/resources/extensions/github-sync/sync.ts +556 -0
  140. package/src/resources/extensions/github-sync/templates.ts +183 -0
  141. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  142. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  143. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  144. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  145. package/src/resources/extensions/github-sync/types.ts +47 -0
  146. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  147. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  148. package/src/resources/extensions/gsd/auto-loop.ts +553 -546
  149. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  150. package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
  151. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  152. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  153. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  154. package/src/resources/extensions/gsd/auto.ts +139 -101
  155. package/src/resources/extensions/gsd/captures.ts +10 -1
  156. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  157. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  158. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  159. package/src/resources/extensions/gsd/commands.ts +26 -4
  160. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  161. package/src/resources/extensions/gsd/detection.ts +2 -2
  162. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  163. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  164. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  165. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  166. package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
  167. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  168. package/src/resources/extensions/gsd/doctor.ts +199 -14
  169. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  170. package/src/resources/extensions/gsd/export.ts +1 -1
  171. package/src/resources/extensions/gsd/files.ts +51 -11
  172. package/src/resources/extensions/gsd/forensics.ts +1 -1
  173. package/src/resources/extensions/gsd/git-service.ts +44 -10
  174. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  175. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  176. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  177. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  178. package/src/resources/extensions/gsd/index.ts +24 -17
  179. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  180. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  181. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  182. package/src/resources/extensions/gsd/paths.ts +4 -0
  183. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  184. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  185. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  186. package/src/resources/extensions/gsd/preferences.ts +25 -11
  187. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  188. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  190. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  191. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
  192. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  193. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  194. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  195. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  196. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  197. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  198. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  199. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  200. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  201. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  202. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  203. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  204. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  205. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  206. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  207. package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
  208. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  209. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  210. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  211. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  212. package/src/resources/extensions/gsd/state.ts +39 -21
  213. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  214. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  215. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  216. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  217. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  218. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  219. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  220. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  221. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  222. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  223. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  224. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  225. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  226. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  227. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  228. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  229. package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
  230. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  231. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  232. package/src/resources/extensions/gsd/types.ts +18 -1
  233. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  234. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  235. package/src/resources/extensions/gsd/worktree.ts +35 -15
  236. package/src/resources/extensions/mcp-client/index.ts +17 -1
  237. package/src/resources/extensions/remote-questions/status.ts +5 -1
  238. package/src/resources/extensions/remote-questions/store.ts +5 -1
  239. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  240. package/src/resources/extensions/shared/frontmatter.ts +1 -1
  241. package/src/resources/extensions/subagent/index.ts +12 -3
  242. package/src/resources/extensions/subagent/isolation.ts +3 -1
  243. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  244. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  245. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  246. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  247. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  248. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  249. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  250. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  251. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  252. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  253. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  254. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  255. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -33,6 +33,7 @@ async function registerBrowserTools(pi: ExtensionAPI): Promise<void> {
33
33
  codegen,
34
34
  actionCache,
35
35
  injectionDetection,
36
+ verify,
36
37
  ] = await Promise.all([
37
38
  importExtensionModule<typeof import("./lifecycle.js")>(import.meta.url, "./lifecycle.js"),
38
39
  importExtensionModule<typeof import("./capture.js")>(import.meta.url, "./capture.js"),
@@ -60,6 +61,7 @@ async function registerBrowserTools(pi: ExtensionAPI): Promise<void> {
60
61
  importExtensionModule<typeof import("./tools/codegen.js")>(import.meta.url, "./tools/codegen.js"),
61
62
  importExtensionModule<typeof import("./tools/action-cache.js")>(import.meta.url, "./tools/action-cache.js"),
62
63
  importExtensionModule<typeof import("./tools/injection-detect.js")>(import.meta.url, "./tools/injection-detect.js"),
64
+ importExtensionModule<typeof import("./tools/verify.js")>(import.meta.url, "./tools/verify.js"),
63
65
  ]);
64
66
 
65
67
  const deps = {
@@ -132,6 +134,7 @@ async function registerBrowserTools(pi: ExtensionAPI): Promise<void> {
132
134
  codegen.registerCodegenTools(pi, deps);
133
135
  actionCache.registerActionCacheTools(pi, deps);
134
136
  injectionDetection.registerInjectionDetectionTools(pi, deps);
137
+ verify.registerVerifyTools(pi, deps);
135
138
  })().catch((error) => {
136
139
  registrationPromise = null;
137
140
  throw error;
@@ -0,0 +1,117 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import type { ToolDeps } from "../state.js";
4
+
5
+ export function registerVerifyTools(pi: ExtensionAPI, deps: ToolDeps): void {
6
+ pi.registerTool({
7
+ name: "browser_verify",
8
+ label: "Browser Verify",
9
+ description:
10
+ "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.",
11
+ promptGuidelines: [
12
+ "Use browser_verify for UAT verification flows that need structured evidence.",
13
+ "Each check produces a pass/fail result with captured evidence.",
14
+ "Prefer this over manual navigation + assertion sequences for verification tasks.",
15
+ ],
16
+ parameters: Type.Object({
17
+ url: Type.String({ description: "URL to navigate to" }),
18
+ checks: Type.Array(
19
+ Type.Object({
20
+ description: Type.String({ description: "What this check verifies" }),
21
+ selector: Type.Optional(Type.String({ description: "CSS selector to check" })),
22
+ expectedText: Type.Optional(Type.String({ description: "Expected text content" })),
23
+ expectedVisible: Type.Optional(Type.Boolean({ description: "Whether element should be visible" })),
24
+ screenshot: Type.Optional(Type.Boolean({ description: "Capture screenshot as evidence" })),
25
+ }),
26
+ { description: "Verification checks to run" },
27
+ ),
28
+ timeout: Type.Optional(Type.Number({ description: "Navigation timeout in ms", default: 10000 })),
29
+ }),
30
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
31
+ const startTime = Date.now();
32
+ const { page } = await deps.ensureBrowser();
33
+ const timeout = params.timeout ?? 10000;
34
+
35
+ try {
36
+ await page.goto(params.url, { waitUntil: "domcontentloaded", timeout });
37
+ } catch (navErr) {
38
+ const msg = navErr instanceof Error ? navErr.message : String(navErr);
39
+ return {
40
+ content: [{ type: "text" as const, text: `Navigation failed: ${msg}` }],
41
+ details: {
42
+ url: params.url,
43
+ passed: false,
44
+ checks: params.checks.map((c) => ({ description: c.description, passed: false, error: msg })),
45
+ duration: Date.now() - startTime,
46
+ },
47
+ };
48
+ }
49
+
50
+ const results: Array<{
51
+ description: string;
52
+ passed: boolean;
53
+ actual?: string;
54
+ evidence?: string;
55
+ error?: string;
56
+ }> = [];
57
+
58
+ for (const check of params.checks) {
59
+ try {
60
+ let passed = true;
61
+ let actual: string | undefined;
62
+ let evidence: string | undefined;
63
+
64
+ if (check.selector) {
65
+ const element = await page.$(check.selector);
66
+
67
+ if (check.expectedVisible !== undefined) {
68
+ const isVisible = element ? await element.isVisible() : false;
69
+ passed = isVisible === check.expectedVisible;
70
+ actual = `visible=${isVisible}`;
71
+ }
72
+
73
+ if (check.expectedText !== undefined && element) {
74
+ const text = await element.textContent();
75
+ passed = passed && (text?.includes(check.expectedText) ?? false);
76
+ actual = `text="${text?.slice(0, 200)}"`;
77
+ }
78
+
79
+ if (!element && (check.expectedVisible === true || check.expectedText)) {
80
+ passed = false;
81
+ actual = "element not found";
82
+ }
83
+ }
84
+
85
+ if (check.screenshot) {
86
+ try {
87
+ const buf = await page.screenshot({ type: "png" });
88
+ evidence = `screenshot captured (${buf.length} bytes)`;
89
+ } catch {
90
+ evidence = "screenshot failed";
91
+ }
92
+ }
93
+
94
+ results.push({ description: check.description, passed, actual, evidence });
95
+ } catch (checkErr) {
96
+ results.push({
97
+ description: check.description,
98
+ passed: false,
99
+ error: checkErr instanceof Error ? checkErr.message : String(checkErr),
100
+ });
101
+ }
102
+ }
103
+
104
+ const allPassed = results.every((r) => r.passed);
105
+ const summary = results.map((r) => `${r.passed ? "PASS" : "FAIL"}: ${r.description}${r.actual ? ` (${r.actual})` : ""}${r.error ? ` — ${r.error}` : ""}`).join("\n");
106
+ return {
107
+ content: [{ type: "text" as const, text: `Verification ${allPassed ? "PASSED" : "FAILED"} (${results.filter(r => r.passed).length}/${results.length})\n\n${summary}` }],
108
+ details: {
109
+ url: params.url,
110
+ passed: allPassed,
111
+ checks: results,
112
+ duration: Date.now() - startTime,
113
+ },
114
+ };
115
+ },
116
+ });
117
+ }
@@ -289,10 +289,17 @@ export class CmuxClient {
289
289
  }
290
290
 
291
291
  async createSplit(direction: "right" | "down" | "left" | "up"): Promise<string | null> {
292
+ return this.createSplitFrom(this.config.surfaceId, direction);
293
+ }
294
+
295
+ async createSplitFrom(
296
+ sourceSurfaceId: string | undefined,
297
+ direction: "right" | "down" | "left" | "up",
298
+ ): Promise<string | null> {
292
299
  if (!this.config.splits) return null;
293
300
  const before = new Set(await this.listSurfaceIds());
294
301
  const args = ["new-split", direction];
295
- const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
302
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
296
303
  await this.runAsync(scopedArgs);
297
304
  const after = await this.listSurfaceIds();
298
305
  for (const id of after) {
@@ -301,6 +308,55 @@ export class CmuxClient {
301
308
  return null;
302
309
  }
303
310
 
311
+ /**
312
+ * Create a grid of surfaces for parallel agent execution.
313
+ *
314
+ * Layout strategy (gsd stays in the original surface):
315
+ * 1 agent: [gsd | A]
316
+ * 2 agents: [gsd | A]
317
+ * [ | B]
318
+ * 3 agents: [gsd | A]
319
+ * [ C | B]
320
+ * 4 agents: [gsd | A]
321
+ * [ C | B] (D splits from B downward)
322
+ * [ | D]
323
+ *
324
+ * Returns surface IDs in order, or empty array on failure.
325
+ */
326
+ async createGridLayout(count: number): Promise<string[]> {
327
+ if (!this.config.splits || count <= 0) return [];
328
+ const surfaces: string[] = [];
329
+
330
+ // First split: create right column from the gsd surface
331
+ const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
332
+ if (!rightCol) return [];
333
+ surfaces.push(rightCol);
334
+ if (count === 1) return surfaces;
335
+
336
+ // Second split: split right column down → bottom-right
337
+ const bottomRight = await this.createSplitFrom(rightCol, "down");
338
+ if (!bottomRight) return surfaces;
339
+ surfaces.push(bottomRight);
340
+ if (count === 2) return surfaces;
341
+
342
+ // Third split: split gsd surface down → bottom-left
343
+ const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
344
+ if (!bottomLeft) return surfaces;
345
+ surfaces.push(bottomLeft);
346
+ if (count === 3) return surfaces;
347
+
348
+ // Fourth+: split subsequent surfaces down from the last created
349
+ let lastSurface = bottomRight;
350
+ for (let i = 3; i < count; i++) {
351
+ const next = await this.createSplitFrom(lastSurface, "down");
352
+ if (!next) break;
353
+ surfaces.push(next);
354
+ lastSurface = next;
355
+ }
356
+
357
+ return surfaces;
358
+ }
359
+
304
360
  async sendSurface(surfaceId: string, text: string): Promise<boolean> {
305
361
  const payload = text.endsWith("\n") ? text : `${text}\n`;
306
362
  const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
@@ -0,0 +1,31 @@
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
+
8
+ import { readFile } from "node:fs/promises";
9
+
10
+ /**
11
+ * Check which keys already exist in a .env file or process.env.
12
+ * Returns the subset of `keys` that are already set.
13
+ */
14
+ export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
15
+ let fileContent = "";
16
+ try {
17
+ fileContent = await readFile(envFilePath, "utf8");
18
+ } catch {
19
+ // ENOENT or other read error — proceed with empty content
20
+ }
21
+
22
+ const existing: string[] = [];
23
+ for (const key of keys) {
24
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
26
+ if (regex.test(fileContent) || key in process.env) {
27
+ existing.push(key);
28
+ }
29
+ }
30
+ return existing;
31
+ }
@@ -67,30 +67,11 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis
67
67
 
68
68
  // ─── Exported utilities ───────────────────────────────────────────────────────
69
69
 
70
- /**
71
- * Check which keys already exist in the .env file or process.env.
72
- * Returns the subset of `keys` that are already set.
73
- * Handles ENOENT gracefully (still checks process.env).
74
- * Empty-string values count as existing.
75
- */
76
- export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
77
- let fileContent = "";
78
- try {
79
- fileContent = await readFile(envFilePath, "utf8");
80
- } catch {
81
- // ENOENT or other read error — proceed with empty content
82
- }
83
-
84
- const existing: string[] = [];
85
- for (const key of keys) {
86
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
87
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
88
- if (regex.test(fileContent) || key in process.env) {
89
- existing.push(key);
90
- }
91
- }
92
- return existing;
93
- }
70
+ // Re-export from env-utils.ts so existing consumers still work.
71
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
72
+ // into modules that only need env-checking (e.g. files.ts during reports).
73
+ import { checkExistingEnvKeys } from "./env-utils.js";
74
+ export { checkExistingEnvKeys };
94
75
 
95
76
  /**
96
77
  * Detect the write destination based on project files in basePath.
@@ -0,0 +1,364 @@
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
+
8
+ import { execFileSync } from "node:child_process";
9
+
10
+ // ─── Result Type ────────────────────────────────────────────────────────────
11
+
12
+ export interface GhResult<T> {
13
+ ok: boolean;
14
+ data?: T;
15
+ error?: string;
16
+ }
17
+
18
+ function ok<T>(data: T): GhResult<T> {
19
+ return { ok: true, data };
20
+ }
21
+
22
+ function fail<T>(error: string): GhResult<T> {
23
+ return { ok: false, error };
24
+ }
25
+
26
+ // ─── gh Availability ────────────────────────────────────────────────────────
27
+
28
+ let _ghAvailable: boolean | null = null;
29
+
30
+ export function ghIsAvailable(): boolean {
31
+ if (_ghAvailable !== null) return _ghAvailable;
32
+ try {
33
+ execFileSync("gh", ["--version"], {
34
+ encoding: "utf-8",
35
+ stdio: ["ignore", "pipe", "ignore"],
36
+ timeout: 5_000,
37
+ });
38
+ _ghAvailable = true;
39
+ } catch {
40
+ _ghAvailable = false;
41
+ }
42
+ return _ghAvailable;
43
+ }
44
+
45
+ /** Reset cached availability (for testing). */
46
+ export function _resetGhCache(): void {
47
+ _ghAvailable = null;
48
+ }
49
+
50
+ // ─── Rate Limit Check ───────────────────────────────────────────────────────
51
+
52
+ let _rateLimitCheckedAt = 0;
53
+ let _rateLimitOk = true;
54
+ const RATE_LIMIT_CHECK_INTERVAL_MS = 300_000; // 5 minutes
55
+
56
+ export function ghHasRateLimit(cwd: string): boolean {
57
+ const now = Date.now();
58
+ if (now - _rateLimitCheckedAt < RATE_LIMIT_CHECK_INTERVAL_MS) return _rateLimitOk;
59
+ _rateLimitCheckedAt = now;
60
+ try {
61
+ const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".rate.remaining"], {
62
+ cwd,
63
+ encoding: "utf-8",
64
+ stdio: ["ignore", "pipe", "ignore"],
65
+ timeout: 10_000,
66
+ }).trim();
67
+ const remaining = parseInt(raw, 10);
68
+ _rateLimitOk = Number.isFinite(remaining) && remaining >= 100;
69
+ } catch {
70
+ // Can't check — assume OK so we don't silently disable sync
71
+ _rateLimitOk = true;
72
+ }
73
+ return _rateLimitOk;
74
+ }
75
+
76
+ // ─── Helpers ────────────────────────────────────────────────────────────────
77
+
78
+ const GH_TIMEOUT = 15_000;
79
+ const MAX_BODY_LENGTH = 65_000;
80
+
81
+ function truncateBody(body: string): string {
82
+ if (body.length <= MAX_BODY_LENGTH) return body;
83
+ return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n*Body truncated (exceeded 65K characters)*";
84
+ }
85
+
86
+ function runGh(args: string[], cwd: string): GhResult<string> {
87
+ try {
88
+ const stdout = execFileSync("gh", args, {
89
+ cwd,
90
+ encoding: "utf-8",
91
+ stdio: ["ignore", "pipe", "pipe"],
92
+ timeout: GH_TIMEOUT,
93
+ }).trim();
94
+ return ok(stdout);
95
+ } catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ return fail(msg);
98
+ }
99
+ }
100
+
101
+ function runGhJson<T>(args: string[], cwd: string): GhResult<T> {
102
+ const result = runGh(args, cwd);
103
+ if (!result.ok) return fail(result.error!);
104
+ try {
105
+ return ok(JSON.parse(result.data!) as T);
106
+ } catch {
107
+ return fail(`Failed to parse JSON: ${result.data}`);
108
+ }
109
+ }
110
+
111
+ // ─── Repo Detection ─────────────────────────────────────────────────────────
112
+
113
+ export function ghDetectRepo(cwd: string): GhResult<string> {
114
+ const result = runGh(
115
+ ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
116
+ cwd,
117
+ );
118
+ if (!result.ok) return fail(result.error!);
119
+ const repo = result.data!.trim();
120
+ if (!repo || !repo.includes("/")) return fail("Could not detect repo");
121
+ return ok(repo);
122
+ }
123
+
124
+ // ─── Issues ─────────────────────────────────────────────────────────────────
125
+
126
+ export interface CreateIssueOpts {
127
+ repo: string;
128
+ title: string;
129
+ body: string;
130
+ labels?: string[];
131
+ milestone?: number;
132
+ parentIssue?: number;
133
+ }
134
+
135
+ export function ghCreateIssue(cwd: string, opts: CreateIssueOpts): GhResult<number> {
136
+ const args = [
137
+ "issue", "create",
138
+ "--repo", opts.repo,
139
+ "--title", opts.title,
140
+ "--body", truncateBody(opts.body),
141
+ ];
142
+ if (opts.labels?.length) {
143
+ args.push("--label", opts.labels.join(","));
144
+ }
145
+ if (opts.milestone) {
146
+ args.push("--milestone", String(opts.milestone));
147
+ }
148
+
149
+ const result = runGh(args, cwd);
150
+ if (!result.ok) return fail(result.error!);
151
+
152
+ // gh issue create returns the URL; extract issue number
153
+ const match = result.data!.match(/\/issues\/(\d+)/);
154
+ if (!match) return fail(`Could not parse issue number from: ${result.data}`);
155
+ const issueNumber = parseInt(match[1], 10);
156
+
157
+ // If parent specified, add as sub-issue via GraphQL
158
+ if (opts.parentIssue) {
159
+ ghAddSubIssue(cwd, opts.repo, opts.parentIssue, issueNumber);
160
+ }
161
+
162
+ return ok(issueNumber);
163
+ }
164
+
165
+ export function ghCloseIssue(cwd: string, repo: string, issueNumber: number, comment?: string): GhResult<void> {
166
+ if (comment) {
167
+ ghAddComment(cwd, repo, issueNumber, comment);
168
+ }
169
+ const result = runGh(
170
+ ["issue", "close", String(issueNumber), "--repo", repo],
171
+ cwd,
172
+ );
173
+ if (!result.ok) return fail(result.error!);
174
+ return ok(undefined);
175
+ }
176
+
177
+ export function ghAddComment(cwd: string, repo: string, issueNumber: number, body: string): GhResult<void> {
178
+ const result = runGh(
179
+ ["issue", "comment", String(issueNumber), "--repo", repo, "--body", truncateBody(body)],
180
+ cwd,
181
+ );
182
+ if (!result.ok) return fail(result.error!);
183
+ return ok(undefined);
184
+ }
185
+
186
+ // ─── Sub-Issues (GraphQL) ───────────────────────────────────────────────────
187
+
188
+ function ghAddSubIssue(cwd: string, repo: string, parentNumber: number, childNumber: number): GhResult<void> {
189
+ // Get node IDs for both issues
190
+ const parentResult = runGhJson<{ id: string }>(
191
+ ["api", `repos/${repo}/issues/${parentNumber}`, "--jq", "{id: .node_id}"],
192
+ cwd,
193
+ );
194
+ const childResult = runGhJson<{ id: string }>(
195
+ ["api", `repos/${repo}/issues/${childNumber}`, "--jq", "{id: .node_id}"],
196
+ cwd,
197
+ );
198
+
199
+ if (!parentResult.ok || !childResult.ok) {
200
+ return fail("Could not resolve issue node IDs for sub-issue linking");
201
+ }
202
+
203
+ const mutation = `mutation { addSubIssue(input: { issueId: "${parentResult.data!.id}", subIssueId: "${childResult.data!.id}" }) { issue { id } } }`;
204
+ return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd) as GhResult<void>;
205
+ }
206
+
207
+ // ─── Milestones ─────────────────────────────────────────────────────────────
208
+
209
+ export function ghCreateMilestone(cwd: string, repo: string, title: string, description: string): GhResult<number> {
210
+ const result = runGhJson<{ number: number }>(
211
+ [
212
+ "api", `repos/${repo}/milestones`,
213
+ "-X", "POST",
214
+ "-f", `title=${title}`,
215
+ "-f", `description=${truncateBody(description)}`,
216
+ "-f", "state=open",
217
+ "--jq", "{number: .number}",
218
+ ],
219
+ cwd,
220
+ );
221
+ if (!result.ok) return fail(result.error!);
222
+ return ok(result.data!.number);
223
+ }
224
+
225
+ export function ghCloseMilestone(cwd: string, repo: string, milestoneNumber: number): GhResult<void> {
226
+ const result = runGh(
227
+ [
228
+ "api", `repos/${repo}/milestones/${milestoneNumber}`,
229
+ "-X", "PATCH",
230
+ "-f", "state=closed",
231
+ ],
232
+ cwd,
233
+ );
234
+ if (!result.ok) return fail(result.error!);
235
+ return ok(undefined);
236
+ }
237
+
238
+ // ─── Pull Requests ──────────────────────────────────────────────────────────
239
+
240
+ export interface CreatePROpts {
241
+ repo: string;
242
+ base: string;
243
+ head: string;
244
+ title: string;
245
+ body: string;
246
+ draft?: boolean;
247
+ }
248
+
249
+ export function ghCreatePR(cwd: string, opts: CreatePROpts): GhResult<number> {
250
+ const args = [
251
+ "pr", "create",
252
+ "--repo", opts.repo,
253
+ "--base", opts.base,
254
+ "--head", opts.head,
255
+ "--title", opts.title,
256
+ "--body", truncateBody(opts.body),
257
+ ];
258
+ if (opts.draft) args.push("--draft");
259
+
260
+ const result = runGh(args, cwd);
261
+ if (!result.ok) return fail(result.error!);
262
+
263
+ const match = result.data!.match(/\/pull\/(\d+)/);
264
+ if (!match) return fail(`Could not parse PR number from: ${result.data}`);
265
+ return ok(parseInt(match[1], 10));
266
+ }
267
+
268
+ export function ghMarkPRReady(cwd: string, repo: string, prNumber: number): GhResult<void> {
269
+ const result = runGh(
270
+ ["pr", "ready", String(prNumber), "--repo", repo],
271
+ cwd,
272
+ );
273
+ if (!result.ok) return fail(result.error!);
274
+ return ok(undefined);
275
+ }
276
+
277
+ export function ghMergePR(cwd: string, repo: string, prNumber: number, strategy: "squash" | "merge" = "squash"): GhResult<void> {
278
+ const args = [
279
+ "pr", "merge", String(prNumber),
280
+ "--repo", repo,
281
+ strategy === "squash" ? "--squash" : "--merge",
282
+ "--delete-branch",
283
+ ];
284
+ const result = runGh(args, cwd);
285
+ if (!result.ok) return fail(result.error!);
286
+ return ok(undefined);
287
+ }
288
+
289
+ // ─── Projects v2 ────────────────────────────────────────────────────────────
290
+
291
+ export function ghAddToProject(cwd: string, repo: string, projectNumber: number, issueNumber: number): GhResult<void> {
292
+ // Get the issue's node ID first
293
+ const issueResult = runGhJson<{ id: string }>(
294
+ ["api", `repos/${repo}/issues/${issueNumber}`, "--jq", "{id: .node_id}"],
295
+ cwd,
296
+ );
297
+ if (!issueResult.ok) return fail(issueResult.error!);
298
+
299
+ // Get the project's node ID
300
+ const [owner] = repo.split("/");
301
+ const projectResult = runGhJson<{ id: string }>(
302
+ [
303
+ "api", "graphql",
304
+ "-f", `query=query { user(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
305
+ "--jq", ".data.user.projectV2.id",
306
+ ],
307
+ cwd,
308
+ );
309
+
310
+ // Try org if user fails
311
+ let projectId: string | undefined;
312
+ if (projectResult.ok && projectResult.data?.id) {
313
+ projectId = projectResult.data.id;
314
+ } else {
315
+ const orgResult = runGhJson<{ id: string }>(
316
+ [
317
+ "api", "graphql",
318
+ "-f", `query=query { organization(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
319
+ "--jq", ".data.organization.projectV2.id",
320
+ ],
321
+ cwd,
322
+ );
323
+ if (orgResult.ok) projectId = orgResult.data?.id;
324
+ }
325
+
326
+ if (!projectId) return fail("Could not find project");
327
+
328
+ const mutation = `mutation { addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${issueResult.data!.id}" }) { item { id } } }`;
329
+ return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd) as GhResult<void>;
330
+ }
331
+
332
+ // ─── Branch Operations ──────────────────────────────────────────────────────
333
+
334
+ export function ghPushBranch(cwd: string, branch: string, setUpstream = true): GhResult<void> {
335
+ const args = ["git", "push"];
336
+ if (setUpstream) args.push("-u", "origin", branch);
337
+ else args.push("origin", branch);
338
+
339
+ try {
340
+ execFileSync(args[0], args.slice(1), {
341
+ cwd,
342
+ encoding: "utf-8",
343
+ stdio: ["ignore", "pipe", "pipe"],
344
+ timeout: 30_000,
345
+ });
346
+ return ok(undefined);
347
+ } catch (err) {
348
+ return fail(err instanceof Error ? err.message : String(err));
349
+ }
350
+ }
351
+
352
+ export function ghCreateBranch(cwd: string, branch: string, from: string): GhResult<void> {
353
+ try {
354
+ execFileSync("git", ["branch", branch, from], {
355
+ cwd,
356
+ encoding: "utf-8",
357
+ stdio: ["ignore", "pipe", "pipe"],
358
+ timeout: 10_000,
359
+ });
360
+ return ok(undefined);
361
+ } catch (err) {
362
+ return fail(err instanceof Error ? err.message : String(err));
363
+ }
364
+ }