gsd-pi 2.38.0-dev.5492881 → 2.38.0-dev.7209774

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.
@@ -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
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, loadActiveOverrides, formatOverridesSection } from "./files.js";
9
9
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
10
- import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
10
+ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
11
11
  import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
12
12
  import { join } from "node:path";
13
13
  import { existsSync } from "node:fs";
@@ -767,8 +767,15 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
767
767
  if (carryForwardSection.length > carryForwardBudget) {
768
768
  finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
769
769
  }
770
+ // Inline RUNTIME.md if present
771
+ const runtimePath = resolveRuntimeFile(base);
772
+ const runtimeContent = existsSync(runtimePath) ? await loadFile(runtimePath) : null;
773
+ const runtimeContext = runtimeContent
774
+ ? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
775
+ : "";
770
776
  return loadPrompt("execute-task", {
771
777
  overridesSection,
778
+ runtimeContext,
772
779
  workingDirectory: base,
773
780
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
774
781
  planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
@@ -343,6 +343,9 @@ function probeGsdRoot(rawBasePath) {
343
343
  export function milestonesDir(basePath) {
344
344
  return join(gsdRoot(basePath), "milestones");
345
345
  }
346
+ export function resolveRuntimeFile(basePath) {
347
+ return join(gsdRoot(basePath), "RUNTIME.md");
348
+ }
346
349
  export function resolveGsdRootFile(basePath, key) {
347
350
  const root = gsdRoot(basePath);
348
351
  const canonical = join(root, GSD_ROOT_FILES[key]);
@@ -10,6 +10,8 @@ A researcher explored the codebase and a planner decomposed the work — you are
10
10
 
11
11
  {{overridesSection}}
12
12
 
13
+ {{runtimeContext}}
14
+
13
15
  {{resumeSection}}
14
16
 
15
17
  {{carryForwardSection}}
@@ -0,0 +1,21 @@
1
+ # Runtime Context
2
+
3
+ ## Stack
4
+ - **Language:** (e.g., TypeScript, Python, Go)
5
+ - **Framework:** (e.g., Next.js, FastAPI, Gin)
6
+ - **Build:** (e.g., npm run build, cargo build)
7
+ - **Test:** (e.g., npm run test, pytest)
8
+ - **Lint:** (e.g., npm run lint, ruff check)
9
+
10
+ ## Environment
11
+ - **Node version:** (e.g., 20.x)
12
+ - **Package manager:** (e.g., npm, pnpm, yarn)
13
+ - **Required env vars:** (list any needed for local dev)
14
+
15
+ ## Dev Server
16
+ - **Start command:** (e.g., npm run dev)
17
+ - **Default port:** (e.g., 3000)
18
+ - **Health check:** (e.g., curl http://localhost:3000/health)
19
+
20
+ ## Notes
21
+ (Any runtime-specific context the executor needs to know)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.38.0-dev.5492881",
3
+ "version": "2.38.0-dev.7209774",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
+ }
@@ -13,7 +13,7 @@ import {
13
13
  resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
14
14
  resolveTasksDir, resolveTaskFiles, resolveTaskFile,
15
15
  relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
16
- resolveGsdRootFile, relGsdRootFile,
16
+ resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
17
17
  } from "./paths.js";
18
18
  import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
19
19
  import type { GSDState, InlineLevel } from "./types.js";
@@ -891,8 +891,16 @@ export async function buildExecuteTaskPrompt(
891
891
  finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
892
892
  }
893
893
 
894
+ // Inline RUNTIME.md if present
895
+ const runtimePath = resolveRuntimeFile(base);
896
+ const runtimeContent = existsSync(runtimePath) ? await loadFile(runtimePath) : null;
897
+ const runtimeContext = runtimeContent
898
+ ? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
899
+ : "";
900
+
894
901
  return loadPrompt("execute-task", {
895
902
  overridesSection,
903
+ runtimeContext,
896
904
  workingDirectory: base,
897
905
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
898
906
  planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
@@ -356,6 +356,10 @@ export function milestonesDir(basePath: string): string {
356
356
  return join(gsdRoot(basePath), "milestones");
357
357
  }
358
358
 
359
+ export function resolveRuntimeFile(basePath: string): string {
360
+ return join(gsdRoot(basePath), "RUNTIME.md");
361
+ }
362
+
359
363
  export function resolveGsdRootFile(basePath: string, key: GSDRootFileKey): string {
360
364
  const root = gsdRoot(basePath);
361
365
  const canonical = join(root, GSD_ROOT_FILES[key]);
@@ -10,6 +10,8 @@ A researcher explored the codebase and a planner decomposed the work — you are
10
10
 
11
11
  {{overridesSection}}
12
12
 
13
+ {{runtimeContext}}
14
+
13
15
  {{resumeSection}}
14
16
 
15
17
  {{carryForwardSection}}
@@ -0,0 +1,21 @@
1
+ # Runtime Context
2
+
3
+ ## Stack
4
+ - **Language:** (e.g., TypeScript, Python, Go)
5
+ - **Framework:** (e.g., Next.js, FastAPI, Gin)
6
+ - **Build:** (e.g., npm run build, cargo build)
7
+ - **Test:** (e.g., npm run test, pytest)
8
+ - **Lint:** (e.g., npm run lint, ruff check)
9
+
10
+ ## Environment
11
+ - **Node version:** (e.g., 20.x)
12
+ - **Package manager:** (e.g., npm, pnpm, yarn)
13
+ - **Required env vars:** (list any needed for local dev)
14
+
15
+ ## Dev Server
16
+ - **Start command:** (e.g., npm run dev)
17
+ - **Default port:** (e.g., 3000)
18
+ - **Health check:** (e.g., curl http://localhost:3000/health)
19
+
20
+ ## Notes
21
+ (Any runtime-specific context the executor needs to know)
@@ -478,3 +478,11 @@ export interface ReactiveExecutionState {
478
478
  };
479
479
  updatedAt: string;
480
480
  }
481
+
482
+ export interface BrowserFlowResult {
483
+ url: string;
484
+ passed: boolean;
485
+ checksTotal: number;
486
+ checksPassed: number;
487
+ duration: number;
488
+ }
@@ -37,6 +37,21 @@ export interface AuditWarningJSON {
37
37
  fixAvailable: boolean;
38
38
  }
39
39
 
40
+ export interface BrowserEvidenceCheckJSON {
41
+ description: string;
42
+ passed: boolean;
43
+ actual?: string;
44
+ evidence?: string;
45
+ error?: string;
46
+ }
47
+
48
+ export interface BrowserEvidenceJSON {
49
+ url: string;
50
+ passed: boolean;
51
+ checks: BrowserEvidenceCheckJSON[];
52
+ duration: number;
53
+ }
54
+
40
55
  export interface EvidenceJSON {
41
56
  schemaVersion: 1;
42
57
  taskId: string;
@@ -49,6 +64,7 @@ export interface EvidenceJSON {
49
64
  maxRetries?: number;
50
65
  runtimeErrors?: RuntimeErrorJSON[];
51
66
  auditWarnings?: AuditWarningJSON[];
67
+ browser?: BrowserEvidenceJSON;
52
68
  }
53
69
 
54
70
  /**