@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.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
- package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
- package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
- package/dist/resources/extensions/browser-tools/index.js +29 -2
- package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
- package/dist/resources/extensions/gsd/auto/phases.js +45 -3
- package/dist/resources/extensions/gsd/auto/session.js +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
- package/dist/resources/extensions/gsd/auto-timers.js +24 -10
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
- package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
- package/dist/resources/extensions/gsd/auto.js +26 -4
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
- package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
- package/dist/resources/extensions/gsd/config-overlay.js +1 -0
- package/dist/resources/extensions/gsd/context-masker.js +129 -5
- package/dist/resources/extensions/gsd/guided-flow.js +93 -108
- package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
- package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
- package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
- package/dist/resources/extensions/gsd/preferences-models.js +1 -0
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
- package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
- package/dist/resources/extensions/gsd/tool-contract.js +6 -1
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
- package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
- package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
- package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -3
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
- package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +3 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +157 -18
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +159 -36
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
- package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install/handoff.js +16 -3
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
- package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
- package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
- package/src/resources/extensions/browser-tools/index.ts +36 -5
- package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
- package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
- package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
- package/src/resources/extensions/gsd/auto/phases.ts +48 -6
- package/src/resources/extensions/gsd/auto/session.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-timers.ts +25 -9
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
- package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
- package/src/resources/extensions/gsd/auto.ts +28 -4
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
- package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
- package/src/resources/extensions/gsd/config-overlay.ts +1 -0
- package/src/resources/extensions/gsd/context-masker.ts +152 -5
- package/src/resources/extensions/gsd/guided-flow.ts +128 -135
- package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
- package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
- package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
- package/src/resources/extensions/gsd/preferences-models.ts +1 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
- package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
- package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
- package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
- package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
- package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
- package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
- package/src/resources/extensions/gsd/tool-contract.ts +7 -1
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
- package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
- package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
- package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
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 {
|
|
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
|
|
630
|
-
if (!
|
|
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
|
-
//
|
|
195
|
-
//
|
|
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 <
|
|
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
|
|
217
|
-
// Clear the stale entries so subsequent ticks don't re-detect
|
|
218
|
-
// and set the flag so the filesystem-activity check
|
|
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).
|