@opengsd/gsd-pi 1.1.1-dev.75048e7 → 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 +10 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
- package/dist/resources/extensions/gsd/auto-timers.js +24 -10
- package/dist/resources/extensions/gsd/auto.js +26 -4
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +29 -21
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
- 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 +4 -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 +2 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
- package/dist/resources/extensions/gsd/tool-contract.js +1 -1
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +19 -2
- package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +32 -4
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +38 -14
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -3
- 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 +8 -8
- 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 +8 -8
- 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/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +158 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +149 -9
- 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/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 +34 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
- package/src/resources/extensions/gsd/auto-timers.ts +25 -9
- package/src/resources/extensions/gsd/auto.ts +28 -4
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +40 -21
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
- 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 +4 -1
- 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 +2 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- 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-supervisor.test.mjs +4 -0
- package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -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-rule-coverage.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +1 -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/planner-handoff.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +113 -1
- package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +131 -2
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
- package/src/resources/extensions/gsd/tool-contract.ts +1 -1
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +21 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +46 -4
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +38 -14
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -3
- 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/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f692671bcb7f8bc4
|
|
@@ -435,11 +435,27 @@ function formatManagedBrowserError(toolName, error) {
|
|
|
435
435
|
return [
|
|
436
436
|
`gsd-browser engine or tool unavailable for ${toolName}: ${message}`,
|
|
437
437
|
"",
|
|
438
|
-
"
|
|
438
|
+
"The managed gsd-browser engine is enabled for this session but is unavailable.",
|
|
439
439
|
"Run /gsd doctor or reinstall dependencies so @opengsd/gsd-browser is available.",
|
|
440
|
-
"
|
|
440
|
+
"Unset GSD_BROWSER_ENGINE or set GSD_BROWSER_ENGINE=playwright to use the default Playwright engine.",
|
|
441
441
|
].join("\n");
|
|
442
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Eagerly establish the managed gsd-browser connection so browser tools are
|
|
445
|
+
* ready before first use. Best-effort: returns the error instead of throwing so
|
|
446
|
+
* callers (e.g. session-start warm-up) can surface a warning without failing the
|
|
447
|
+
* session. Connecting only spawns the gsd-browser MCP daemon; it does not launch
|
|
448
|
+
* Chrome (that happens lazily on the first navigation).
|
|
449
|
+
*/
|
|
450
|
+
export async function warmUpManagedGsdBrowser(ctx, signal) {
|
|
451
|
+
try {
|
|
452
|
+
await getOrConnectManagedGsdBrowser(ctx, signal);
|
|
453
|
+
return { ok: true };
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
443
459
|
export function registerManagedGsdBrowserTools(pi) {
|
|
444
460
|
for (const tool of MANAGED_BROWSER_TOOLS) {
|
|
445
461
|
pi.registerTool({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "browser-tools",
|
|
3
3
|
"name": "Browser Tools",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"description": "GSD browser automation contract adapter backed by
|
|
5
|
+
"description": "GSD browser automation contract adapter backed by Playwright with optional managed gsd-browser support",
|
|
6
6
|
"tier": "bundled",
|
|
7
7
|
"requires": { "platform": ">=2.29.0" },
|
|
8
8
|
"provides": {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** browser-tools — Pi Browser Automation Contract adapter. */
|
|
2
2
|
import { importExtensionModule } from "@gsd/pi-coding-agent";
|
|
3
|
-
import { closeManagedGsdBrowser, registerManagedGsdBrowserTools } from "./engine/managed-gsd-browser.js";
|
|
3
|
+
import { closeManagedGsdBrowser, registerManagedGsdBrowserTools, warmUpManagedGsdBrowser } from "./engine/managed-gsd-browser.js";
|
|
4
4
|
import { resolveBrowserEngineMode } from "./engine/selection.js";
|
|
5
|
+
import { detectWebApp } from "./web-app-detect.js";
|
|
5
6
|
let legacyRegistrationPromise = null;
|
|
6
7
|
let managedRegistrationPromise = null;
|
|
7
8
|
let registeredEngine = null;
|
|
@@ -147,6 +148,29 @@ async function registerBrowserTools(pi) {
|
|
|
147
148
|
throw error;
|
|
148
149
|
}
|
|
149
150
|
}
|
|
151
|
+
function isWarmUpDisabled() {
|
|
152
|
+
const value = process.env.GSD_BROWSER_WARMUP?.trim().toLowerCase();
|
|
153
|
+
return value === "0" || value === "false" || value === "off";
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Auto-initialize the managed gsd-browser engine only when explicitly selected
|
|
157
|
+
* for a web app. Best-effort and non-blocking: warm-up runs in the background
|
|
158
|
+
* and only surfaces a warning if it fails.
|
|
159
|
+
*/
|
|
160
|
+
function maybeWarmUpManagedEngine(pi, ctx) {
|
|
161
|
+
if (isWarmUpDisabled())
|
|
162
|
+
return;
|
|
163
|
+
if (resolveBrowserEngineMode() !== "gsd-browser")
|
|
164
|
+
return;
|
|
165
|
+
const projectRoot = ctx.cwd || process.cwd();
|
|
166
|
+
if (!detectWebApp(projectRoot))
|
|
167
|
+
return;
|
|
168
|
+
void warmUpManagedGsdBrowser(ctx).then((result) => {
|
|
169
|
+
if (!result.ok && ctx.hasUI) {
|
|
170
|
+
ctx.ui.notify(`gsd-browser auto-init failed: ${result.error}. Browser UAT tools will retry on first use; run /gsd doctor if this persists.`, "warning");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
150
174
|
async function closeActiveBrowserEngines() {
|
|
151
175
|
await closeManagedGsdBrowser();
|
|
152
176
|
if (legacyRegistrationPromise) {
|
|
@@ -157,12 +181,15 @@ async function closeActiveBrowserEngines() {
|
|
|
157
181
|
export default function (pi) {
|
|
158
182
|
pi.on("session_start", async (_event, ctx) => {
|
|
159
183
|
if (ctx.hasUI) {
|
|
160
|
-
void registerBrowserTools(pi)
|
|
184
|
+
void registerBrowserTools(pi)
|
|
185
|
+
.then(() => maybeWarmUpManagedEngine(pi, ctx))
|
|
186
|
+
.catch((error) => {
|
|
161
187
|
ctx.ui.notify(`browser-tools failed to load: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
162
188
|
});
|
|
163
189
|
return;
|
|
164
190
|
}
|
|
165
191
|
await registerBrowserTools(pi);
|
|
192
|
+
maybeWarmUpManagedEngine(pi, ctx);
|
|
166
193
|
});
|
|
167
194
|
pi.on("session_shutdown", async () => {
|
|
168
195
|
await closeActiveBrowserEngines();
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
// Frontend frameworks / bundlers whose presence in dependencies indicates a
|
|
9
|
+
// browser-facing web app worth warming the optional managed engine for.
|
|
10
|
+
const WEB_DEPENDENCY_RE = /^(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)/;
|
|
11
|
+
// package.json scripts that imply a dev server / browser-facing build.
|
|
12
|
+
const WEB_SCRIPT_RE = /\b(vite|next|nuxt|astro|remix|webpack(-dev-server)?|parcel|ng serve|serve\b|http-server|live-server|gatsby)\b/;
|
|
13
|
+
function readPackageJson(projectRoot) {
|
|
14
|
+
const packageJsonPath = resolve(projectRoot, "package.json");
|
|
15
|
+
if (!existsSync(packageJsonPath))
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
19
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function dependencyNames(pkg) {
|
|
26
|
+
return [
|
|
27
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
28
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
29
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns true when the project looks like a browser-facing web app. Conservative
|
|
34
|
+
* and dependency-free: a false negative just means lazy connection (the prior
|
|
35
|
+
* behavior); a false positive only warms an idle engine connection.
|
|
36
|
+
*/
|
|
37
|
+
export function detectWebApp(projectRoot) {
|
|
38
|
+
const pkg = readPackageJson(projectRoot);
|
|
39
|
+
if (pkg) {
|
|
40
|
+
if (dependencyNames(pkg).some((name) => WEB_DEPENDENCY_RE.test(name)))
|
|
41
|
+
return true;
|
|
42
|
+
const scriptValues = Object.values(pkg.scripts ?? {}).filter((value) => typeof value === "string");
|
|
43
|
+
if (scriptValues.some((script) => WEB_SCRIPT_RE.test(script)))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// No package.json signal — fall back to a top-level index.html (static sites).
|
|
47
|
+
if (existsSync(resolve(projectRoot, "index.html")))
|
|
48
|
+
return true;
|
|
49
|
+
if (existsSync(resolve(projectRoot, "public", "index.html")))
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
@@ -16,6 +16,8 @@ import { detectStuck } from "./detect-stuck.js";
|
|
|
16
16
|
import { runUnit } from "./run-unit.js";
|
|
17
17
|
import { debugLog } from "../debug-logger.js";
|
|
18
18
|
import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "../worktree-root.js";
|
|
19
|
+
import { buildManualValidationGuidance } from "../worktree-manager.js";
|
|
20
|
+
import { relSliceFile } from "../paths.js";
|
|
19
21
|
import { classifyProject } from "../detection.js";
|
|
20
22
|
import { MergeConflictError } from "../git-service.js";
|
|
21
23
|
import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
|
|
@@ -47,6 +49,7 @@ import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
|
|
|
47
49
|
import { getContextPauseAction } from "../auto-budget.js";
|
|
48
50
|
import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, supportsStructuredQuestions, } from "../workflow-mcp.js";
|
|
49
51
|
import { prepareWorkflowMcpForProject } from "../workflow-mcp-auto-prep.js";
|
|
52
|
+
import { getToolBaselineSnapshot } from "../auto-model-selection.js";
|
|
50
53
|
import { resolveManifest } from "../unit-context-manifest.js";
|
|
51
54
|
import { createWorktreeSafetyModule } from "../worktree-safety.js";
|
|
52
55
|
import { isSuspiciousGhostCompletion } from "../auto-unit-closeout.js";
|
|
@@ -302,6 +305,8 @@ async function validateSourceWriteWorktreeSafety(ic, unitType, unitId, milestone
|
|
|
302
305
|
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
303
306
|
let consecutiveSessionTimeouts = 0;
|
|
304
307
|
const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
|
|
308
|
+
/** Maximum zero-tool-call retries before pausing — context exhaustion is deterministic. */
|
|
309
|
+
const MAX_ZERO_TOOL_RETRIES = 1;
|
|
305
310
|
export function resetSessionTimeoutState() {
|
|
306
311
|
consecutiveSessionTimeouts = 0;
|
|
307
312
|
}
|
|
@@ -1070,7 +1075,13 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
1070
1075
|
const authMode = provider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1071
1076
|
? ctx.modelRegistry.getProviderAuthMode(provider)
|
|
1072
1077
|
: undefined;
|
|
1073
|
-
|
|
1078
|
+
// Use the baseline snapshot rather than the live active-tool set: a prior
|
|
1079
|
+
// unit's per-provider narrowing (hook overrides, Groq 128-tool cap, etc.)
|
|
1080
|
+
// can strip required MCP tools from the live set even though
|
|
1081
|
+
// selectAndApplyModel will restore them before the unit is dispatched.
|
|
1082
|
+
// Checking a stale-narrowed set causes false transport-preflight warnings
|
|
1083
|
+
// that repeat on every /gsd auto resume (#477 follow-up).
|
|
1084
|
+
const activeTools = getToolBaselineSnapshot(pi);
|
|
1074
1085
|
// Deep planning intentionally keeps human checkpoints in plain chat. In
|
|
1075
1086
|
// Claude Code/local MCP transports, structured question requests can be
|
|
1076
1087
|
// cancelled outside the normal chat flow, which made approval gates easy to
|
|
@@ -1093,6 +1104,9 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
1093
1104
|
sessionContextWindow: ctx.model?.contextWindow,
|
|
1094
1105
|
sessionProvider: ctx.model?.provider,
|
|
1095
1106
|
modelRegistry: ctx.modelRegistry,
|
|
1107
|
+
activeTools,
|
|
1108
|
+
sessionBaseUrl: ctx.model?.baseUrl,
|
|
1109
|
+
sessionAuthMode: authMode,
|
|
1096
1110
|
});
|
|
1097
1111
|
if (isUnhandledPhaseWarning(dispatchResult)) {
|
|
1098
1112
|
deps.invalidateAllCaches();
|
|
@@ -1116,6 +1130,9 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
1116
1130
|
sessionContextWindow: ctx.model?.contextWindow,
|
|
1117
1131
|
sessionProvider: ctx.model?.provider,
|
|
1118
1132
|
modelRegistry: ctx.modelRegistry,
|
|
1133
|
+
activeTools,
|
|
1134
|
+
sessionBaseUrl: ctx.model?.baseUrl,
|
|
1135
|
+
sessionAuthMode: authMode,
|
|
1119
1136
|
});
|
|
1120
1137
|
}
|
|
1121
1138
|
if (dispatchResult.action === "stop") {
|
|
@@ -2059,13 +2076,23 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
2059
2076
|
});
|
|
2060
2077
|
}
|
|
2061
2078
|
else {
|
|
2079
|
+
const zeroToolKey = `${unitType}/${unitId}`;
|
|
2080
|
+
const attempt = (s.zeroToolRetryCount.get(zeroToolKey) ?? 0) + 1;
|
|
2062
2081
|
debugLog("runUnitPhase", {
|
|
2063
2082
|
phase: "zero-tool-calls",
|
|
2064
2083
|
unitType,
|
|
2065
2084
|
unitId,
|
|
2085
|
+
attempt,
|
|
2066
2086
|
warning: "Unit completed with 0 tool calls — likely context exhaustion, marking as failed",
|
|
2067
2087
|
});
|
|
2068
|
-
|
|
2088
|
+
if (attempt > MAX_ZERO_TOOL_RETRIES) {
|
|
2089
|
+
s.zeroToolRetryCount.delete(zeroToolKey);
|
|
2090
|
+
ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, pausing auto-mode after ${MAX_ZERO_TOOL_RETRIES} retry.`, "error");
|
|
2091
|
+
await deps.pauseAuto(ctx, pi);
|
|
2092
|
+
return { action: "break", reason: "zero-tool-calls-exhausted" };
|
|
2093
|
+
}
|
|
2094
|
+
s.zeroToolRetryCount.set(zeroToolKey, attempt);
|
|
2095
|
+
ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry (attempt ${attempt}/${MAX_ZERO_TOOL_RETRIES})`, "warning");
|
|
2069
2096
|
return {
|
|
2070
2097
|
action: "retry",
|
|
2071
2098
|
reason: "zero-tool-calls",
|
|
@@ -2087,6 +2114,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
2087
2114
|
if (artifactVerified) {
|
|
2088
2115
|
s.unitDispatchCount.delete(dispatchKey);
|
|
2089
2116
|
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
2117
|
+
s.zeroToolRetryCount.delete(dispatchKey);
|
|
2090
2118
|
}
|
|
2091
2119
|
// Write phase handoff anchor after successful research/planning completion
|
|
2092
2120
|
const anchorPhases = new Set(["research-milestone", "research-slice", "plan-milestone", "plan-slice"]);
|
|
@@ -2232,7 +2260,21 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|
|
2232
2260
|
}
|
|
2233
2261
|
}
|
|
2234
2262
|
if (pauseAfterUatDispatch) {
|
|
2235
|
-
|
|
2263
|
+
const pauseMid = iterData.mid;
|
|
2264
|
+
const pauseSliceId = pauseMid && iterData.unitId.startsWith(`${pauseMid}/`)
|
|
2265
|
+
? iterData.unitId.slice(pauseMid.length + 1)
|
|
2266
|
+
: undefined;
|
|
2267
|
+
const guidance = pauseMid
|
|
2268
|
+
? buildManualValidationGuidance(s.basePath, pauseMid, {
|
|
2269
|
+
uatPath: pauseSliceId
|
|
2270
|
+
? relSliceFile(s.basePath, pauseMid, pauseSliceId, "UAT")
|
|
2271
|
+
: undefined,
|
|
2272
|
+
})
|
|
2273
|
+
: null;
|
|
2274
|
+
const pauseMessage = guidance
|
|
2275
|
+
? `UAT requires human execution. Auto-mode will pause after this unit writes the result file.\n\n${guidance}`
|
|
2276
|
+
: "UAT requires human execution. Auto-mode will pause after this unit writes the result file.";
|
|
2277
|
+
ctx.ui.notify(pauseMessage, "info");
|
|
2236
2278
|
await deps.pauseAuto(ctx, pi);
|
|
2237
2279
|
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
2238
2280
|
clearFinalizingUnit();
|
|
@@ -94,6 +94,7 @@ export class AutoSession {
|
|
|
94
94
|
verificationRetryCount = new Map();
|
|
95
95
|
verificationRetryFailureHashes = new Map();
|
|
96
96
|
exhaustedVerificationUnits = new Set();
|
|
97
|
+
zeroToolRetryCount = new Map();
|
|
97
98
|
pausedSessionFile = null;
|
|
98
99
|
pausedUnitType = null;
|
|
99
100
|
pausedUnitId = null;
|
|
@@ -266,6 +267,7 @@ export class AutoSession {
|
|
|
266
267
|
this.verificationRetryCount.clear();
|
|
267
268
|
this.verificationRetryFailureHashes.clear();
|
|
268
269
|
this.exhaustedVerificationUnits.clear();
|
|
270
|
+
this.zeroToolRetryCount.clear();
|
|
269
271
|
this.pausedSessionFile = null;
|
|
270
272
|
this.pausedUnitType = null;
|
|
271
273
|
this.pausedUnitId = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Declarative auto-mode dispatch rules and dispatch resolver.
|
|
3
3
|
import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
|
|
4
|
-
import { isDbAvailable, getMilestoneSlices, getPendingGates, markAllGatesOmitted, getMilestone, insertAssessment, setSliceSketchFlag, transaction, getAssessment } from "./gsd-db.js";
|
|
4
|
+
import { isDbAvailable, getMilestoneSlices, getPendingGates, markAllGatesOmitted, getMilestone, insertAssessment, setSliceSketchFlag, transaction, getAssessment, } from "./gsd-db.js";
|
|
5
5
|
import { isClosedStatus } from "./status-guards.js";
|
|
6
6
|
import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
|
|
7
7
|
import { gsdRoot, resolveGsdPathContract, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, relTaskFile, relSliceFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, gsdProjectionRoot, } from "./paths.js";
|
|
@@ -21,6 +21,7 @@ import { isAutoActive } from "./auto.js";
|
|
|
21
21
|
import { markDepthVerified } from "./bootstrap/write-gate.js";
|
|
22
22
|
import { ensureWorkflowPreferencesCaptured } from "./planning-depth.js";
|
|
23
23
|
import { MILESTONE_ID_RE } from "./milestone-ids.js";
|
|
24
|
+
import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "./workflow-mcp.js";
|
|
24
25
|
import { PROJECT_RESEARCH_INFLIGHT_MARKER, } from "./project-research-policy.js";
|
|
25
26
|
import { isWorkflowPrefsCaptured, resolveDeepProjectSetupState, } from "./deep-project-setup-policy.js";
|
|
26
27
|
import { annotateBackgroundable } from "./delegation-policy.js";
|
|
@@ -467,11 +468,18 @@ export const DISPATCH_RULES = [
|
|
|
467
468
|
},
|
|
468
469
|
{
|
|
469
470
|
name: "run-uat (post-completion)",
|
|
470
|
-
match: async ({ state, mid, basePath, prefs }) => {
|
|
471
|
+
match: async ({ state, mid, basePath, prefs, sessionProvider, sessionAuthMode, activeTools, sessionBaseUrl }) => {
|
|
471
472
|
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
472
473
|
if (!needsRunUat)
|
|
473
474
|
return null;
|
|
474
475
|
const { sliceId, uatType } = needsRunUat;
|
|
476
|
+
// Transport preflight: verify required MCP tools are actually connected
|
|
477
|
+
// before consuming a retry attempt. Fixes tool-starved sessions burning
|
|
478
|
+
// all MAX_UAT_ATTEMPTS before stopping (#477).
|
|
479
|
+
const transportError = getWorkflowTransportSupportError(sessionProvider, getRequiredWorkflowToolsForAutoUnit("run-uat"), { projectRoot: basePath, surface: "auto-mode", unitType: "run-uat", authMode: sessionAuthMode, baseUrl: sessionBaseUrl, activeTools });
|
|
480
|
+
if (transportError) {
|
|
481
|
+
return { action: "stop", reason: transportError, level: "warning" };
|
|
482
|
+
}
|
|
475
483
|
// Cap run-uat dispatch attempts to prevent infinite replay (#3624).
|
|
476
484
|
// Check before incrementing so an exhausted counter cannot create a
|
|
477
485
|
// no-progress skip loop that starves later dispatch rules.
|
|
@@ -63,6 +63,32 @@ const TOOL_BASELINE = new WeakMap();
|
|
|
63
63
|
export function clearToolBaseline(pi) {
|
|
64
64
|
TOOL_BASELINE.delete(pi);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Return the union of the pre-dispatch baseline tool set and the current live
|
|
68
|
+
* active tools, or just the live tools when no baseline has been recorded yet.
|
|
69
|
+
*
|
|
70
|
+
* Use this instead of `pi.getActiveTools()` anywhere you need the full tool
|
|
71
|
+
* surface for a preflight/routing check that runs BEFORE `selectAndApplyModel`
|
|
72
|
+
* restores the baseline — e.g. in `runDispatch` and `decideNextUnit`.
|
|
73
|
+
*
|
|
74
|
+
* The union is intentional:
|
|
75
|
+
* - Baseline covers tools that a prior unit's per-provider narrowing (hook
|
|
76
|
+
* overrides, Groq 128-tool cap, etc.) has removed from the live set.
|
|
77
|
+
* Those tools will be restored by `selectAndApplyModel` before dispatch, so
|
|
78
|
+
* dropping them from the preflight check would be a false negative.
|
|
79
|
+
* - Live set covers tools connected after the baseline was first captured
|
|
80
|
+
* (e.g. MCP servers attached mid-session or after a paused resume).
|
|
81
|
+
* Without the live merge, a stale baseline permanently hides newly
|
|
82
|
+
* connected MCP tools and prevents transport-preflight from clearing on
|
|
83
|
+
* resume (#477 follow-up).
|
|
84
|
+
*/
|
|
85
|
+
export function getToolBaselineSnapshot(pi) {
|
|
86
|
+
const live = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
87
|
+
const baseline = TOOL_BASELINE.get(pi);
|
|
88
|
+
if (baseline === undefined)
|
|
89
|
+
return live;
|
|
90
|
+
return [...new Set([...baseline, ...live])];
|
|
91
|
+
}
|
|
66
92
|
/**
|
|
67
93
|
* Models eligible for the pre-dispatch policy gate. Prefer registry-available
|
|
68
94
|
* models; when that list is empty (common after worktree resume before registry
|
|
@@ -104,6 +104,14 @@ export function startUnitSupervision(sctx) {
|
|
|
104
104
|
const softTimeoutMs = supervisionTimeouts.softTimeoutMs;
|
|
105
105
|
const idleTimeoutMs = supervisionTimeouts.idleTimeoutMs;
|
|
106
106
|
const hardTimeoutMs = supervisionTimeouts.hardTimeoutMs;
|
|
107
|
+
// A single hung tool gets its own short budget, NOT the general idle window:
|
|
108
|
+
// a long-but-progressing session is not idle, but a tool stuck for minutes
|
|
109
|
+
// is. Falls back to the idle window only if misconfigured to zero. The
|
|
110
|
+
// hung-tool budget is intentionally not scaled by task estimate — a stuck
|
|
111
|
+
// tool call is stuck regardless of how long the overall task should take.
|
|
112
|
+
const stalledToolTimeoutMs = (supervisor.stalled_tool_timeout_minutes ?? 0) > 0
|
|
113
|
+
? supervisor.stalled_tool_timeout_minutes * 60 * 1000
|
|
114
|
+
: idleTimeoutMs;
|
|
107
115
|
// ── 1. Soft timeout warning ──
|
|
108
116
|
s.wrapupWarningHandle = setTimeout(() => {
|
|
109
117
|
s.wrapupWarningHandle = null;
|
|
@@ -144,10 +152,12 @@ export function startUnitSupervision(sctx) {
|
|
|
144
152
|
const runtime = readUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
145
153
|
if (!runtime)
|
|
146
154
|
return;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
//
|
|
155
|
+
// In-flight tool handling runs on its own dedicated hung-tool budget,
|
|
156
|
+
// independent of the general idle gate below, so a genuinely stuck tool
|
|
157
|
+
// is caught in minutes instead of waiting out the (typically much longer)
|
|
158
|
+
// idle window (#2527, follow-up). A tool actively executing within budget
|
|
159
|
+
// is real progress, so refreshing lastProgressAt here also keeps the idle
|
|
160
|
+
// gate from firing during legitimate long-running tool calls.
|
|
151
161
|
let stalledToolDetected = false;
|
|
152
162
|
if (getInFlightToolCount() > 0) {
|
|
153
163
|
// User-interactive tools (ask_user_questions, secure_env_collect) block
|
|
@@ -161,21 +171,25 @@ export function startUnitSupervision(sctx) {
|
|
|
161
171
|
}
|
|
162
172
|
const oldestStart = getOldestInFlightToolStart();
|
|
163
173
|
const toolAgeMs = Date.now() - oldestStart;
|
|
164
|
-
if (toolAgeMs <
|
|
174
|
+
if (toolAgeMs < stalledToolTimeoutMs) {
|
|
165
175
|
writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
166
176
|
lastProgressAt: Date.now(),
|
|
167
177
|
lastProgressKind: "tool-in-flight",
|
|
168
178
|
});
|
|
169
179
|
return;
|
|
170
180
|
}
|
|
171
|
-
// Tool has been in-flight longer than
|
|
172
|
-
// Clear the stale entries so subsequent ticks don't re-detect
|
|
173
|
-
// and set the flag so the filesystem-activity check
|
|
174
|
-
// override the stall verdict (#2527).
|
|
181
|
+
// Tool has been in-flight longer than the hung-tool budget — treat as
|
|
182
|
+
// hung. Clear the stale entries so subsequent ticks don't re-detect
|
|
183
|
+
// them, and set the flag so the idle gate and filesystem-activity check
|
|
184
|
+
// below do not override the stall verdict (#2527).
|
|
175
185
|
stalledToolDetected = true;
|
|
176
186
|
clearInFlightTools();
|
|
177
|
-
ctx.ui.notify(`Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min. Treating as hung — attempting idle recovery.`, "warning");
|
|
187
|
+
ctx.ui.notify(`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.`, "warning");
|
|
178
188
|
}
|
|
189
|
+
// No hung tool — apply the general idle gate. A unit that has made
|
|
190
|
+
// meaningful progress within the idle window is not idle yet.
|
|
191
|
+
if (!stalledToolDetected && Date.now() - runtime.lastProgressAt < idleTimeoutMs)
|
|
192
|
+
return;
|
|
179
193
|
// Check if the agent is producing work on disk.
|
|
180
194
|
// Skip this when a stalled tool was just detected — filesystem changes
|
|
181
195
|
// from earlier in the task should not override the stall verdict (#2527).
|
|
@@ -30,7 +30,7 @@ import { playNotificationBell, sendDesktopNotification } from "./notifications.j
|
|
|
30
30
|
import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, } from "./auto-budget.js";
|
|
31
31
|
import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, isToolInvocationError, isQueuedUserMessageSkip, isDeterministicPolicyError, } from "./auto-tool-tracking.js";
|
|
32
32
|
import { closeoutUnit } from "./auto-unit-closeout.js";
|
|
33
|
-
import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-model-selection.js";
|
|
33
|
+
import { selectAndApplyModel, resolveModelId, clearToolBaseline, getToolBaselineSnapshot } from "./auto-model-selection.js";
|
|
34
34
|
import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
|
|
35
35
|
import { resetHookState, runPreDispatchHooks, restoreHookState, clearPersistedHookState, } from "./post-unit-hooks.js";
|
|
36
36
|
import { runGSDDoctor, rebuildState } from "./doctor.js";
|
|
@@ -287,8 +287,24 @@ export function _synthesizePausedSessionRecoveryForTest(basePath, unitType, unit
|
|
|
287
287
|
function handlePausedSessionResumeRecovery(basePath, state, notify) {
|
|
288
288
|
if (!state.pausedSessionFile)
|
|
289
289
|
return { skippedReplay: false };
|
|
290
|
-
const pausedRecoveryUnitType = state.currentUnit?.type ?? state.pausedUnitType ??
|
|
291
|
-
const pausedRecoveryUnitId = state.currentUnit?.id ?? state.pausedUnitId ??
|
|
290
|
+
const pausedRecoveryUnitType = state.currentUnit?.type ?? state.pausedUnitType ?? null;
|
|
291
|
+
const pausedRecoveryUnitId = state.currentUnit?.id ?? state.pausedUnitId ?? null;
|
|
292
|
+
// When the paused-session metadata never captured the unit identity (the
|
|
293
|
+
// pause happened between units, or the worker died before currentUnit was
|
|
294
|
+
// set), we have nothing to verify against and nothing correct to target. A
|
|
295
|
+
// replay synthesized with an "unknown" unit re-injects an unbounded,
|
|
296
|
+
// mis-identified tool-call blob into the fresh resume context — exactly the
|
|
297
|
+
// thrash that turns one stuck unit into several. Disk state has already been
|
|
298
|
+
// rebuilt (rebuildState + doctor) before this runs, so skip the replay and
|
|
299
|
+
// let the normal dispatcher recompute the next unit from disk.
|
|
300
|
+
if (!pausedRecoveryUnitType || !pausedRecoveryUnitId) {
|
|
301
|
+
state.pausedSessionFile = null;
|
|
302
|
+
state.pausedUnitType = null;
|
|
303
|
+
state.pausedUnitId = null;
|
|
304
|
+
state.pendingCrashRecovery = null;
|
|
305
|
+
notify("Paused session had no recorded unit identity. Skipping tool-call replay and resuming from disk state.");
|
|
306
|
+
return { skippedReplay: true };
|
|
307
|
+
}
|
|
292
308
|
const completedPausedUnit = verifyExpectedArtifact(pausedRecoveryUnitType, pausedRecoveryUnitId, basePath);
|
|
293
309
|
if (completedPausedUnit) {
|
|
294
310
|
state.pausedSessionFile = null;
|
|
@@ -1637,7 +1653,10 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath, session) {
|
|
|
1637
1653
|
const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1638
1654
|
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
1639
1655
|
: undefined;
|
|
1640
|
-
|
|
1656
|
+
// Use baseline snapshot — same reason as phases.ts:runDispatch: the live
|
|
1657
|
+
// active set may be narrowed by the prior unit before selectAndApplyModel
|
|
1658
|
+
// restores it, causing false transport-preflight failures (#477 follow-up).
|
|
1659
|
+
const activeTools = getToolBaselineSnapshot(pi);
|
|
1641
1660
|
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
1642
1661
|
// because structured questions can be cancelled outside the chat turn on
|
|
1643
1662
|
// some transports.
|
|
@@ -1678,6 +1697,9 @@ export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath, session) {
|
|
|
1678
1697
|
sessionContextWindow,
|
|
1679
1698
|
sessionProvider,
|
|
1680
1699
|
modelRegistry,
|
|
1700
|
+
activeTools,
|
|
1701
|
+
sessionAuthMode: authMode,
|
|
1702
|
+
sessionBaseUrl: ctx.model?.baseUrl,
|
|
1681
1703
|
});
|
|
1682
1704
|
if (action.action === "stop") {
|
|
1683
1705
|
if (session)
|
|
@@ -210,7 +210,12 @@ export function buildRunUatGsdToolSet(activeToolNames, registeredToolNames = act
|
|
|
210
210
|
"subagent",
|
|
211
211
|
...RUN_UAT_BROWSER_TOOL_NAMES,
|
|
212
212
|
]);
|
|
213
|
-
|
|
213
|
+
const resolved = [...new Set(scoped)];
|
|
214
|
+
const unresolved = RUN_UAT_WORKFLOW_TOOL_NAMES.filter((tool) => !resolved.some((name) => name === tool || (name.startsWith("mcp__") && name.endsWith(`__${tool}`))));
|
|
215
|
+
if (unresolved.length > 0) {
|
|
216
|
+
safetyLogWarning("bootstrap", `buildRunUatGsdToolSet: required run-uat workflow tool(s) not found in active/registered surface: ${unresolved.join(", ")}. Session may lack gsd-workflow MCP connection.`);
|
|
217
|
+
}
|
|
218
|
+
return resolved;
|
|
214
219
|
}
|
|
215
220
|
export function buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames = activeToolNames) {
|
|
216
221
|
const autoBaseTools = new Set(MINIMAL_AUTO_BASE_TOOL_NAMES);
|
|
@@ -463,6 +468,18 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
463
468
|
if (isAutoActive() || preserveCloseoutSurface) {
|
|
464
469
|
ctx.ui.setWidget("gsd-health", undefined);
|
|
465
470
|
}
|
|
471
|
+
// Cold start after /quit relaunches with cwd at the project root. When
|
|
472
|
+
// auto-mode is neither active nor paused (its own resume path re-enters the
|
|
473
|
+
// worktree with a lease check — auto.ts:3032), proactively chdir back into
|
|
474
|
+
// the active milestone's worktree so subsequent work isn't stranded at the
|
|
475
|
+
// root. Best-effort and a no-op when already inside a worktree.
|
|
476
|
+
if (!isAutoActive() && !isAutoPaused() && !preserveCloseoutSurface) {
|
|
477
|
+
try {
|
|
478
|
+
const { reenterActiveWorktreeIfNeeded } = await import("../worktree-reentry.js");
|
|
479
|
+
await reenterActiveWorktreeIfNeeded(basePath);
|
|
480
|
+
}
|
|
481
|
+
catch { /* non-fatal */ }
|
|
482
|
+
}
|
|
466
483
|
});
|
|
467
484
|
pi.on("session_switch", async (_event, ctx) => {
|
|
468
485
|
const basePath = contextBasePath(ctx);
|
|
@@ -1084,16 +1101,19 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1084
1101
|
if (isAutoActive()) {
|
|
1085
1102
|
try {
|
|
1086
1103
|
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
1104
|
+
const { createObservationMask, createResponsesInputObservationMask, truncateContextResultMessages, truncateResponsesInputResultItems, } = await import("../context-masker.js");
|
|
1087
1105
|
const prefs = loadEffectiveGSDPreferences();
|
|
1088
1106
|
const cmConfig = prefs?.preferences.context_management;
|
|
1089
1107
|
// Observation masking: replace old tool results with placeholders
|
|
1090
1108
|
if (cmConfig?.observation_masking !== false) {
|
|
1091
1109
|
const keepTurns = cmConfig?.observation_mask_turns ?? 8;
|
|
1092
|
-
const { createObservationMask } = await import("../context-masker.js");
|
|
1093
|
-
const mask = createObservationMask(keepTurns);
|
|
1094
1110
|
const messages = payload.messages;
|
|
1095
1111
|
if (Array.isArray(messages)) {
|
|
1096
|
-
payload.messages =
|
|
1112
|
+
payload.messages = createObservationMask(keepTurns)(messages);
|
|
1113
|
+
}
|
|
1114
|
+
const input = payload.input;
|
|
1115
|
+
if (Array.isArray(input)) {
|
|
1116
|
+
payload.input = createResponsesInputObservationMask(keepTurns)(input);
|
|
1097
1117
|
}
|
|
1098
1118
|
}
|
|
1099
1119
|
// Tool result truncation: cap individual tool result content length.
|
|
@@ -1102,23 +1122,11 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1102
1122
|
const maxChars = cmConfig?.tool_result_max_chars ?? 800;
|
|
1103
1123
|
const msgs = payload.messages;
|
|
1104
1124
|
if (Array.isArray(msgs)) {
|
|
1105
|
-
payload.messages = msgs
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (totalLen > maxChars) {
|
|
1111
|
-
const truncated = blocks.map(b => {
|
|
1112
|
-
if (typeof b.text === "string" && b.text.length > maxChars) {
|
|
1113
|
-
return { ...b, text: b.text.slice(0, maxChars) + "\n…[truncated]" };
|
|
1114
|
-
}
|
|
1115
|
-
return b;
|
|
1116
|
-
});
|
|
1117
|
-
return { ...msg, content: truncated };
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
return msg;
|
|
1121
|
-
});
|
|
1125
|
+
payload.messages = truncateContextResultMessages(msgs, maxChars);
|
|
1126
|
+
}
|
|
1127
|
+
const input = payload.input;
|
|
1128
|
+
if (Array.isArray(input)) {
|
|
1129
|
+
payload.input = truncateResponsesInputResultItems(input, maxChars);
|
|
1122
1130
|
}
|
|
1123
1131
|
}
|
|
1124
1132
|
catch { /* non-fatal */ }
|
|
@@ -56,7 +56,7 @@ export const BUNDLED_SKILL_TRIGGERS = [
|
|
|
56
56
|
{ trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
|
|
57
57
|
{ trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
|
|
58
58
|
{ trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
|
|
59
|
-
{ trigger: "gsd-browser
|
|
59
|
+
{ trigger: "gsd-browser opt-in and External MCP UAT — screenshots, assertions, console/network diagnostics", skill: "gsd-browser" },
|
|
60
60
|
{ trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
|
|
61
61
|
{ trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
|
|
62
62
|
{ trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
|
|
@@ -198,6 +198,16 @@ export async function handleAutoCommand(trimmed, ctx, pi) {
|
|
|
198
198
|
if (!(await guardRemoteSession(ctx, pi)))
|
|
199
199
|
return true;
|
|
200
200
|
const basePath = projectRoot();
|
|
201
|
+
// Cold start after /quit lands at the project root, not the worktree. If the
|
|
202
|
+
// active milestone has a live worktree, chdir back into it now so the agent
|
|
203
|
+
// doesn't have to search for it. Best-effort; resolves to a no-op otherwise.
|
|
204
|
+
try {
|
|
205
|
+
const { reenterActiveWorktreeIfNeeded } = await import("../../worktree-reentry.js");
|
|
206
|
+
await reenterActiveWorktreeIfNeeded(basePath, {
|
|
207
|
+
notify: (message) => ctx.ui.notify(message, "info"),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch { /* non-fatal */ }
|
|
201
211
|
const { hasGsdBootstrapArtifacts } = await import("../../detection.js");
|
|
202
212
|
const { gsdRoot } = await import("../../paths.js");
|
|
203
213
|
if (!hasGsdBootstrapArtifacts(gsdRoot(basePath))) {
|
|
@@ -34,7 +34,7 @@ export function formatMcpInitResult(status, configPath, targetPath) {
|
|
|
34
34
|
`Config: ${configPath}`,
|
|
35
35
|
"",
|
|
36
36
|
"MCP-capable clients can now load the GSD workflow and gsd-browser MCP servers from this folder.",
|
|
37
|
-
"Pi Providers use
|
|
37
|
+
"Pi Providers use built-in browser tools directly; this project config is for External MCP Clients.",
|
|
38
38
|
"Restart or reconnect any client that already has this project open.",
|
|
39
39
|
].join("\n");
|
|
40
40
|
}
|
|
@@ -122,6 +122,7 @@ function collectConfigSections() {
|
|
|
122
122
|
supRows.push({ label: "Model", value: sup.model });
|
|
123
123
|
supRows.push({ label: "Soft timeout", value: `${sup.soft_timeout_minutes}m` });
|
|
124
124
|
supRows.push({ label: "Idle timeout", value: `${sup.idle_timeout_minutes}m` });
|
|
125
|
+
supRows.push({ label: "Stalled tool timeout", value: `${sup.stalled_tool_timeout_minutes}m` });
|
|
125
126
|
supRows.push({ label: "Hard timeout", value: `${sup.hard_timeout_minutes}m` });
|
|
126
127
|
sections.push({ title: "Auto Supervisor", rows: supRows });
|
|
127
128
|
}
|