@opengsd/gsd-pi 1.1.1-dev.2034b16 → 1.1.1-dev.2de7ea0

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 (142) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/help-text.js +10 -6
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +495 -0
  5. package/dist/resources/extensions/browser-tools/engine/selection.js +16 -0
  6. package/dist/resources/extensions/browser-tools/extension-manifest.json +2 -2
  7. package/dist/resources/extensions/browser-tools/index.js +57 -9
  8. package/dist/resources/extensions/browser-tools/package.json +5 -1
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +21 -3
  10. package/dist/resources/extensions/gsd/auto-prompts.js +15 -6
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
  12. package/dist/resources/extensions/gsd/browser-evidence.js +29 -2
  13. package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -2
  14. package/dist/resources/extensions/gsd/commands-handlers.js +76 -11
  15. package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -1
  16. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -0
  17. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +2 -2
  18. package/dist/resources/extensions/gsd/mcp-project-config.js +9 -76
  19. package/dist/resources/extensions/gsd/post-unit-hooks.js +9 -0
  20. package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
  21. package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
  22. package/dist/resources/extensions/gsd/prompts/run-uat.md +40 -22
  23. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
  24. package/dist/resources/extensions/gsd/rule-registry.js +428 -52
  25. package/dist/resources/extensions/gsd/tools/validate-milestone.js +46 -16
  26. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +29 -14
  27. package/dist/resources/extensions/gsd/verdict-parser.js +59 -15
  28. package/dist/resources/extensions/shared/gsd-browser-cli.js +145 -0
  29. package/dist/rtk.d.ts +7 -1
  30. package/dist/rtk.js +27 -11
  31. package/dist/update-check.d.ts +15 -1
  32. package/dist/update-check.js +87 -12
  33. package/dist/update-cmd.d.ts +1 -0
  34. package/dist/update-cmd.js +53 -2
  35. package/dist/web/standalone/.next/BUILD_ID +1 -1
  36. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  37. package/dist/web/standalone/.next/build-manifest.json +2 -2
  38. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  39. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/index.html +1 -1
  57. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  64. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  65. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  67. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  68. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  69. package/package.json +3 -2
  70. package/packages/cloud-mcp-gateway/package.json +2 -2
  71. package/packages/contracts/package.json +1 -1
  72. package/packages/daemon/package.json +4 -4
  73. package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts +2 -0
  74. package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts.map +1 -1
  75. package/packages/gsd-agent-core/dist/session/agent-session-compaction.js +8 -2
  76. package/packages/gsd-agent-core/dist/session/agent-session-compaction.js.map +1 -1
  77. package/packages/gsd-agent-core/package.json +5 -5
  78. package/packages/gsd-agent-modes/package.json +7 -7
  79. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  80. package/packages/mcp-server/dist/remote-questions.js +23 -9
  81. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  82. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  83. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  84. package/packages/mcp-server/package.json +3 -3
  85. package/packages/native/package.json +1 -1
  86. package/packages/pi-agent-core/package.json +1 -1
  87. package/packages/pi-ai/dist/models.generated.d.ts +17 -0
  88. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  89. package/packages/pi-ai/dist/models.generated.js +19 -2
  90. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  91. package/packages/pi-ai/package.json +1 -1
  92. package/packages/pi-coding-agent/package.json +7 -7
  93. package/packages/pi-tui/package.json +1 -1
  94. package/packages/rpc-client/package.json +2 -2
  95. package/pkg/package.json +1 -1
  96. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +579 -0
  97. package/src/resources/extensions/browser-tools/engine/selection.ts +19 -0
  98. package/src/resources/extensions/browser-tools/extension-manifest.json +2 -2
  99. package/src/resources/extensions/browser-tools/index.ts +60 -9
  100. package/src/resources/extensions/browser-tools/package.json +5 -1
  101. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +35 -0
  102. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +33 -0
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +28 -2
  104. package/src/resources/extensions/gsd/auto-prompts.ts +16 -6
  105. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
  106. package/src/resources/extensions/gsd/browser-evidence.ts +26 -2
  107. package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -2
  108. package/src/resources/extensions/gsd/commands-handlers.ts +76 -11
  109. package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -1
  110. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -0
  111. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +2 -2
  112. package/src/resources/extensions/gsd/mcp-project-config.ts +13 -78
  113. package/src/resources/extensions/gsd/post-unit-hooks.ts +14 -1
  114. package/src/resources/extensions/gsd/preferences-validation.ts +36 -0
  115. package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
  116. package/src/resources/extensions/gsd/prompts/run-uat.md +40 -22
  117. package/src/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
  118. package/src/resources/extensions/gsd/rule-registry.ts +558 -58
  119. package/src/resources/extensions/gsd/rule-types.ts +2 -0
  120. package/src/resources/extensions/gsd/tests/browser-evidence.test.ts +142 -0
  121. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +30 -0
  122. package/src/resources/extensions/gsd/tests/doctor-runtime-checks.test.ts +27 -0
  123. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +4 -4
  124. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +66 -10
  125. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +32 -0
  126. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +2 -0
  127. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +157 -0
  128. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +179 -0
  129. package/src/resources/extensions/gsd/tests/preferences.test.ts +29 -0
  130. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +22 -1
  131. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +14 -0
  132. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +75 -0
  133. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +6 -3
  134. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +133 -0
  135. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +74 -0
  136. package/src/resources/extensions/gsd/tools/validate-milestone.ts +46 -15
  137. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +31 -14
  138. package/src/resources/extensions/gsd/types.ts +63 -0
  139. package/src/resources/extensions/gsd/verdict-parser.ts +54 -13
  140. package/src/resources/extensions/shared/gsd-browser-cli.ts +172 -0
  141. /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → JdwzU6IGLVBZPf84PIaJQ}/_buildManifest.js +0 -0
  142. /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → JdwzU6IGLVBZPf84PIaJQ}/_ssgManifest.js +0 -0
