@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.9f86580

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 (219) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  4. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  5. package/dist/resources/extensions/browser-tools/index.js +29 -2
  6. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +45 -3
  8. package/dist/resources/extensions/gsd/auto/session.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  13. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  14. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  15. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  16. package/dist/resources/extensions/gsd/auto.js +26 -4
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  21. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  23. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  24. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  25. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  26. package/dist/resources/extensions/gsd/guided-flow.js +93 -108
  27. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  28. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  29. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  30. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
  33. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  34. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  35. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  36. package/dist/resources/extensions/gsd/tool-contract.js +6 -1
  37. package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
  38. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  39. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
  40. package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
  42. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  43. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  73. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/package.json +1 -1
  81. package/packages/daemon/package.json +4 -4
  82. package/packages/gsd-agent-core/package.json +5 -5
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  86. package/packages/gsd-agent-modes/package.json +7 -7
  87. package/packages/mcp-server/package.json +3 -3
  88. package/packages/native/package.json +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -3
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  92. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  93. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  94. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  95. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  97. package/packages/pi-agent-core/dist/types.d.ts +3 -1
  98. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  99. package/packages/pi-agent-core/dist/types.js.map +1 -1
  100. package/packages/pi-agent-core/package.json +1 -1
  101. package/packages/pi-ai/dist/models.generated.d.ts +157 -18
  102. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  103. package/packages/pi-ai/dist/models.generated.js +159 -36
  104. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  105. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  107. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  108. package/packages/pi-ai/package.json +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  110. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  113. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  116. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  118. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  120. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  123. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  124. package/packages/pi-coding-agent/package.json +7 -7
  125. package/packages/pi-tui/package.json +1 -1
  126. package/packages/rpc-client/package.json +2 -2
  127. package/pkg/package.json +1 -1
  128. package/scripts/install/handoff.js +16 -3
  129. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  130. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  131. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  132. package/src/resources/extensions/browser-tools/index.ts +36 -5
  133. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  134. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  135. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  136. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  137. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  138. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  139. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
  140. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  141. package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  143. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  144. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  145. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  146. package/src/resources/extensions/gsd/auto.ts +28 -4
  147. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
  148. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
  149. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  150. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  151. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  152. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  153. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  154. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  155. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  156. package/src/resources/extensions/gsd/guided-flow.ts +128 -135
  157. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  158. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  159. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  160. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  161. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  162. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
  164. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  165. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  166. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  167. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  168. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  169. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  170. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  171. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  172. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  173. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  174. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  175. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  176. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  177. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  178. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  179. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  181. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  182. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  183. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  184. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  185. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  186. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  187. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +7 -1
  188. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  189. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  190. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  191. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  193. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  194. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  195. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  196. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
  197. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  198. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  199. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  200. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +56 -0
  201. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  202. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
  205. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  206. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tool-contract.ts +7 -1
  208. package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
  209. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  210. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
  211. package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
  212. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
  213. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  214. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  215. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  217. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  218. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  219. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
