gsd-pi 2.22.0 → 2.24.0

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 (228) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +74 -7
  3. package/dist/headless.d.ts +25 -0
  4. package/dist/headless.js +454 -0
  5. package/dist/help-text.js +47 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resource-loader.js +64 -9
  11. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  12. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  13. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  14. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  15. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  16. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  17. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  18. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  19. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  20. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  21. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  22. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  23. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  24. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  25. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  26. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  27. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  28. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  29. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  30. package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
  31. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  32. package/dist/resources/extensions/gsd/auto.ts +560 -52
  33. package/dist/resources/extensions/gsd/captures.ts +49 -0
  34. package/dist/resources/extensions/gsd/commands.ts +194 -11
  35. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  36. package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  37. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  38. package/dist/resources/extensions/gsd/doctor.ts +76 -12
  39. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  41. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  42. package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
  43. package/dist/resources/extensions/gsd/index.ts +34 -1
  44. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  45. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  46. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  47. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  48. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  50. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  51. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  52. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  54. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  55. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  56. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  57. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  58. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  59. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  60. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  61. package/dist/resources/extensions/gsd/state.ts +72 -30
  62. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  63. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  64. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  65. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  66. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  67. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  68. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  69. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  70. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  71. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  72. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  73. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  74. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  75. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  76. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  77. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  78. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  79. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  80. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  81. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  82. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  83. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  84. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  85. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  86. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  87. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  88. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  89. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  90. package/dist/resources/extensions/gsd/types.ts +15 -1
  91. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  92. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  93. package/dist/resources/extensions/subagent/index.ts +5 -0
  94. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  95. package/dist/update-check.d.ts +9 -0
  96. package/dist/update-check.js +97 -0
  97. package/package.json +6 -1
  98. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  100. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  101. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  103. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  106. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  112. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  117. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  118. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  119. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  125. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  127. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  129. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  133. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/index.js +1 -1
  137. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  139. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  140. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  141. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  142. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  143. package/packages/pi-coding-agent/src/index.ts +1 -0
  144. package/scripts/postinstall.js +7 -109
  145. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  146. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  147. package/src/resources/extensions/bg-shell/types.ts +33 -1
  148. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  149. package/src/resources/extensions/browser-tools/index.ts +20 -0
  150. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  151. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  152. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  153. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  154. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  155. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  156. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  157. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  158. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  159. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  160. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  161. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  163. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  164. package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
  165. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  166. package/src/resources/extensions/gsd/auto.ts +560 -52
  167. package/src/resources/extensions/gsd/captures.ts +49 -0
  168. package/src/resources/extensions/gsd/commands.ts +194 -11
  169. package/src/resources/extensions/gsd/complexity.ts +1 -0
  170. package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  171. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  172. package/src/resources/extensions/gsd/doctor.ts +76 -12
  173. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  174. package/src/resources/extensions/gsd/forensics.ts +95 -52
  175. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  176. package/src/resources/extensions/gsd/guided-flow.ts +85 -5
  177. package/src/resources/extensions/gsd/index.ts +34 -1
  178. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  179. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  180. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  181. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  182. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  183. package/src/resources/extensions/gsd/preferences.ts +65 -1
  184. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  185. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  188. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  190. package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  191. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  192. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  193. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  194. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  195. package/src/resources/extensions/gsd/state.ts +72 -30
  196. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  197. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  198. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  199. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  200. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  201. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  202. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  204. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  205. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  206. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  207. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  208. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  209. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  210. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  211. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  212. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  214. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  215. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  216. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  217. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  218. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  219. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  220. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  221. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  222. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  223. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  224. package/src/resources/extensions/gsd/types.ts +15 -1
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  226. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
  227. package/src/resources/extensions/subagent/index.ts +5 -0
  228. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -0,0 +1,92 @@
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 registerPdfTools(pi: ExtensionAPI, deps: ToolDeps): void {
6
+ pi.registerTool({
7
+ name: "browser_save_pdf",
8
+ label: "Browser Save PDF",
9
+ description:
10
+ "Render current page as PDF artifact via Playwright's page.pdf(). " +
11
+ "Supports A4/Letter/custom page formats and optional background graphics. " +
12
+ "Writes to session artifacts directory. Chromium only.",
13
+ parameters: Type.Object({
14
+ filename: Type.Optional(
15
+ Type.String({ description: "Output filename (default: auto-generated from page title + timestamp)." }),
16
+ ),
17
+ format: Type.Optional(
18
+ Type.String({
19
+ description:
20
+ "Page format: 'A4' (default), 'Letter', 'Legal', 'Tabloid', or custom like '8.5in x 11in'. " +
21
+ "Custom format uses CSS dimension syntax for width x height.",
22
+ }),
23
+ ),
24
+ printBackground: Type.Optional(
25
+ Type.Boolean({ description: "Include background graphics (default: true)." }),
26
+ ),
27
+ }),
28
+
29
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
30
+ try {
31
+ const { page: p } = await deps.ensureBrowser();
32
+
33
+ const url = p.url();
34
+ const title = await p.title().catch(() => "untitled");
35
+
36
+ // Resolve filename
37
+ const timestamp = deps.formatArtifactTimestamp(Date.now());
38
+ const safeName = deps.sanitizeArtifactName(params.filename || `${title}-${timestamp}`, `pdf-${timestamp}`);
39
+ const filename = safeName.endsWith(".pdf") ? safeName : `${safeName}.pdf`;
40
+
41
+ // Resolve format
42
+ const knownFormats = new Set(["A4", "Letter", "Legal", "Tabloid", "Ledger", "A0", "A1", "A2", "A3", "A5", "A6"]);
43
+ const formatInput = params.format ?? "A4";
44
+ let pdfOptions: Record<string, unknown> = {};
45
+
46
+ if (knownFormats.has(formatInput)) {
47
+ pdfOptions.format = formatInput;
48
+ } else {
49
+ // Custom format: parse "WIDTHin x HEIGHTin" or "WIDTHcm x HEIGHTcm" etc.
50
+ const customMatch = formatInput.match(/^(.+?)\s*[xX×]\s*(.+)$/);
51
+ if (customMatch) {
52
+ pdfOptions.width = customMatch[1]!.trim();
53
+ pdfOptions.height = customMatch[2]!.trim();
54
+ } else {
55
+ pdfOptions.format = "A4"; // fallback
56
+ }
57
+ }
58
+
59
+ pdfOptions.printBackground = params.printBackground ?? true;
60
+
61
+ // Generate PDF
62
+ await deps.ensureSessionArtifactDir();
63
+ const outputPath = deps.buildSessionArtifactPath(filename);
64
+ pdfOptions.path = outputPath;
65
+
66
+ await p.pdf(pdfOptions as any);
67
+
68
+ // Read file size
69
+ const { stat } = await import("node:fs/promises");
70
+ const fileStat = await stat(outputPath);
71
+ const sizeBytes = fileStat.size;
72
+ const sizeKB = (sizeBytes / 1024).toFixed(1);
73
+
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: `PDF saved: ${outputPath}\nSize: ${sizeKB} KB\nFormat: ${formatInput}\nPage: ${title}\nURL: ${url}`,
79
+ },
80
+ ],
81
+ details: { path: outputPath, sizeBytes, format: formatInput, pageUrl: url, pageTitle: title },
82
+ };
83
+ } catch (err: any) {
84
+ return {
85
+ content: [{ type: "text", text: `PDF generation failed: ${err.message}` }],
86
+ details: { error: err.message },
87
+ isError: true,
88
+ };
89
+ }
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,202 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import type { ToolDeps } from "../state.js";
4
+
5
+ /**
6
+ * State persistence tools — save/restore cookies, localStorage, sessionStorage.
7
+ */
8
+
9
+ const STATE_DIR = ".gsd/browser-state";
10
+
11
+ export function registerStatePersistenceTools(pi: ExtensionAPI, deps: ToolDeps): void {
12
+ // -------------------------------------------------------------------------
13
+ // browser_save_state
14
+ // -------------------------------------------------------------------------
15
+ pi.registerTool({
16
+ name: "browser_save_state",
17
+ label: "Browser Save State",
18
+ description:
19
+ "Save cookies, localStorage, and sessionStorage to disk so authenticated sessions survive browser restarts. " +
20
+ "State files are written to .gsd/browser-state/ and should be gitignored (may contain auth tokens). " +
21
+ "Never displays secret values in output.",
22
+ parameters: Type.Object({
23
+ name: Type.Optional(
24
+ Type.String({ description: "Name for the state file (default: 'default'). Used as the filename stem." }),
25
+ ),
26
+ }),
27
+
28
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
29
+ try {
30
+ const { context: ctx, page: p } = await deps.ensureBrowser();
31
+ const name = deps.sanitizeArtifactName(params.name ?? "default", "default");
32
+
33
+ const { mkdir, writeFile } = await import("node:fs/promises");
34
+ const path = await import("node:path");
35
+ const stateDir = path.resolve(process.cwd(), STATE_DIR);
36
+ await mkdir(stateDir, { recursive: true });
37
+
38
+ // 1. Playwright storageState: cookies + localStorage
39
+ const storageState = await ctx.storageState();
40
+
41
+ // 2. sessionStorage: must be extracted per-origin via page.evaluate
42
+ const sessionStorageData: Record<string, Record<string, string>> = {};
43
+ try {
44
+ const origin = new URL(p.url()).origin;
45
+ const ssData = await p.evaluate(() => {
46
+ const data: Record<string, string> = {};
47
+ for (let i = 0; i < sessionStorage.length; i++) {
48
+ const key = sessionStorage.key(i);
49
+ if (key) data[key] = sessionStorage.getItem(key) ?? "";
50
+ }
51
+ return data;
52
+ });
53
+ if (Object.keys(ssData).length > 0) {
54
+ sessionStorageData[origin] = ssData;
55
+ }
56
+ } catch {
57
+ // Page may not have a valid origin (about:blank, etc.)
58
+ }
59
+
60
+ const combined = {
61
+ storageState,
62
+ sessionStorage: sessionStorageData,
63
+ savedAt: new Date().toISOString(),
64
+ url: p.url(),
65
+ };
66
+
67
+ const filePath = path.join(stateDir, `${name}.json`);
68
+ await writeFile(filePath, JSON.stringify(combined, null, 2));
69
+
70
+ // Ensure .gitignore covers the state dir
71
+ const gitignorePath = path.resolve(process.cwd(), STATE_DIR, ".gitignore");
72
+ await writeFile(gitignorePath, "*\n!.gitignore\n").catch(() => {});
73
+
74
+ const cookieCount = storageState.cookies?.length ?? 0;
75
+ const localStorageOrigins = storageState.origins?.length ?? 0;
76
+ const sessionStorageOrigins = Object.keys(sessionStorageData).length;
77
+
78
+ return {
79
+ content: [{
80
+ type: "text",
81
+ text: `State saved: ${filePath}\nCookies: ${cookieCount}\nlocalStorage origins: ${localStorageOrigins}\nsessionStorage origins: ${sessionStorageOrigins}`,
82
+ }],
83
+ details: {
84
+ path: filePath,
85
+ cookieCount,
86
+ localStorageOrigins,
87
+ sessionStorageOrigins,
88
+ },
89
+ };
90
+ } catch (err: any) {
91
+ return {
92
+ content: [{ type: "text", text: `Save state failed: ${err.message}` }],
93
+ details: { error: err.message },
94
+ isError: true,
95
+ };
96
+ }
97
+ },
98
+ });
99
+
100
+ // -------------------------------------------------------------------------
101
+ // browser_restore_state
102
+ // -------------------------------------------------------------------------
103
+ pi.registerTool({
104
+ name: "browser_restore_state",
105
+ label: "Browser Restore State",
106
+ description:
107
+ "Restore cookies, localStorage, and sessionStorage from a previously saved state file. " +
108
+ "Injects cookies via context.addCookies() and storage via page.evaluate(). " +
109
+ "For full fidelity, restore before navigating to the target site.",
110
+ parameters: Type.Object({
111
+ name: Type.Optional(
112
+ Type.String({ description: "Name of the state file to restore (default: 'default')." }),
113
+ ),
114
+ }),
115
+
116
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
117
+ try {
118
+ const { context: ctx, page: p } = await deps.ensureBrowser();
119
+ const name = deps.sanitizeArtifactName(params.name ?? "default", "default");
120
+
121
+ const { readFile } = await import("node:fs/promises");
122
+ const path = await import("node:path");
123
+ const filePath = path.join(process.cwd(), STATE_DIR, `${name}.json`);
124
+
125
+ let raw: string;
126
+ try {
127
+ raw = await readFile(filePath, "utf-8");
128
+ } catch {
129
+ return {
130
+ content: [{ type: "text", text: `State file not found: ${filePath}` }],
131
+ details: { error: "file_not_found", path: filePath },
132
+ isError: true,
133
+ };
134
+ }
135
+
136
+ const combined = JSON.parse(raw);
137
+ const storageState = combined.storageState;
138
+ const sessionStorageData: Record<string, Record<string, string>> = combined.sessionStorage ?? {};
139
+
140
+ // 1. Restore cookies
141
+ let cookieCount = 0;
142
+ if (storageState?.cookies?.length) {
143
+ await ctx.addCookies(storageState.cookies);
144
+ cookieCount = storageState.cookies.length;
145
+ }
146
+
147
+ // 2. Restore localStorage via page.evaluate
148
+ let localStorageOrigins = 0;
149
+ if (storageState?.origins?.length) {
150
+ for (const origin of storageState.origins) {
151
+ try {
152
+ await p.evaluate((items: Array<{ name: string; value: string }>) => {
153
+ for (const { name, value } of items) {
154
+ localStorage.setItem(name, value);
155
+ }
156
+ }, origin.localStorage ?? []);
157
+ localStorageOrigins++;
158
+ } catch {
159
+ // Origin mismatch — localStorage can only be set on matching origin
160
+ }
161
+ }
162
+ }
163
+
164
+ // 3. Restore sessionStorage via page.evaluate
165
+ let sessionStorageOrigins = 0;
166
+ for (const [_origin, data] of Object.entries(sessionStorageData)) {
167
+ try {
168
+ await p.evaluate((items: Record<string, string>) => {
169
+ for (const [key, value] of Object.entries(items)) {
170
+ sessionStorage.setItem(key, value);
171
+ }
172
+ }, data);
173
+ sessionStorageOrigins++;
174
+ } catch {
175
+ // Origin mismatch
176
+ }
177
+ }
178
+
179
+ return {
180
+ content: [{
181
+ type: "text",
182
+ text: `State restored from: ${filePath}\nCookies: ${cookieCount}\nlocalStorage origins: ${localStorageOrigins}\nsessionStorage origins: ${sessionStorageOrigins}\nSaved at: ${combined.savedAt ?? "unknown"}`,
183
+ }],
184
+ details: {
185
+ path: filePath,
186
+ cookieCount,
187
+ localStorageOrigins,
188
+ sessionStorageOrigins,
189
+ savedAt: combined.savedAt,
190
+ savedUrl: combined.url,
191
+ },
192
+ };
193
+ } catch (err: any) {
194
+ return {
195
+ content: [{ type: "text", text: `Restore state failed: ${err.message}` }],
196
+ details: { error: err.message },
197
+ isError: true,
198
+ };
199
+ }
200
+ },
201
+ });
202
+ }
@@ -0,0 +1,209 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import type { ToolDeps } from "../state.js";
4
+
5
+ /**
6
+ * Visual regression diffing — compare current page screenshot against a stored baseline.
7
+ */
8
+
9
+ const BASELINE_DIR = ".gsd/browser-baselines";
10
+
11
+ export function registerVisualDiffTools(pi: ExtensionAPI, deps: ToolDeps): void {
12
+ pi.registerTool({
13
+ name: "browser_visual_diff",
14
+ label: "Browser Visual Diff",
15
+ description:
16
+ "Compare current page screenshot against a stored baseline pixel-by-pixel. " +
17
+ "Returns similarity score (0–1), diff pixel count, and optionally generates a diff image highlighting changes. " +
18
+ "On first run with no baseline, saves the current screenshot as the baseline. " +
19
+ "Baselines are stored in .gsd/browser-baselines/ (gitignored, environment-specific).",
20
+ parameters: Type.Object({
21
+ name: Type.Optional(
22
+ Type.String({
23
+ description:
24
+ "Baseline name (default: auto-generated from URL + viewport). " +
25
+ "Use consistent names to compare the same view across runs.",
26
+ }),
27
+ ),
28
+ selector: Type.Optional(
29
+ Type.String({
30
+ description: "CSS selector to scope comparison to a specific element instead of full viewport.",
31
+ }),
32
+ ),
33
+ threshold: Type.Optional(
34
+ Type.Number({
35
+ description:
36
+ "Pixel matching threshold 0–1 (default: 0.1). " +
37
+ "Higher values are more tolerant of anti-aliasing and rendering differences.",
38
+ }),
39
+ ),
40
+ updateBaseline: Type.Optional(
41
+ Type.Boolean({
42
+ description: "If true, overwrite the existing baseline with the current screenshot (default: false).",
43
+ }),
44
+ ),
45
+ }),
46
+
47
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
48
+ try {
49
+ const { page: p } = await deps.ensureBrowser();
50
+ const { mkdir, readFile, writeFile } = await import("node:fs/promises");
51
+ const pathMod = await import("node:path");
52
+
53
+ const baselineDir = pathMod.resolve(process.cwd(), BASELINE_DIR);
54
+ await mkdir(baselineDir, { recursive: true });
55
+
56
+ // Ensure .gitignore
57
+ const gitignorePath = pathMod.join(baselineDir, ".gitignore");
58
+ await writeFile(gitignorePath, "*\n!.gitignore\n").catch(() => {});
59
+
60
+ // Generate baseline name
61
+ const url = p.url();
62
+ const viewport = p.viewportSize();
63
+ const vpSuffix = viewport ? `${viewport.width}x${viewport.height}` : "unknown";
64
+ const autoName = deps.sanitizeArtifactName(
65
+ `${new URL(url).pathname.replace(/\//g, "-")}-${vpSuffix}`,
66
+ `baseline-${vpSuffix}`,
67
+ );
68
+ const name = deps.sanitizeArtifactName(params.name ?? autoName, autoName);
69
+
70
+ const baselinePath = pathMod.join(baselineDir, `${name}.png`);
71
+ const diffPath = pathMod.join(baselineDir, `${name}-diff.png`);
72
+
73
+ // Capture current screenshot as PNG (needed for pixel comparison)
74
+ let currentBuffer: Buffer;
75
+ if (params.selector) {
76
+ const locator = p.locator(params.selector).first();
77
+ currentBuffer = await locator.screenshot({ type: "png" });
78
+ } else {
79
+ currentBuffer = await p.screenshot({ type: "png", fullPage: false });
80
+ }
81
+
82
+ // Check if baseline exists
83
+ let baselineBuffer: Buffer | null = null;
84
+ try {
85
+ baselineBuffer = await readFile(baselinePath) as Buffer;
86
+ } catch {
87
+ // No baseline yet
88
+ }
89
+
90
+ if (!baselineBuffer || params.updateBaseline) {
91
+ // Save as new baseline
92
+ await writeFile(baselinePath, currentBuffer);
93
+ return {
94
+ content: [{
95
+ type: "text",
96
+ text: baselineBuffer
97
+ ? `Baseline updated: ${baselinePath}\nSize: ${(currentBuffer.length / 1024).toFixed(1)} KB`
98
+ : `Baseline created (first run): ${baselinePath}\nSize: ${(currentBuffer.length / 1024).toFixed(1)} KB\nRe-run to compare against this baseline.`,
99
+ }],
100
+ details: {
101
+ baselinePath,
102
+ baselineCreated: !baselineBuffer,
103
+ baselineUpdated: !!baselineBuffer,
104
+ sizeBytes: currentBuffer.length,
105
+ },
106
+ };
107
+ }
108
+
109
+ // Perform pixel comparison using sharp for PNG decoding
110
+ const sharp = (await import("sharp")).default;
111
+
112
+ const baselineMeta = await sharp(baselineBuffer).metadata();
113
+ const currentMeta = await sharp(currentBuffer).metadata();
114
+
115
+ const bWidth = baselineMeta.width ?? 0;
116
+ const bHeight = baselineMeta.height ?? 0;
117
+ const cWidth = currentMeta.width ?? 0;
118
+ const cHeight = currentMeta.height ?? 0;
119
+
120
+ // If dimensions differ, report mismatch
121
+ if (bWidth !== cWidth || bHeight !== cHeight) {
122
+ return {
123
+ content: [{
124
+ type: "text",
125
+ text: `Dimension mismatch: baseline is ${bWidth}x${bHeight}, current is ${cWidth}x${cHeight}. Cannot compare.\nUse updateBaseline: true to reset.`,
126
+ }],
127
+ details: {
128
+ match: false,
129
+ dimensionMismatch: true,
130
+ baselineDimensions: { width: bWidth, height: bHeight },
131
+ currentDimensions: { width: cWidth, height: cHeight },
132
+ },
133
+ };
134
+ }
135
+
136
+ // Extract raw RGBA pixel data
137
+ const baselineRaw = await sharp(baselineBuffer).ensureAlpha().raw().toBuffer();
138
+ const currentRaw = await sharp(currentBuffer).ensureAlpha().raw().toBuffer();
139
+
140
+ const width = bWidth;
141
+ const height = bHeight;
142
+ const totalPixels = width * height;
143
+ const threshold = params.threshold ?? 0.1;
144
+
145
+ // Simple pixel-by-pixel comparison (avoiding pixelmatch dependency)
146
+ const diffData = Buffer.alloc(width * height * 4);
147
+ let diffPixels = 0;
148
+ const thresholdSq = threshold * threshold * 255 * 255 * 3;
149
+
150
+ for (let i = 0; i < totalPixels; i++) {
151
+ const offset = i * 4;
152
+ const dr = baselineRaw[offset] - currentRaw[offset];
153
+ const dg = baselineRaw[offset + 1] - currentRaw[offset + 1];
154
+ const db = baselineRaw[offset + 2] - currentRaw[offset + 2];
155
+ const distSq = dr * dr + dg * dg + db * db;
156
+
157
+ if (distSq > thresholdSq) {
158
+ diffPixels++;
159
+ // Mark diff pixels as red
160
+ diffData[offset] = 255; // R
161
+ diffData[offset + 1] = 0; // G
162
+ diffData[offset + 2] = 0; // B
163
+ diffData[offset + 3] = 255; // A
164
+ } else {
165
+ // Dim unchanged pixels
166
+ diffData[offset] = currentRaw[offset] >> 1;
167
+ diffData[offset + 1] = currentRaw[offset + 1] >> 1;
168
+ diffData[offset + 2] = currentRaw[offset + 2] >> 1;
169
+ diffData[offset + 3] = 255;
170
+ }
171
+ }
172
+
173
+ const similarity = 1 - (diffPixels / totalPixels);
174
+ const match = diffPixels === 0;
175
+
176
+ // Save diff image
177
+ await sharp(diffData, { raw: { width, height, channels: 4 } })
178
+ .png()
179
+ .toFile(diffPath);
180
+
181
+ return {
182
+ content: [{
183
+ type: "text",
184
+ text: match
185
+ ? `Visual diff: MATCH (100% similar)\nBaseline: ${baselinePath}`
186
+ : `Visual diff: ${(similarity * 100).toFixed(2)}% similar\nDiff pixels: ${diffPixels} of ${totalPixels} (${((diffPixels / totalPixels) * 100).toFixed(2)}%)\nDiff image: ${diffPath}\nBaseline: ${baselinePath}`,
187
+ }],
188
+ details: {
189
+ match,
190
+ similarity,
191
+ diffPixels,
192
+ totalPixels,
193
+ diffPercentage: (diffPixels / totalPixels) * 100,
194
+ dimensions: { width, height },
195
+ baselinePath,
196
+ diffImagePath: match ? undefined : diffPath,
197
+ threshold,
198
+ },
199
+ };
200
+ } catch (err: any) {
201
+ return {
202
+ content: [{ type: "text", text: `Visual diff failed: ${err.message}` }],
203
+ details: { error: err.message },
204
+ isError: true,
205
+ };
206
+ }
207
+ },
208
+ });
209
+ }
@@ -0,0 +1,104 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import type { ToolDeps } from "../state.js";
4
+
5
+ /**
6
+ * Region zoom / high-res capture — capture and upscale specific page regions.
7
+ */
8
+
9
+ export function registerZoomTools(pi: ExtensionAPI, deps: ToolDeps): void {
10
+ pi.registerTool({
11
+ name: "browser_zoom_region",
12
+ label: "Browser Zoom Region",
13
+ description:
14
+ "Capture and optionally upscale a specific rectangular region of the page for detailed inspection. " +
15
+ "Useful for dense UIs where full-page screenshots have text too small to read. " +
16
+ "Returns the region as an inline image, same as browser_screenshot.",
17
+ parameters: Type.Object({
18
+ x: Type.Number({ description: "Left coordinate of the region in CSS pixels." }),
19
+ y: Type.Number({ description: "Top coordinate of the region in CSS pixels." }),
20
+ width: Type.Number({ description: "Width of the region in CSS pixels." }),
21
+ height: Type.Number({ description: "Height of the region in CSS pixels." }),
22
+ scale: Type.Optional(
23
+ Type.Number({
24
+ description: "Upscale factor (default: 2). Use 1 for native resolution, 2-4 for zoomed detail.",
25
+ }),
26
+ ),
27
+ }),
28
+
29
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
30
+ try {
31
+ const { page: p } = await deps.ensureBrowser();
32
+ const { x, y, width, height } = params;
33
+ const scale = params.scale ?? 2;
34
+
35
+ // Validate dimensions
36
+ if (width <= 0 || height <= 0) {
37
+ return {
38
+ content: [{ type: "text", text: "Width and height must be positive." }],
39
+ details: { error: "invalid_dimensions" },
40
+ isError: true,
41
+ };
42
+ }
43
+
44
+ // Capture the region using Playwright's clip option
45
+ const regionBuffer = await p.screenshot({
46
+ type: "png",
47
+ clip: { x, y, width, height },
48
+ });
49
+
50
+ let outputBuffer: Buffer = regionBuffer;
51
+ let outputMime = "image/png";
52
+
53
+ // Upscale if scale > 1
54
+ if (scale > 1) {
55
+ const sharp = (await import("sharp")).default;
56
+ const targetWidth = Math.round(width * scale);
57
+ const targetHeight = Math.round(height * scale);
58
+
59
+ outputBuffer = await sharp(regionBuffer)
60
+ .resize(targetWidth, targetHeight, {
61
+ kernel: "lanczos3",
62
+ fit: "fill",
63
+ })
64
+ .png()
65
+ .toBuffer();
66
+ }
67
+
68
+ const base64Data = outputBuffer.toString("base64");
69
+ const title = await p.title();
70
+ const url = p.url();
71
+
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: `Region capture: ${width}x${height} at (${x},${y})${scale > 1 ? ` upscaled ${scale}x to ${Math.round(width * scale)}x${Math.round(height * scale)}` : ""}\nPage: ${title}\nURL: ${url}`,
77
+ },
78
+ {
79
+ type: "image",
80
+ data: base64Data,
81
+ mimeType: outputMime,
82
+ },
83
+ ],
84
+ details: {
85
+ region: { x, y, width, height },
86
+ scale,
87
+ outputDimensions: {
88
+ width: Math.round(width * scale),
89
+ height: Math.round(height * scale),
90
+ },
91
+ title,
92
+ url,
93
+ },
94
+ };
95
+ } catch (err: any) {
96
+ return {
97
+ content: [{ type: "text", text: `Region zoom failed: ${err.message}` }],
98
+ details: { error: err.message },
99
+ isError: true,
100
+ };
101
+ }
102
+ },
103
+ });
104
+ }
@@ -41,6 +41,8 @@ export interface AutoDashboardData {
41
41
  profileDowngraded?: boolean;
42
42
  /** Number of pending captures awaiting triage (0 if none or file missing) */
43
43
  pendingCaptureCount: number;
44
+ /** Cross-process: another auto-mode session detected via auto.lock (PID, startedAt) */
45
+ remoteSession?: { pid: number; startedAt: string; unitType: string; unitId: string };
44
46
  }