@@ -2,7 +2,7 @@
2
2
  "id": "browser-tools",
3
3
  "name": "Browser Tools",
4
4
  "version": "1.0.0",
5
- "description": "Playwright-based web automation, screenshots, and analysis",
5
+ "description": "GSD browser automation contract adapter backed by the managed gsd-browser engine",
6
6
  "tier": "bundled",
7
7
  "requires": { "platform": ">=2.29.0" },
8
8
  "provides": {
@@ -32,6 +32,6 @@
32
32
  "hooks": ["session_start", "session_shutdown"]
33
33
  },
34
34
  "dependencies": {
35
- "runtime": ["playwright"]
35
+ "runtime": ["@opengsd/gsd-browser"]
36
36
  }
37
37
  }
@@ -1,11 +1,16 @@
1
- /** browser-tools — pi extension: full browser interaction via Playwright. */
1
+ /** browser-tools — Pi Browser Automation Contract adapter. */
2
2
  import { importExtensionModule, type ExtensionAPI } from "@gsd/pi-coding-agent";
3
3
 
4
- let registrationPromise: Promise<void> | null = null;
4
+ import { closeManagedGsdBrowser, registerManagedGsdBrowserTools } from "./engine/managed-gsd-browser.js";
5
+ import { resolveBrowserEngineMode, type BrowserEngineMode } from "./engine/selection.js";
5
6
 