@@ -0,0 +1,68 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ const { detectWebApp } = await import("../web-app-detect.ts");
8
+
9
+ function makeProject(files) {
10
+ const root = mkdtempSync(join(tmpdir(), "gsd-webapp-detect-"));
11
+ for (const [relPath, contents] of Object.entries(files)) {
12
+ const full = join(root, relPath);
13
+ mkdirSync(join(full, ".."), { recursive: true });
14
+ writeFileSync(full, contents);
15
+ }
16
+ return root;
17
+ }
18
+
19
+ describe("detectWebApp", () => {
20
+ const roots = [];
21
+ after(() => roots.forEach((root) => rmSync(root, { recursive: true, force: true })));
22
+
23
+ const project = (files) => {
24
+ const root = makeProject(files);
25
+ roots.push(root);
26
+ return root;
27
+ };
28
+
29
+ it("detects a React dependency", () => {
30
+ const root = project({ "package.json": JSON.stringify({ dependencies: { react: "^18.0.0" } }) });
31
+ assert.equal(detectWebApp(root), true);
32
+ });
33
+
34
+ it("detects a Vite/Next dev dependency", () => {
35
+ const root = project({ "package.json": JSON.stringify({ devDependencies: { vite: "^5.0.0" } }) });
36
+ assert.equal(detectWebApp(root), true);
37
+ });
38
+
39
+ it("detects a dev-server script", () => {
40
+ const root = project({ "package.json": JSON.stringify({ scripts: { dev: "next dev" } }) });
41
+ assert.equal(detectWebApp(root), true);
42
+ });
43
+
44
+ it("detects a static index.html site", () => {
45
+ const root = project({ "index.html": "<!doctype html>" });
46
+ assert.equal(detectWebApp(root), true);
47
+ });
48
+
49
+ it("returns false for a CLI/library package", () => {
50
+ const root = project({
51
+ "package.json": JSON.stringify({
52
+ dependencies: { commander: "^12.0.0" },
53
+ scripts: { build: "tsc", test: "node --test" },
54
+ }),
55
+ });
56
+ assert.equal(detectWebApp(root), false);
57
+ });
58
+
59
+ it("returns false when there is no package.json or index.html", () => {
60
+ const root = project({ "README.md": "# nothing" });
61
+ assert.equal(detectWebApp(root), false);
62
+ });
63
+
64
+ it("does not throw on malformed package.json", () => {
65
+ const root = project({ "package.json": "{ not valid json" });
66
+ assert.equal(detectWebApp(root), false);
67
+ });
68
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * web-app-detect — lightweight, synchronous heuristic for deciding whether the
3
+ * project under development is a web app. Used only when the optional managed
4
+ * gsd-browser engine is selected and can be warmed before first use.
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+
9
+ // Frontend frameworks / bundlers whose presence in dependencies indicates a
10
+ // browser-facing web app worth warming the optional managed engine for.
11
+ const WEB_DEPENDENCY_RE =
12
+ /^(react|react-dom|next|nuxt|vue|@vue\/|svelte|@sveltejs\/|solid-js|astro|@remix-run\/|gatsby|preact|@angular\/core|vite|@vitejs\/|@builder\.io\/qwik|@web\/dev-server|@11ty\/eleventy)/;
13
+
14
+ // package.json scripts that imply a dev server / browser-facing build.
15
+ const WEB_SCRIPT_RE = /\b(vite|next|nuxt|astro|remix|webpack(-dev-server)?|parcel|ng serve|serve\b|http-server|live-server|gatsby)\b/;
16
+
17
+ interface MinimalPackageJson {
18
+ dependencies?: Record<string, unknown>;
19
+ devDependencies?: Record<string, unknown>;
20
+ peerDependencies?: Record<string, unknown>;
21
+ scripts?: Record<string, unknown>;
22
+ }
23
+
24
+ function readPackageJson(projectRoot: string): MinimalPackageJson | null {
25
+ const packageJsonPath = resolve(projectRoot, "package.json");
26
+ if (!existsSync(packageJsonPath)) return null;
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as unknown;
29
+ return parsed && typeof parsed === "object" ? (parsed as MinimalPackageJson) : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function dependencyNames(pkg: MinimalPackageJson): string[] {
36
+ return [
37
+ ...Object.keys(pkg.dependencies ?? {}),
38
+ ...Object.keys(pkg.devDependencies ?? {}),
39
+ ...Object.keys(pkg.peerDependencies ?? {}),
40
+ ];
41
+ }
42
+
43
+ /**
44
+ * Returns true when the project looks like a browser-facing web app. Conservative
45
+ * and dependency-free: a false negative just means lazy connection (the prior
46
+ * behavior); a false positive only warms an idle engine connection.
47
+ */
48
+ export function detectWebApp(projectRoot: string): boolean {
49
+ const pkg = readPackageJson(projectRoot);
50
+ if (pkg) {
51
+ if (dependencyNames(pkg).some((name) => WEB_DEPENDENCY_RE.test(name))) return true;
52
+ const scriptValues = Object.values(pkg.scripts ?? {}).filter(
53
+ (value): value is string => typeof value === "string",
54
+ );
55
+ if (scriptValues.some((script) => WEB_SCRIPT_RE.test(script))) return true;
56
+ }
57
+
58
+ // No package.json signal — fall back to a top-level index.html (static sites).
59
+ if (existsSync(resolve(projectRoot, "index.html"))) return true;
60
+ if (existsSync(resolve(projectRoot, "public", "index.html"))) return true;
61
+
62
+ return false;
63
+ }
@@ -35,6 +35,8 @@ import { detectStuck } from "./detect-stuck.js";
35
35
  import { runUnit } from "./run-unit.js";
36
36
  import { debugLog } from "../debug-logger.js";
37
37
  import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "../worktree-root.js";
38
+ import { buildManualValidationGuidance } from "../worktree-manager.js";
39
+ import { relSliceFile } from "../paths.js";
38
40
  import { classifyProject } from "../detection.js";
39
41
  import { MergeConflictError } from "../git-service.js";
40
42
  import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
@@ -83,6 +85,7 @@ import {
83
85
  supportsStructuredQuestions,
84
86
  } from "../workflow-mcp.js";
85
87
  import { prepareWorkflowMcpForProject } from "../workflow-mcp-auto-prep.js";
88
+ import { getToolBaselineSnapshot } from "../auto-model-selection.js";
86
89
  import type { DispatchAction } from "../auto-dispatch.js";
87
90
  import { resolveManifest } from "../unit-context-manifest.js";
88
91
  import { createWorktreeSafetyModule, type WorktreeSafetyResult } from "../worktree-safety.js";
@@ -397,6 +400,8 @@ async function validateSourceWriteWorktreeSafety(
397
400
 
398
401
  let consecutiveSessionTimeouts = 0;
399
402
  const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
403
+ /** Maximum zero-tool-call retries before pausing — context exhaustion is deterministic. */
404
+ const MAX_ZERO_TOOL_RETRIES = 1;
400
405
 
401
406
  export function resetSessionTimeoutState(): void {
402
407
  consecutiveSessionTimeouts = 0;
@@ -1446,7 +1451,13 @@ export async function runDispatch(
1446
1451
  const authMode = provider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
1447
1452
  ? ctx.modelRegistry.getProviderAuthMode(provider)
1448
1453
  : undefined;
1449
- const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1454
+ // Use the baseline snapshot rather than the live active-tool set: a prior
1455
+ // unit's per-provider narrowing (hook overrides, Groq 128-tool cap, etc.)
1456
+ // can strip required MCP tools from the live set even though
1457
+ // selectAndApplyModel will restore them before the unit is dispatched.
1458
+ // Checking a stale-narrowed set causes false transport-preflight warnings
1459
+ // that repeat on every /gsd auto resume (#477 follow-up).
1460
+ const activeTools = getToolBaselineSnapshot(pi);
1450
1461
  // Deep planning intentionally keeps human checkpoints in plain chat. In
1451
1462
  // Claude Code/local MCP transports, structured question requests can be
1452
1463
  // cancelled outside the normal chat flow, which made approval gates easy to
@@ -1470,6 +1481,9 @@ export async function runDispatch(
1470
1481
  sessionContextWindow: ctx.model?.contextWindow,
1471
1482
  sessionProvider: ctx.model?.provider,
1472
1483
  modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
1484
+ activeTools,
1485
+ sessionBaseUrl: ctx.model?.baseUrl,
1486
+ sessionAuthMode: authMode,
1473
1487
  });
1474
1488
  if (isUnhandledPhaseWarning(dispatchResult)) {
1475
1489
  deps.invalidateAllCaches();
@@ -1493,6 +1507,9 @@ export async function runDispatch(
1493
1507
  sessionContextWindow: ctx.model?.contextWindow,
1494
1508
  sessionProvider: ctx.model?.provider,
1495
1509
  modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
1510
+ activeTools,
1511
+ sessionBaseUrl: ctx.model?.baseUrl,
1512
+ sessionAuthMode: authMode,
1496
1513
  });
1497
1514
  }
1498
1515
 
@@ -2711,14 +2728,27 @@ export async function runUnitPhase(
2711
2728
  unitId,
2712
2729
  });
2713
2730
  } else {
2731
+ const zeroToolKey = `${unitType}/${unitId}`;
2732
+ const attempt = (s.zeroToolRetryCount.get(zeroToolKey) ?? 0) + 1;
2714
2733
  debugLog("runUnitPhase", {
2715
2734
  phase: "zero-tool-calls",
2716
2735
  unitType,
2717
2736
  unitId,
2737
+ attempt,
2718
2738
  warning: "Unit completed with 0 tool calls — likely context exhaustion, marking as failed",
2719
2739
  });
2740
+ if (attempt > MAX_ZERO_TOOL_RETRIES) {
2741
+ s.zeroToolRetryCount.delete(zeroToolKey);
2742
+ ctx.ui.notify(
2743
+ `${unitType} ${unitId} completed with 0 tool calls — context exhaustion, pausing auto-mode after ${MAX_ZERO_TOOL_RETRIES} retry.`,
2744
+ "error",
2745
+ );
2746
+ await deps.pauseAuto(ctx, pi);
2747
+ return { action: "break", reason: "zero-tool-calls-exhausted" };
2748
+ }
2749
+ s.zeroToolRetryCount.set(zeroToolKey, attempt);
2720
2750
  ctx.ui.notify(
2721
- `${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry`,
2751
+ `${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry (attempt ${attempt}/${MAX_ZERO_TOOL_RETRIES})`,
2722
2752
  "warning",
2723
2753
  );
2724
2754
  return {
@@ -2748,6 +2778,7 @@ export async function runUnitPhase(
2748
2778
  if (artifactVerified) {
2749
2779
  s.unitDispatchCount.delete(dispatchKey);
2750
2780
  s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
2781
+ s.zeroToolRetryCount.delete(dispatchKey);
2751
2782
  }
2752
2783
 
2753
2784
  // Write phase handoff anchor after successful research/planning completion
@@ -2927,10 +2958,21 @@ export async function runFinalize(
2927
2958
  }
2928
2959
 
2929
2960
  if (pauseAfterUatDispatch) {
2930
- ctx.ui.notify(
2931
- "UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
2932
- "info",
2933
- );
2961
+ const pauseMid = iterData.mid;
2962
+ const pauseSliceId = pauseMid && iterData.unitId.startsWith(`${pauseMid}/`)
2963
+ ? iterData.unitId.slice(pauseMid.length + 1)
2964
+ : undefined;
2965
+ const guidance = pauseMid
2966
+ ? buildManualValidationGuidance(s.basePath, pauseMid, {
2967
+ uatPath: pauseSliceId
2968
+ ? relSliceFile(s.basePath, pauseMid, pauseSliceId, "UAT")
2969
+ : undefined,
2970
+ })
2971
+ : null;
2972
+ const pauseMessage = guidance
2973
+ ? `UAT requires human execution. Auto-mode will pause after this unit writes the result file.\n\n${guidance}`
2974
+ : "UAT requires human execution. Auto-mode will pause after this unit writes the result file.";
2975
+ ctx.ui.notify(pauseMessage, "info");
2934
2976
  await deps.pauseAuto(ctx, pi);
2935
2977
  debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
2936
2978
  clearFinalizingUnit();
@@ -176,6 +176,7 @@ export class AutoSession {
176
176
  readonly verificationRetryCount = new Map<string, number>();
177
177
  readonly verificationRetryFailureHashes = new Map<string, string>();
178
178
  readonly exhaustedVerificationUnits = new Set<string>();
179
+ readonly zeroToolRetryCount = new Map<string, number>();
179
180
  pausedSessionFile: string | null = null;
180
181
  pausedUnitType: string | null = null;
181
182
  pausedUnitId: string | null = null;
@@ -362,6 +363,7 @@ export class AutoSession {
362
363
  this.verificationRetryCount.clear();
363
364
  this.verificationRetryFailureHashes.clear();
364
365
  this.exhaustedVerificationUnits.clear();
366
+ this.zeroToolRetryCount.clear();
365
367
  this.pausedSessionFile = null;
366
368
  this.pausedUnitType = null;
367
369
  this.pausedUnitId = null;
@@ -17,7 +17,17 @@ import type { GSDPreferences } from "./preferences.js";
17
17
  import type { UatType } from "./files.js";
18
18
  import type { MinimalModelRegistry } from "./context-budget.js";
19
19
  import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
20
- import { isDbAvailable, getMilestoneSlices, getPendingGates, markAllGatesOmitted, getMilestone, insertAssessment, setSliceSketchFlag, transaction, getAssessment } from "./gsd-db.js";
20
+ import {
21
+ isDbAvailable,
22
+ getMilestoneSlices,
23
+ getPendingGates,
24
+ markAllGatesOmitted,
25
+ getMilestone,
26
+ insertAssessment,
27
+ setSliceSketchFlag,
28
+ transaction,
29
+ getAssessment,
30
+ } from "./gsd-db.js";
21
31
  import { isClosedStatus } from "./status-guards.js";
22
32
  import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
23
33
 
@@ -76,6 +86,10 @@ import { isAutoActive } from "./auto.js";
76
86
  import { markDepthVerified } from "./bootstrap/write-gate.js";
77
87
  import { ensureWorkflowPreferencesCaptured } from "./planning-depth.js";
78
88
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
89
+ import {
90
+ getWorkflowTransportSupportError,
91
+ getRequiredWorkflowToolsForAutoUnit,
92
+ } from "./workflow-mcp.js";
79
93
  import {
80
94
  PROJECT_RESEARCH_INFLIGHT_MARKER,
81
95
  } from "./project-research-policy.js";
@@ -96,6 +110,10 @@ import { probeGitConflictState } from "./git-conflict-state.js";
96
110
  import { runTurnGitAction } from "./git-service.js";
97
111
  import { parseUnitId } from "./unit-id.js";
98
112
  import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
113
+ import {
114
+ checkCloseoutConsistencyGate,
115
+ formatCloseoutConsistencyBlock,
116
+ } from "./closeout-consistency-gate.js";
99
117
 
100
118
  // ─── Types ────────────────────────────────────────────────────────────────
101
119
 
@@ -132,6 +150,12 @@ export interface DispatchContext {
132
150
  modelRegistry?: MinimalModelRegistry;
133
151
  /** Session model provider, used for provider-specific effective context windows. */
134
152
  sessionProvider?: string;
153
+ /** Active tools in the current session, used for transport preflight checks. */
154
+ activeTools?: string[];
155
+ /** Session model base URL, used for transport preflight checks. */
156
+ sessionBaseUrl?: string;
157
+ /** Session model auth mode, used for transport preflight checks. */
158
+ sessionAuthMode?: "apiKey" | "oauth" | "externalCli" | "none";
135
159
  }
136
160
 
137
161
  function resolveExistingExpectedArtifact(
@@ -649,11 +673,23 @@ export const DISPATCH_RULES: DispatchRule[] = [
649
673
  },
650
674
  {
651
675
  name: "run-uat (post-completion)",
652
- match: async ({ state, mid, basePath, prefs }) => {
676
+ match: async ({ state, mid, basePath, prefs, sessionProvider, sessionAuthMode, activeTools, sessionBaseUrl }) => {
653
677
  const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
654
678
  if (!needsRunUat) return null;
655
679
  const { sliceId, uatType } = needsRunUat;
656
680
 
681
+ // Transport preflight: verify required MCP tools are actually connected
682
+ // before consuming a retry attempt. Fixes tool-starved sessions burning
683
+ // all MAX_UAT_ATTEMPTS before stopping (#477).
684
+ const transportError = getWorkflowTransportSupportError(
685
+ sessionProvider,
686
+ getRequiredWorkflowToolsForAutoUnit("run-uat"),
687
+ { projectRoot: basePath, surface: "auto-mode", unitType: "run-uat", authMode: sessionAuthMode, baseUrl: sessionBaseUrl, activeTools },
688
+ );
689
+ if (transportError) {
690
+ return { action: "stop" as const, reason: transportError, level: "warning" as const };
691
+ }
692
+
657
693
  // Cap run-uat dispatch attempts to prevent infinite replay (#3624).
658
694
  // Check before incrementing so an exhausted counter cannot create a
659
695
  // no-progress skip loop that starves later dispatch rules.
@@ -1635,6 +1671,16 @@ export const DISPATCH_RULES: DispatchRule[] = [
1635
1671
  prompt: await buildCompleteMilestonePrompt(mid, midTitle, basePath),
1636
1672
  };
1637
1673
  }
1674
+ if (milestone) {
1675
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
1676
+ if (!closeoutGate.ok) {
1677
+ return {
1678
+ action: "stop",
1679
+ reason: formatCloseoutConsistencyBlock(closeoutGate),
1680
+ level: "warning",
1681
+ };
1682
+ }
1683
+ }
1638
1684
  }
1639
1685
  return {
1640
1686
  action: "stop",
@@ -90,6 +90,32 @@ export function clearToolBaseline(pi: ExtensionAPI | object): void {
90
90
  TOOL_BASELINE.delete(pi as unknown as object);
91
91
  }
92
92
 
93
+ /**
94
+ * Return the union of the pre-dispatch baseline tool set and the current live
95
+ * active tools, or just the live tools when no baseline has been recorded yet.
96
+ *
97
+ * Use this instead of `pi.getActiveTools()` anywhere you need the full tool
98
+ * surface for a preflight/routing check that runs BEFORE `selectAndApplyModel`
99
+ * restores the baseline — e.g. in `runDispatch` and `decideNextUnit`.
100
+ *
101
+ * The union is intentional:
102
+ * - Baseline covers tools that a prior unit's per-provider narrowing (hook
103
+ * overrides, Groq 128-tool cap, etc.) has removed from the live set.
104
+ * Those tools will be restored by `selectAndApplyModel` before dispatch, so
105
+ * dropping them from the preflight check would be a false negative.
106
+ * - Live set covers tools connected after the baseline was first captured
107
+ * (e.g. MCP servers attached mid-session or after a paused resume).
108
+ * Without the live merge, a stale baseline permanently hides newly
109
+ * connected MCP tools and prevents transport-preflight from clearing on
110
+ * resume (#477 follow-up).
111
+ */
112
+ export function getToolBaselineSnapshot(pi: ExtensionAPI): string[] {
113
+ const live = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
114
+ const baseline = TOOL_BASELINE.get(pi as unknown as object);
115
+ if (baseline === undefined) return live;
116
+ return [...new Set([...baseline, ...live])];
117
+ }
118
+
93
119
  /**
94
120
  * Models eligible for the pre-dispatch policy gate. Prefer registry-available
95
121
  * models; when that list is empty (common after worktree resume before registry
@@ -46,6 +46,7 @@ import { hasBrowserRequiredText } from "./browser-evidence.js";
46
46
  import { debugLog } from "./debug-logger.js";
47
47
  import { buildSkillActivationBlock, buildSkillDiscoveryVars } from "./skill-activation.js";
48
48
  import { findMilestoneIds } from "./milestone-ids.js";
49
+ import { buildRunUatPresentationForType, RUN_UAT_TOOL_PRESENTATION_PLAN_ID } from "./tool-presentation-plan.js";
49
50
 
50
51
  export { buildSkillActivationBlock, buildSkillDiscoveryVars };
51
52
 
@@ -3384,6 +3385,7 @@ export async function buildRunUatPrompt(
3384
3385
 
3385
3386
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "ASSESSMENT"));
3386
3387
  const uatType = resolveEffectiveUatType(uatContent);
3388
+ const canonicalPresentation = JSON.stringify(buildRunUatPresentationForType(uatType), null, 2);
3387
3389
 
3388
3390
  return loadPrompt("run-uat", {
3389
3391
  workingDirectory: base,
@@ -3392,6 +3394,8 @@ export async function buildRunUatPrompt(
3392
3394
  uatPath,
3393
3395
  uatResultPath,
3394
3396
  uatType,
3397
+ toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
3398
+ canonicalPresentation,
3395
3399
  inlinedContext,
3396
3400
  skillActivation: buildSkillActivationBlock({
3397
3401
  base,
@@ -54,6 +54,7 @@ import { isGsdWorktreePath } from "./worktree-root.js";
54
54
  import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
55
55
  import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
56
56
  import { loadAllCaptures, loadPendingCaptures } from "./captures.js";
57
+ import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
57
58
 
58
59
  // Re-export so existing consumers of auto-recovery.ts keep working.
59
60
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
@@ -626,9 +627,8 @@ export function verifyExpectedArtifact(
626
627
  if (summaryOutcome === "failure") return false;
627
628
  const { milestone: mid } = parseUnitId(unitId);
628
629
  if (mid && isDbAvailable()) {
629
- const dbMilestone = getMilestone(mid);
630
- if (!dbMilestone) return false;
631
- if (!isClosedStatus(dbMilestone.status) && summaryOutcome !== "success") return false;
630
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
631
+ if (!closeoutGate.ok) return false;
632
632
  }
633
633
  if (hasImplementationArtifacts(base, mid) === "absent") return false;
634
634
  }
@@ -147,6 +147,15 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
147
147
  const softTimeoutMs = supervisionTimeouts.softTimeoutMs;
148
148
  const idleTimeoutMs = supervisionTimeouts.idleTimeoutMs;
149
149
  const hardTimeoutMs = supervisionTimeouts.hardTimeoutMs;
150
+ // A single hung tool gets its own short budget, NOT the general idle window:
151
+ // a long-but-progressing session is not idle, but a tool stuck for minutes
152
+ // is. Falls back to the idle window only if misconfigured to zero. The
153
+ // hung-tool budget is intentionally not scaled by task estimate — a stuck
154
+ // tool call is stuck regardless of how long the overall task should take.
155
+ const stalledToolTimeoutMs =
156
+ (supervisor.stalled_tool_timeout_minutes ?? 0) > 0
157
+ ? supervisor.stalled_tool_timeout_minutes! * 60 * 1000
158
+ : idleTimeoutMs;
150
159
 
151
160
  // ── 1. Soft timeout warning ──
152
161
  s.wrapupWarningHandle = setTimeout(() => {
@@ -189,10 +198,13 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
189
198
  };
190
199
  const runtime = readUnitRuntimeRecord(s.basePath, unitType, unitId);
191
200
  if (!runtime) return;
192
- if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
193
201
 
194
- // Agent has tool calls currently executing not idle, just waiting.
195
- // But only suppress recovery if the tool started recently.
202
+ // In-flight tool handling runs on its own dedicated hung-tool budget,
203
+ // independent of the general idle gate below, so a genuinely stuck tool
204
+ // is caught in minutes instead of waiting out the (typically much longer)
205
+ // idle window (#2527, follow-up). A tool actively executing within budget
206
+ // is real progress, so refreshing lastProgressAt here also keeps the idle
207
+ // gate from firing during legitimate long-running tool calls.
196
208
  let stalledToolDetected = false;
197
209
  if (getInFlightToolCount() > 0) {
198
210
  // User-interactive tools (ask_user_questions, secure_env_collect) block
@@ -206,25 +218,29 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
206
218
  }
207
219
  const oldestStart = getOldestInFlightToolStart()!;
208
220
  const toolAgeMs = Date.now() - oldestStart;
209
- if (toolAgeMs < idleTimeoutMs) {
221
+ if (toolAgeMs < stalledToolTimeoutMs) {
210
222
  writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
211
223
  lastProgressAt: Date.now(),
212
224
  lastProgressKind: "tool-in-flight",
213
225
  });
214
226
  return;
215
227
  }
216
- // Tool has been in-flight longer than idle timeout — treat as hung.
217
- // Clear the stale entries so subsequent ticks don't re-detect them,
218
- // and set the flag so the filesystem-activity check below does not
219
- // override the stall verdict (#2527).
228
+ // Tool has been in-flight longer than the hung-tool budget — treat as
229
+ // hung. Clear the stale entries so subsequent ticks don't re-detect
230
+ // them, and set the flag so the idle gate and filesystem-activity check
231
+ // below do not override the stall verdict (#2527).
220
232
  stalledToolDetected = true;
221
233
  clearInFlightTools();
222
234
  ctx.ui.notify(
223
- `Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min. Treating as hung — attempting idle recovery.`,
235
+ `Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min (budget ${Math.round(stalledToolTimeoutMs / 60000)}min). Treating as hung — attempting idle recovery.`,
224
236
  "warning",
225
237
  );
226
238
  }
227
239
 
240
+ // No hung tool — apply the general idle gate. A unit that has made
241
+ // meaningful progress within the idle window is not idle yet.
242
+ if (!stalledToolDetected && Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
243
+
228
244
  // Check if the agent is producing work on disk.
229
245
  // Skip this when a stalled tool was just detected — filesystem changes
230
246
  // from earlier in the task should not override the stall verdict (#2527).