45
47
 
46
48
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
@@ -14,9 +14,11 @@ import type { GSDPreferences } from "./preferences.js";
14
14
  import type { UatType } from "./files.js";
15
15
  import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
16
16
  import {
17
- resolveMilestoneFile, resolveSliceFile,
18
- relSliceFile,
17
+ resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
18
+ relSliceFile, buildMilestoneFileName,
19
19
  } from "./paths.js";
20
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
21
+ import { join } from "node:path";
20
22
  import {
21
23
  buildResearchMilestonePrompt,
22
24
  buildPlanMilestonePrompt,
@@ -25,6 +27,7 @@ import {
25
27
  buildExecuteTaskPrompt,
26
28
  buildCompleteSlicePrompt,
27
29
  buildCompleteMilestonePrompt,
30
+ buildValidateMilestonePrompt,
28
31
  buildReplanSlicePrompt,
29
32
  buildRunUatPrompt,
30
33
  buildReassessRoadmapPrompt,
@@ -246,6 +249,20 @@ const DISPATCH_RULES: DispatchRule[] = [
246
249
  const sTitle = state.activeSlice!.title;
247
250
  const tid = state.activeTask.id;
248
251
  const tTitle = state.activeTask.title;
252
+
253
+ // Guard: refuse to dispatch execute-task when the task plan file is missing.
254
+ // This prevents the agent from running blind after a failed plan-slice that
255
+ // wrote S{sid}-PLAN.md but omitted the individual T{tid}-PLAN.md files.
256
+ // (See issue #739 — missing task plan caused runaway execution and EPIPE crash.)
257
+ const taskPlanPath = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
258
+ if (!taskPlanPath || !existsSync(taskPlanPath)) {
259
+ return {
260
+ action: "stop",
261
+ reason: `Task plan ${tid}-PLAN.md is missing for ${mid}/${sid}/${tid}. Re-run plan-slice to regenerate task plans, or create the file manually and resume.`,
262
+ level: "error",
263
+ };
264
+ }
265
+
249
266
  return {
250
267
  action: "dispatch",
251
268
  unitType: "execute-task",
@@ -254,6 +271,38 @@ const DISPATCH_RULES: DispatchRule[] = [
254
271
  };
255
272
  },
256
273
  },
274
+ {
275
+ name: "validating-milestone → validate-milestone",
276
+ match: async ({ state, mid, midTitle, basePath, prefs }) => {
277
+ if (state.phase !== "validating-milestone") return null;
278
+ // Skip preference: write a minimal pass-through VALIDATION file
279
+ if (prefs?.phases?.skip_milestone_validation) {
280
+ const mDir = resolveMilestonePath(basePath, mid);
281
+ if (mDir) {
282
+ if (!existsSync(mDir)) mkdirSync(mDir, { recursive: true });
283
+ const validationPath = join(mDir, buildMilestoneFileName(mid, "VALIDATION"));
284
+ const content = [
285
+ "---",
286
+ "verdict: pass",
287
+ "remediation_round: 0",
288
+ "---",
289
+ "",
290
+ "# Milestone Validation (skipped by preference)",
291
+ "",
292
+ "Milestone validation was skipped via `skip_milestone_validation` preference.",
293
+ ].join("\n");
294
+ writeFileSync(validationPath, content, "utf-8");
295
+ }
296
+ return { action: "skip" };
297
+ }
298
+ return {
299
+ action: "dispatch",
300
+ unitType: "validate-milestone",
301
+ unitId: mid,
302
+ prompt: await buildValidateMilestonePrompt(mid, midTitle, basePath),
303
+ };
304
+ },
305
+ },
257
306
  {
258
307
  name: "completing-milestone → complete-milestone",
259
308
  match: async ({ state, mid, midTitle, basePath }) => {