6
- async function registerBrowserTools(pi: ExtensionAPI): Promise<void> {
7
- if (!registrationPromise) {
8
- registrationPromise = (async () => {
7
+ let legacyRegistrationPromise: Promise<void> | null = null;
8
+ let managedRegistrationPromise: Promise<void> | null = null;
9
+ let registeredEngine: Exclude<BrowserEngineMode, "off"> | null = null;
10
+
11
+ async function registerLegacyBrowserTools(pi: ExtensionAPI): Promise<void> {
12
+ if (!legacyRegistrationPromise) {
13
+ legacyRegistrationPromise = (async () => {
9
14
  const [
10
15
  lifecycle,
11
16
  capture,
@@ -136,12 +141,55 @@ async function registerBrowserTools(pi: ExtensionAPI): Promise<void> {
136
141
  injectionDetection.registerInjectionDetectionTools(pi, deps);
137
142
  verify.registerVerifyTools(pi, deps);
138
143
  })().catch((error) => {
139
- registrationPromise = null;
144
+ legacyRegistrationPromise = null;
140
145
  throw error;
141
146
  });
142
147
  }
143
148
 
144
- return registrationPromise;
149
+ return legacyRegistrationPromise;
150
+ }
151
+
152
+ async function registerBrowserTools(pi: ExtensionAPI): Promise<void> {
153
+ const engine = resolveBrowserEngineMode();
154
+ if (engine === "off") return;
155
+ if (registeredEngine && registeredEngine !== engine) {
156
+ throw new Error(
157
+ `Browser tools already registered with GSD_BROWSER_ENGINE=${registeredEngine}. Restart GSD before switching to ${engine}.`,
158
+ );
159
+ }
160
+
161
+ let registration: Promise<void>;
162
+ if (engine === "legacy") {
163
+ registration = registerLegacyBrowserTools(pi);
164
+ } else if (!managedRegistrationPromise) {
165
+ managedRegistrationPromise = Promise.resolve()
166
+ .then(() => {
167
+ registerManagedGsdBrowserTools(pi);
168
+ })
169
+ .catch((error) => {
170
+ managedRegistrationPromise = null;
171
+ throw error;
172
+ });
173
+ registration = managedRegistrationPromise;
174
+ } else {
175
+ registration = managedRegistrationPromise;
176
+ }
177
+
178
+ registeredEngine = engine;
179
+ try {
180
+ await registration;
181
+ } catch (error) {
182
+ if (registeredEngine === engine) registeredEngine = null;
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ async function closeActiveBrowserEngines(): Promise<void> {
188
+ await closeManagedGsdBrowser();
189
+ if (legacyRegistrationPromise) {
190
+ const { closeBrowser } = await importExtensionModule<typeof import("./lifecycle.js")>(import.meta.url, "./lifecycle.js");
191
+ await closeBrowser();
192
+ }
145
193
  }
146
194
 
147
195
  export default function (pi: ExtensionAPI) {
@@ -157,7 +205,10 @@ export default function (pi: ExtensionAPI) {
157
205
  });
158
206
 
159
207
  pi.on("session_shutdown", async () => {
160
- const { closeBrowser } = await importExtensionModule<typeof import("./lifecycle.js")>(import.meta.url, "./lifecycle.js");
161
- await closeBrowser();
208
+ await closeActiveBrowserEngines();
209
+ });
210
+
211
+ pi.on("session_switch", async () => {
212
+ await closeActiveBrowserEngines();
162
213
  });
163
214
  }
@@ -4,16 +4,20 @@
4
4
  "version": "1.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test tests/*.test.mjs"
7
+ "test": "node --import ../gsd/tests/resolve-ts.mjs --experimental-strip-types --test tests/*.test.mjs"
8
8
  },
9
9
  "pi": {
10
10
  "extensions": ["./index.ts"]
11
11
  },
12
12
  "peerDependencies": {
13
+ "@opengsd/gsd-browser": ">=0.1.27",
13
14
  "playwright": ">=1.40.0",
14
15
  "sharp": ">=0.33.0"
15
16
  },
16
17
  "peerDependenciesMeta": {
18
+ "@opengsd/gsd-browser": {
19
+ "optional": true
20
+ },
17
21
  "playwright": {
18
22
  "optional": true
19
23
  },
@@ -0,0 +1,35 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createRequire } from "node:module";
4
+ import { dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+ const jiti = require("jiti")(__dirname, { interopDefault: true, debug: false });
10
+
11
+ const { resolveBrowserEngineMode } = jiti("../engine/selection.ts");
12
+
13
+ describe("resolveBrowserEngineMode", () => {
14
+ it("defaults to gsd-browser", () => {
15
+ assert.equal(resolveBrowserEngineMode({}), "gsd-browser");
16
+ });
17
+
18
+ it("accepts the explicit engine modes", () => {
19
+ assert.equal(resolveBrowserEngineMode({ GSD_BROWSER_ENGINE: "gsd-browser" }), "gsd-browser");
20
+ assert.equal(resolveBrowserEngineMode({ GSD_BROWSER_ENGINE: "legacy" }), "legacy");
21
+ assert.equal(resolveBrowserEngineMode({ GSD_BROWSER_ENGINE: "off" }), "off");
22
+ });
23
+
24
+ it("accepts compatibility aliases", () => {
25
+ assert.equal(resolveBrowserEngineMode({ GSD_BROWSER_ENGINE: "playwright" }), "legacy");
26
+ assert.equal(resolveBrowserEngineMode({ GSD_BROWSER_ENGINE: "false" }), "off");
27
+ });
28
+
29
+ it("rejects unknown engine modes", () => {
30
+ assert.throws(
31
+ () => resolveBrowserEngineMode({ GSD_BROWSER_ENGINE: "surprise" }),
32
+ /Expected "gsd-browser", "legacy", or "off"/,
33
+ );
34
+ });
35
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ const {
5
+ MANAGED_GSD_BROWSER_TOOL_NAMES,
6
+ registerManagedGsdBrowserTools,
7
+ } = await import("../engine/managed-gsd-browser.ts");
8
+
9
+ describe("registerManagedGsdBrowserTools", () => {
10
+ it("registers the curated Pi browser contract", () => {
11
+ const tools = [];
12
+ registerManagedGsdBrowserTools({
13
+ registerTool(tool) {
14
+ tools.push(tool);
15
+ },
16
+ });
17
+
18
+ assert.deepEqual(tools.map((tool) => tool.name), [...MANAGED_GSD_BROWSER_TOOL_NAMES]);
19
+ assert.equal(new Set(tools.map((tool) => tool.name)).size, tools.length);
20
+ });
21
+
22
+ it("keeps screenshots marked as image-producing evidence", () => {
23
+ const tools = [];
24
+ registerManagedGsdBrowserTools({
25
+ registerTool(tool) {
26
+ tools.push(tool);
27
+ },
28
+ });
29
+
30
+ const screenshot = tools.find((tool) => tool.name === "browser_screenshot");
31
+ assert.equal(screenshot?.compatibility?.producesImages, true);
32
+ });
33
+ });
@@ -55,8 +55,10 @@ import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
55
55
  import { consumeSignal } from "./session-status-io.js";
56
56
  import {
57
57
  checkPostUnitHooks,
58
+ consumeHookFailure,
58
59
  isRetryPending,
59
60
  consumeRetryTrigger,
61
+ consumeGateBlock,
60
62
  persistHookState,
61
63
  resolveHookArtifactPath,
62
64
  } from "./post-unit-hooks.js";
@@ -2227,11 +2229,11 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
2227
2229
  // ── Post-unit hooks ──
2228
2230
  if (s.currentUnit && !s.stepMode) {
2229
2231
  const hookUnit = checkPostUnitHooks(s.currentUnit.type, s.currentUnit.id, s.basePath);
2232
+ persistHookState(s.basePath);
2230
2233
  if (hookUnit) {
2231
2234
  if (s.currentUnit) {
2232
2235
  await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
2233
2236
  }
2234
- persistHookState(s.basePath);
2235
2237
 
2236
2238
  return enqueueSidecar(
2237
2239
  s, ctx,
@@ -2240,12 +2242,23 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
2240
2242
  );
2241
2243
  }
2242
2244
 
2245
+ const hookFailure = consumeHookFailure();
2246
+ if (hookFailure) {
2247
+ ctx.ui.notify(
2248
+ `Post-unit hook ${hookFailure.hookName} failed for ${hookFailure.unitId}: ${hookFailure.reason}. Pausing auto-mode.`,
2249
+ "warning",
2250
+ );
2251
+ await pauseAuto(ctx, pi);
2252
+ return "stopped";
2253
+ }
2254
+
2243
2255
  // Check if a hook requested a retry of the trigger unit
2244
2256
  if (isRetryPending()) {
2245
2257
  const trigger = consumeRetryTrigger();
2246
2258
  if (trigger) {
2259
+ persistHookState(s.basePath);
2247
2260
  ctx.ui.notify(
2248
- `Hook requested retry of ${trigger.unitType} ${trigger.unitId} — resetting task state.`,
2261
+ `Hook requested retry of ${trigger.unitType} ${trigger.unitId} — resetting trigger unit state.`,
2249
2262
  "info",
2250
2263
  );
2251
2264
 
@@ -2299,6 +2312,19 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
2299
2312
  // Fall through to normal dispatch — deriveState will re-derive the unit
2300
2313
  }
2301
2314
  }
2315
+
2316
+ const gateBlock = consumeGateBlock();
2317
+ if (gateBlock) {
2318
+ persistHookState(s.basePath);
2319
+ const verdict = gateBlock.verdict ? ` verdict=${gateBlock.verdict};` : "";
2320
+ const artifact = gateBlock.artifact ? ` artifact=${gateBlock.artifact};` : "";
2321
+ const message =
2322
+ `Post-unit gate "${gateBlock.hookName}" blocked ${gateBlock.triggerUnitType} ${gateBlock.triggerUnitId}:` +
2323
+ `${verdict}${artifact} ${gateBlock.reason}. Run /gsd status to inspect, then /gsd auto after recovery.`;
2324
+ ctx.ui.notify(message, "warning");
2325
+ await pauseAuto(ctx, pi);
2326
+ return "stopped";
2327
+ }
2302
2328
  }
2303
2329
 
2304
2330
  // ── Fast-path stop detection (#3487) ──
@@ -1429,7 +1429,7 @@ export async function checkNeedsRunUat(
1429
1429
  // If the UAT file already contains a verdict, UAT has been run — skip
1430
1430
  if (hasVerdict(uatContent)) continue;
1431
1431
  // Also check the ASSESSMENT file — the run-uat prompt writes the verdict
1432
- // there (via gsd_summary_save artifact_type:"ASSESSMENT"), not into the
1432
+ // there (via gsd_uat_result_save), not into the
1433
1433
  // UAT spec file. Without this check the unit re-dispatches indefinitely.
1434
1434
  const assessmentFile = resolveSliceFile(base, mid, sid, "ASSESSMENT");
1435
1435
  if (assessmentFile) {
@@ -2986,13 +2986,23 @@ export async function buildValidateMilestonePrompt(
2986
2986
  if (isDbAvailable()) {
2987
2987
  const milestone = getMilestone(mid);
2988
2988
  if (milestone) {
2989
+ const escapeCell = (value: string) =>
2990
+ value.replace(/[\\|]/g, (char) => `\\${char}`).replace(/\r?\n/g, " ");
2989
2991
  const classes: string[] = [];
2990
- if (milestone.verification_contract) classes.push(`- **Contract:** ${milestone.verification_contract}`);
2991
- if (milestone.verification_integration) classes.push(`- **Integration:** ${milestone.verification_integration}`);
2992
- if (milestone.verification_operational) classes.push(`- **Operational:** ${milestone.verification_operational}`);
2993
- if (milestone.verification_uat) classes.push(`- **UAT:** ${milestone.verification_uat}`);
2992
+ if (milestone.verification_contract) classes.push(`| Contract | ${escapeCell(milestone.verification_contract)} |`);
2993
+ if (milestone.verification_integration) classes.push(`| Integration | ${escapeCell(milestone.verification_integration)} |`);
2994
+ if (milestone.verification_operational) classes.push(`| Operational | ${escapeCell(milestone.verification_operational)} |`);
2995
+ if (milestone.verification_uat) classes.push(`| UAT | ${escapeCell(milestone.verification_uat)} |`);
2994
2996
  if (classes.length > 0) {
2995
- const verificationClasses = `### Verification Classes (from planning)\n\nThese verification tiers were defined during milestone planning. Each non-empty class must be checked for evidence during validation.\n\n${classes.join("\n")}`;
2997
+ const verificationClasses = [
2998
+ "### Verification Classes (from planning)",
2999
+ "",
3000
+ "These verification tiers were defined during milestone planning. Every row in this table must appear in `verificationClasses` with the same canonical class name.",
3001
+ "",
3002
+ "| Class | Planned Check |",
3003
+ "| --- | --- |",
3004
+ ...classes,
3005
+ ].join("\n");
2996
3006
  inlined.push(verificationClasses);
2997
3007
  trackPromptContext(contextTelemetry, "verification-classes", "inline", verificationClasses);
2998
3008
  }
@@ -1095,7 +1095,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
1095
1095
  promptGuidelines: [
1096
1096
  "Use gsd_validate_milestone when all slices are done and the milestone needs validation before completion.",
1097
1097
  "Parameters: milestoneId, verdict, remediationRound, successCriteriaChecklist, sliceDeliveryAudit, crossSliceIntegration, requirementCoverage, verificationClasses (optional), verdictRationale, remediationPlan (optional).",
1098
- "If verification classes were planned, verificationClasses must include canonical class rows using the exact class names Contract, Integration, Operational, and UAT when present in planning.",
1098
+ "If verification classes were planned, verificationClasses must be a complete canonical table with one row for every applicable planned class using the exact class names Contract, Integration, Operational, and UAT. Do not submit a partial table.",
1099
1099
  "Planned verification text marked as none/not required/not applicable/N/A (including suffixed variants such as 'not required - backend-only') is treated as not applicable and does not require a class row.",
1100
1100
  "If verdict is 'needs-remediation', also provide remediationPlan and use gsd_reassess_roadmap to add remediation slices to the roadmap.",
1101
1101
  "On success, returns validationPath where VALIDATION.md was written.",
@@ -1108,7 +1108,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
1108
1108
  sliceDeliveryAudit: Type.String({ description: "Markdown table auditing each slice's claimed vs delivered output" }),
1109
1109
  crossSliceIntegration: Type.String({ description: "Markdown describing any cross-slice boundary mismatches" }),
1110
1110
  requirementCoverage: Type.String({ description: "Markdown describing any unaddressed requirements" }),
1111
- verificationClasses: Type.Optional(Type.String({ description: "Markdown describing verification class compliance and gaps using canonical class names (Contract, Integration, Operational, UAT) for each applicable planned class" })),
1111
+ verificationClasses: Type.Optional(Type.String({ description: "Complete markdown table describing verification class compliance and gaps; include one canonical row for every applicable planned class (Contract, Integration, Operational, UAT)" })),
1112
1112
  verdictRationale: Type.String({ description: "Why this verdict was chosen" }),
1113
1113
  remediationPlan: Type.Optional(Type.String({ description: "Remediation plan (required if verdict is needs-remediation)" })),
1114
1114
  }),
@@ -1,11 +1,13 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Shared browser-observable UAT requirement and evidence detection.
3
3
 
4
- export const BROWSER_REQUIREMENT_RE = /\b(?:browser|file:\/\/|localhost|dom|localstorage|click(?:ing|ed)?|button|screenshot|snapshot|reload(?:ed)?|page refresh|user-visible|strikethrough|search box)\b/i;
4
+ export const BROWSER_REQUIREMENT_RE = /\b(?:file:\/\/|localhost|playwright|chrome|screenshot|snapshot|browser_(?:assert|batch|find|verify|snapshot_refs))\b|\b(?:open|launch|navigate|load|visit|serve|start)\b.{0,80}\b(?:browser|page|localhost|file:\/\/)\b|\bbrowser\s+(?:check|session|test|uat|tool|automation|interaction|flow)\b/i;
5
5
  export const NO_BROWSER_EVIDENCE_RE = /\b(?:no|without|not|wasn'?t|isn'?t)\s+(?:automated\s+)?(?:live\s+)?browser(?:\s+(?:session|test|uat))?|\bno\s+automated\s+browser\b|\bnot\s+conducted\b/i;
6
6
  export const BROWSER_RUNTIME_RE = /\b(?:browser|playwright|chrome|camoufox|browser_(?:assert|batch|find|verify|snapshot_refs)|screenshot|snapshot|file:\/\/|localhost)\b/i;
7
7
  export const BROWSER_ACTION_RE = /\b(?:open(?:ed)?|navigate(?:d)?|click(?:ed)?|type(?:d)?|reload(?:ed)?|capture(?:d)?|screenshot|snapshot)\b/i;
8
8
  export const BROWSER_ASSERTION_RE = /\b(?:assert(?:ed|ion)?|observed|confirmed|verified|expected|visible|text|count|label|strikethrough|localstorage|screenshot|snapshot|passed)\b/i;
9
+ const NON_REQUIREMENT_BROWSER_HEADING_RE = /^(?:not\s+proven|not\s+covered|out\s+of\s+scope|deferred|follow-?ups?|known\s+limitations|notes\s+for\s+tester)\b/i;
10
+ const NON_REQUIREMENT_BROWSER_LINE_RE = /\b(?:deferred|not\s+proven|not\s+covered|out\s+of\s+scope|future\s+slice|follow-?up|no\s+(?:live\s+)?browser|without\s+(?:a\s+)?browser|not\s+(?:a\s+)?browser)\b/i;
9
11
 
10
12
  export function compactTextParts(parts: Array<string | string[] | null | undefined>): string {
11
13
  return parts.flatMap((part) => Array.isArray(part) ? part : [part])
@@ -14,7 +16,29 @@ export function compactTextParts(parts: Array<string | string[] | null | undefin
14
16
  }
15
17
 
16
18
  export function hasBrowserRequiredText(text: string): boolean {
17
- return BROWSER_REQUIREMENT_RE.test(text);
19
+ let inNonRequirementSection = false;
20
+ let nonRequirementDepth = 0;
21
+ for (const line of text.split(/\r?\n/)) {
22
+ const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*$/);
23
+ if (headingMatch) {
24
+ const depth = headingMatch[1]!.length;
25
+ const title = headingMatch[2] ?? "";
26
+ // Only update section context when at the same or higher level than the
27
+ // heading that opened the non-requirement zone. A sub-heading deeper than
28
+ // the opening heading must not escape or re-enter the zone on its own.
29
+ if (!inNonRequirementSection || depth <= nonRequirementDepth) {
30
+ inNonRequirementSection = NON_REQUIREMENT_BROWSER_HEADING_RE.test(title);
31
+ nonRequirementDepth = inNonRequirementSection ? depth : 0;
32
+ }
33
+ // Check the heading title itself — section state is already updated, so
34
+ // we correctly skip headings that opened a non-requirement zone.
35
+ if (!inNonRequirementSection && BROWSER_REQUIREMENT_RE.test(title)) return true;
36
+ continue;
37
+ }
38
+ if (inNonRequirementSection || NON_REQUIREMENT_BROWSER_LINE_RE.test(line)) continue;
39
+ if (BROWSER_REQUIREMENT_RE.test(line)) return true;
40
+ }
41
+ return false;
18
42
  }
19
43
 
20
44
  export function hasBrowserEvidenceText(text: string): boolean {
@@ -282,8 +282,8 @@ Examples:
282
282
  await handleInspect(ctx);
283
283
  return true;
284
284
  }
285
- if (trimmed === "update" || trimmed === "upgrade") {
286
- await handleUpdate(ctx);
285
+ if (trimmed === "update" || trimmed.startsWith("update ") || trimmed === "upgrade" || trimmed.startsWith("upgrade ")) {
286
+ await handleUpdate(ctx, trimmed.replace(/^(?:update|upgrade)\s*/, "").trim());
287
287
  return true;
288
288
  }
289
289
  if (trimmed === "fast" || trimmed.startsWith("fast ")) {
@@ -7,6 +7,8 @@
7
7
 
8
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
9
9
  import { existsSync, readFileSync, mkdirSync } from "node:fs";
10
+ import { execFileSync } from "node:child_process";
11
+ import { createRequire } from "node:module";
10
12
  import { join, resolve as resolvePath, sep } from "node:path";
11
13
  import { homedir } from "node:os";
12
14
  import { deriveState } from "./state.js";
@@ -37,7 +39,10 @@ import {
37
39
  scopeGsdWorkflowToolsForDispatch,
38
40
  } from "./bootstrap/register-hooks.js";
39
41
 
42
+ const GSD_PI_PACKAGE = "@opengsd/gsd-pi";
43
+ const GSD_BROWSER_PACKAGE = "@opengsd/gsd-browser";
40
44
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-pi/latest";
45
+ const BROWSER_UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-browser/latest";
41
46
  const UPDATE_FETCH_TIMEOUT_MS = 5000;
42
47
 
43
48
  // Detects a bun-installed gsd via `process.argv[1]`. Mirrors isBunInstall in
@@ -62,12 +67,12 @@ function resolveInstallCommand(pkg: string): string {
62
67
  return `npm install -g ${pkg}`;
63
68
  }
64
69
 
65
- async function fetchLatestVersionForCommand(): Promise<string | null> {
70
+ async function fetchLatestVersionForCommand(registryUrl: string = UPDATE_REGISTRY_URL): Promise<string | null> {
66
71
  const controller = new AbortController();
67
72
  const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
68
73
 
69
74
  try {
70
- const res = await fetch(UPDATE_REGISTRY_URL, { signal: controller.signal });
75
+ const res = await fetch(registryUrl, { signal: controller.signal });
71
76
  if (!res.ok) return null;
72
77
  const data = (await res.json()) as { version?: string };
73
78
  const latest = typeof data.version === "string" ? data.version.trim().replace(/^v/, "") : "";
@@ -79,6 +84,19 @@ async function fetchLatestVersionForCommand(): Promise<string | null> {
79
84
  }
80
85
  }
81
86
 
87
+ function resolveInstalledPackageVersionForCommand(packageName: string): string | null {
88
+ try {
89
+ const requireFromHere = createRequire(import.meta.url);
90
+ const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
91
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version?: unknown };
92
+ return typeof pkg.version === "string" && pkg.version.trim().length > 0
93
+ ? pkg.version.trim().replace(/^v/, "")
94
+ : null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
82
100
  export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
83
101
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(gsdHome(), "agent", "GSD-WORKFLOW.md");
84
102
  const workflow = readFileSync(workflowPath, "utf-8");
@@ -473,34 +491,81 @@ function compareSemverLocal(a: string, b: string): number {
473
491
  return 0
474
492
  }
475
493
 
476
- export async function handleUpdate(ctx: ExtensionCommandContext): Promise<void> {
494
+ function formatCommandVersion(version: string | null): string {
495
+ return version ? `v${version}` : "unknown";
496
+ }
497
+
498
+ function pickHigherVersionForCommand(a: string | null, b: string | null): string | null {
499
+ if (!a) return b;
500
+ if (!b) return a;
501
+ return compareSemverLocal(a, b) >= 0 ? a : b;
502
+ }
503
+
504
+ // Mirrors resolveGsdBrowserPathVersion in src/update-check.ts — duplicated because
505
+ // tsconfig.resources.json rootDir prevents importing from src/.
506
+ function resolveGsdBrowserPathVersionForCommand(env: NodeJS.ProcessEnv = process.env): string | null {
507
+ const explicit = env.GSD_BROWSER_PATH_VERSION?.trim();
508
+ if (explicit) return explicit.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
509
+ try {
510
+ const out = execFileSync("gsd-browser", ["--version"], {
511
+ encoding: "utf-8",
512
+ env,
513
+ stdio: ["ignore", "pipe", "ignore"],
514
+ timeout: 2000,
515
+ });
516
+ return out.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+
522
+ export async function handleUpdate(ctx: ExtensionCommandContext, args = ""): Promise<void> {
477
523
  const { execSync } = await import("node:child_process");
478
524
 
479
- const NPM_PACKAGE = "@opengsd/gsd-pi";
480
- const current = process.env.GSD_VERSION || "0.0.0";
525
+ const target = args.trim();
526
+ const browserUpdate = target === "browser" || target === "gsd-browser";
527
+ if (target && !browserUpdate) {
528
+ ctx.ui.notify("Usage: /gsd update [browser]", "warning");
529
+ return;
530
+ }
531
+
532
+ const NPM_PACKAGE = browserUpdate ? GSD_BROWSER_PACKAGE : GSD_PI_PACKAGE;
533
+ const registryUrl = browserUpdate ? BROWSER_UPDATE_REGISTRY_URL : UPDATE_REGISTRY_URL;
534
+ const bundledVersion = browserUpdate
535
+ ? resolveInstalledPackageVersionForCommand(GSD_BROWSER_PACKAGE)
536
+ : null;
537
+ const current = browserUpdate
538
+ ? pickHigherVersionForCommand(bundledVersion, resolveGsdBrowserPathVersionForCommand())
539
+ : process.env.GSD_VERSION || "0.0.0";
540
+ const label = browserUpdate ? "gsd-browser version" : "version";
481
541
 
482
- ctx.ui.notify(`Current version: v${current}\nChecking npm registry...`, "info");
542
+ ctx.ui.notify(`Current ${label}: ${formatCommandVersion(current)}\nChecking npm registry...`, "info");
483
543
 
484
- const latest = await fetchLatestVersionForCommand();
544
+ const latest = await fetchLatestVersionForCommand(registryUrl);
485
545
  if (!latest) {
486
546
  ctx.ui.notify("Failed to reach npm registry. Check your network connection.", "error");
487
547
  return;
488
548
  }
489
549
 
490
- if (compareSemverLocal(latest, current) <= 0) {
491
- ctx.ui.notify(`Already up to date (v${current}).`, "info");
550
+ if (current && compareSemverLocal(latest, current) <= 0) {
551
+ ctx.ui.notify(`Already up to date (${formatCommandVersion(current)}).`, "info");
492
552
  return;
493
553
  }
494
554
 
495
- ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
555
+ ctx.ui.notify(`Updating: ${formatCommandVersion(current)} → v${latest}...`, "info");
496
556
 
497
557
  const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
498
558
  try {
499
559
  execSync(installCmd, {
500
560
  stdio: ["ignore", "pipe", "ignore"],
501
561
  });
562
+ const newPathVersion = browserUpdate ? resolveGsdBrowserPathVersionForCommand() : null;
563
+ const pathReady = !browserUpdate || (!!newPathVersion && compareSemverLocal(newPathVersion, latest) >= 0);
502
564
  ctx.ui.notify(
503
- `Updated to v${latest}. Restart your GSD session to use the new version.`,
565
+ browserUpdate
566
+ ? `Updated gsd-browser to v${latest}. Restart your GSD session to use the new browser automation version.` +
567
+ (pathReady ? "" : "\nNote: Ensure the npm global bin directory is on your PATH so MCP automation uses the updated binary.")
568
+ : `Updated to v${latest}. Restart your GSD session to use the new version.`,
504
569
  "info",
505
570
  );
506
571
  } catch {
@@ -72,7 +72,8 @@ export function formatMcpInitResult(
72
72
  `Project: ${targetPath}`,
73
73
  `Config: ${configPath}`,
74
74
  "",
75
- "MCP-capable clients can now load the GSD workflow MCP server from this folder.",
75
+ "MCP-capable clients can now load the GSD workflow and gsd-browser MCP servers from this folder.",
76
+ "Pi Providers use the managed gsd-browser engine directly; this project config is for External MCP Clients.",
76
77
  "Restart or reconnect any client that already has this project open.",
77
78
  ].join("\n");
78
79
  }
@@ -305,10 +305,18 @@ This config sets a parent workspace with two child repositories. The implicit `p
305
305
  - `max_cycles`: number — max times this hook fires per trigger (default: 1, max: 10).
306
306
  - `model`: string — optional model override.
307
307
  - `artifact`: string — expected output file name (relative to task/slice dir). Hook is skipped if file already exists (idempotent).
308
+ - `criticality`: `"advisory"` or `"blocking"` — advisory preserves current best-effort behavior; blocking requires clean hook completion plus a valid outcome verdict before auto-mode advances. Default: `"advisory"`.
308
309
  - `retry_on`: string — if this file is produced instead of the artifact, re-run the trigger unit then re-run hooks.
310
+ - `on_block`: object — optional routing for blocking findings:
311
+ - `action`: `"retry-unit"`, `"retry-task"`, `"queue-task"`, `"queue-slice"`, or `"pause"`.
312
+ - `artifact`: string — optional compatibility artifact for retry routing.
309
313
  - `agent`: string — agent definition file to use for hook execution.
310
314
  - `enabled`: boolean — toggle without removing (default: `true`).
311
315
 
316
+ Blocking hook artifacts must begin with YAML frontmatter containing either `verdict` or `outcome.verdict`.
317
+ Supported verdicts are `pass`, `advisory`, `needs-rework`, `needs-remediation`, and `needs-attention`.
318
+ `pass` and `advisory` continue; `needs-rework` retries the trigger unit when routed with `retry-unit`/`retry-task`; `needs-remediation` and `needs-attention` pause with recovery guidance.
319
+
312
320
  - `pre_dispatch_hooks`: array — hooks that fire before a unit is dispatched. Each entry has:
313
321
  - `name`: string — unique hook identifier.
314
322
  - `before`: string[] — unit types to intercept.
@@ -269,14 +269,14 @@ export async function checkRuntimeHealth(
269
269
  } catch {
270
270
  count = MAX_UAT_ATTEMPTS + 1;
271
271
  }
272
- if (count <= MAX_UAT_ATTEMPTS) continue;
272
+ if (count < MAX_UAT_ATTEMPTS) continue;
273
273
 
274
274
  issues.push({
275
275
  severity: "warning",
276
276
  code: "uat_retry_exhausted",
277
277
  scope: "slice",
278
278
  unitId: `${mid}/${sid}`,
279
- message: `run-uat for ${mid}/${sid} exhausted ${count - 1} retry attempt(s) without an ASSESSMENT verdict. Reset the retry counter after fixing the underlying UAT/tool issue, then rerun /gsd auto.`,
279
+ message: `run-uat for ${mid}/${sid} exhausted ${count} attempt(s) without an ASSESSMENT verdict. Reset the retry counter after fixing the underlying UAT/tool issue, then rerun /gsd auto.`,
280
280
  file: `.gsd/runtime/${fileName}`,
281
281
  fixable: true,
282
282
  });