FN-7130: require agent-browser for browser verification

Browser Verification steps now explicitly request and log agent-browser usage.

- Mark the built-in browser verification workflow step as browser-required and document that it runs through agent-browser.
- Add executor helpers to inject the agent-browser skill, probe CLI availability, and log start/finish outcomes.
- Cover workflow metadata, browser verification execution logging, availability probing, and skill augmentation with tests.
- Add a patch changeset for the published Fusion package.

Files changed:
 .../fn-7130-browser-verification-agent-browser.md  |   7 +
 docs/workflow-steps.md                             |   4 +-
 .../__tests__/builtin-coding-workflow-ir.test.ts   |  23 ++
 .../core/src/__tests__/builtin-workflows.test.ts   |  12 +
 .../core/src/builtin-browser-verification-group.ts |   4 +
 packages/core/src/types.ts                         |   7 +
 ...iltin-coding-browser-verification-group.test.ts |   8 +-
 .../executor-browser-verification.test.ts          | 267 +++++++++++++++++++++
 packages/engine/src/executor.ts                    | 118 +++++++++
 9 files changed, 447 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-7130

Fusion-Task-Lineage: 3a76522e-70e1-4445-ad18-b9e1b9b355fc

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-27 14:27:06 -07:00
parent c1613ad7f6
commit a593cc7428
9 changed files with 447 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: The Browser Verification workflow step now uses the agent-browser tool, checks availability, and logs its actions.
category: feature
dev: Adds a `requiresBrowser` flag to `WorkflowStep`, set on the built-in browser-verification inner node and threaded through `runGraphCustomNode` into `executeWorkflowStep`, which merges the `agent-browser-navigation` skill, runs a bounded non-fatal `agent-browser --version` availability preflight (async exec), and emits start/availability agent-log entries. Absent the flag, prompt-step execution is unchanged.

View File

@@ -357,9 +357,11 @@ FN-7039 (U6) DELETED the `WORKFLOW_STEP_TEMPLATES` built-in catalog array (the f
The built-in quality gates ship as inlined `optional-group` node builders in `@fusion/core`, not as a template catalog (the former `WORKFLOW_STEP_TEMPLATES` array was removed):
- **Browser Verification** (`browser-verification`, `builtin-browser-verification-group.ts`) — browser-automation-style checks for UI validation flows; an optional-group node on `builtin:coding`.
- **Browser Verification** (`browser-verification`, `builtin-browser-verification-group.ts`) — browser-automation-style checks for UI validation flows; an optional-group node on `builtin:coding` and `builtin:stepwise-coding`.
- **Code Review** (`code-review`, `builtin-code-review-group.ts`) — the inlined code-review gate.
The Browser Verification inner prompt node carries `requiresBrowser: true` while keeping `toolMode: "coding"`. When that step runs, the executor best-effort adds the `agent-browser-navigation` skill (when the agent-browser plugin is installed), runs a bounded non-fatal `agent-browser --version` preflight, and writes start, availability, and finish entries into both the task log and the task's agent log. A missing or timed-out `agent-browser` binary is logged as an actionable warning rather than failing the step solely because of the preflight; the prompt can still fast-bail or report a normal verification failure. Because Bash tool events are already streamed to the agent log, `agent-browser open ...`, `agent-browser snapshot ...`, and related commands appear as the browser-verification activity the step performed.
Plugin-contributed gate kinds (documentation review, QA, security audit, performance, accessibility, frontend UX design, etc.) can still be supplied as palette templates; see [Plugin-Contributed Steps](#plugin-contributed-steps).
Built-in gate prompts emit the structured `{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}` envelope (final line JSON only). The legacy `REQUEST REVISION` prose path remains as a backward-compatible fallback. See [Prompt-mode Structured Verdict Contract](#prompt-mode-structured-verdict-contract).

View File

@@ -7,9 +7,20 @@ import {
parseWorkflowIr,
serializeWorkflowIr,
} from "../index.js";
import { BROWSER_VERIFICATION_GROUP_ID, BROWSER_VERIFICATION_STEP_NODE_ID } from "../builtin-browser-verification-group.js";
import type { WorkflowIrV2 } from "../workflow-ir-types.js";
const EXECUTE_NODE_MAX_RETRIES = 2;
function browserVerificationInnerConfig(ir: WorkflowIrV2): Record<string, unknown> {
const group = ir.nodes.find((node) => node.id === BROWSER_VERIFICATION_GROUP_ID);
expect(group?.kind).toBe("optional-group");
const template = group?.config?.template as { nodes?: Array<{ id: string; config?: Record<string, unknown> }> } | undefined;
const inner = template?.nodes?.find((node) => node.id === BROWSER_VERIFICATION_STEP_NODE_ID);
expect(inner).toBeDefined();
return inner?.config ?? {};
}
function executeNodeConfig(ir = BUILTIN_CODING_WORKFLOW_IR): Record<string, unknown> {
const executeNodes = ir.nodes.filter((node) => node.id === "execute" && node.config?.seam === "execute");
expect(executeNodes).toHaveLength(1);
@@ -53,6 +64,11 @@ describe("builtin coding workflow ir", () => {
expect(group?.kind).toBe("optional-group");
expect(group?.config?.name).toBe("Browser Verification");
expect(group?.config?.defaultOn).toBe(false);
expect(browserVerificationInnerConfig(BUILTIN_CODING_WORKFLOW_IR)).toMatchObject({
toolMode: "coding",
gateMode: "advisory",
requiresBrowser: true,
});
// execute → browser-verification → code-review → review on the success path; the
// pre-merge code-review optional-group sits next to browser-verification. failure → end.
expect(BUILTIN_CODING_WORKFLOW_IR.edges).toEqual(
@@ -149,6 +165,13 @@ describe("builtin coding workflow ir", () => {
);
});
it("marks browser verification as browser-capable in both coding built-ins", () => {
expect(browserVerificationInnerConfig(BUILTIN_CODING_WORKFLOW_IR).requiresBrowser).toBe(true);
expect(browserVerificationInnerConfig(BUILTIN_STEPWISE_CODING_WORKFLOW_IR).requiresBrowser).toBe(true);
expect(browserVerificationInnerConfig(BUILTIN_CODING_WORKFLOW_IR).toolMode).toBe("coding");
expect(browserVerificationInnerConfig(BUILTIN_STEPWISE_CODING_WORKFLOW_IR).toolMode).toBe("coding");
});
it("expresses merge policy regions in stepwise and PR built-ins", () => {
expect(BUILTIN_STEPWISE_CODING_WORKFLOW_IR.nodes.map((node) => node.kind)).toEqual(
expect.arrayContaining([

View File

@@ -9,6 +9,7 @@ import {
isBuiltinWorkflowPluginGated,
} from "../builtin-workflows.js";
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
import { BROWSER_VERIFICATION_GROUP_ID, BROWSER_VERIFICATION_STEP_NODE_ID } from "../builtin-browser-verification-group.js";
import { builtinPromptConfig, BUILTIN_SEAM_PROMPTS } from "../builtin-workflow-prompts.js";
import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js";
import { resolveColumnFlags } from "../trait-registry.js";
@@ -18,6 +19,12 @@ import { createSharedTaskStoreTestHarness } from "./store-test-helpers.js";
const EXECUTE_NODE_MAX_RETRIES = 2;
function browserVerificationInnerConfig(ir: { nodes: Array<{ id: string; kind: string; config?: Record<string, unknown> }> }): Record<string, unknown> {
const group = ir.nodes.find((node) => node.id === BROWSER_VERIFICATION_GROUP_ID);
const template = group?.config?.template as { nodes?: Array<{ id: string; config?: Record<string, unknown> }> } | undefined;
return template?.nodes?.find((node) => node.id === BROWSER_VERIFICATION_STEP_NODE_ID)?.config ?? {};
}
describe("built-in workflows", () => {
// Non-compiler built-ins model graph-only node kinds or reusable fragments the
// linear compiler cannot lower to a step list. They still must parse as valid IR.
@@ -165,6 +172,11 @@ describe("built-in workflows", () => {
expect(byId.get("workflow-step")).toBeUndefined();
expect(byId.get("browser-verification")?.kind).toBe("optional-group");
expect(byId.get("browser-verification")?.column).toBe("in-progress");
expect(browserVerificationInnerConfig(ir)).toMatchObject({
toolMode: "coding",
gateMode: "advisory",
requiresBrowser: true,
});
expect(byId.get("review")?.column).toBe("in-review");
// Merge is the native primitive region (FN-6035), placed in in-review.
expect(byId.get("merge")).toBeUndefined();

View File

@@ -24,6 +24,9 @@ to the former `browser-verification` catalog entry). These built-ins are the par
oracle, so the produced node bytes must NOT change. Plugin-contributed templates still
use the `WorkflowStepTemplate` shape via the editor palette, but built-ins no longer
read from a shared array.
FNXC:WorkflowBrowserVerification 2026-06-27-13:20:
The Browser Verification step must actually use the `agent-browser` workflow surface, not only mention it in the prompt. `requiresBrowser` tells the engine to load the agent-browser navigation skill when available, preflight CLI availability, and surface start/probe/tool activity in the task agent log while keeping the step's coding tool mode intact.
*/
/** Stable per-task enable key + group node id (preserved from the prior templateId). */
@@ -100,6 +103,7 @@ export function browserVerificationOptionalGroupNode(column: string): WorkflowIr
prompt: BROWSER_VERIFICATION_PROMPT,
toolMode: "coding",
gateMode: "advisory",
requiresBrowser: true,
},
},
],

View File

@@ -658,6 +658,13 @@ export interface WorkflowStep {
* skill (discovery + selection) and the engine injects the Fusion workflow-step
* conventions preamble. Only meaningful for skill-executor graph nodes. */
skillName?: string;
/**
* Browser capability requested by prompt-mode steps. When true, the executor
* loads the agent-browser navigation skill when available, preflights the
* `agent-browser` CLI, and records browser-verification activity in the agent
* log. Ignored for script-mode steps.
*/
requiresBrowser?: boolean;
/** Name of a script from project settings `scripts` map to execute (required when mode is "script") */
scriptName?: string;
/** Whether this step is available for selection on new tasks */

View File

@@ -35,7 +35,10 @@ function codingTask(enabledWorkflowSteps?: string[]): TaskDetail {
* handler keyed on the inner template node id; everything else succeeds. */
function makeExecutor(onInnerStep: () => void) {
const prompt = vi.fn<WorkflowNodeHandler>(async (node) => {
if (node.id === "browser-verification-step") onInnerStep();
if (node.id === "browser-verification-step") {
expect(node.config?.requiresBrowser).toBe(true);
onInnerStep();
}
return { outcome: "success" };
});
return new WorkflowGraphExecutor({ handlers: { prompt } });
@@ -55,7 +58,8 @@ describe("builtin coding browser-verification optional-group (U6)", () => {
disabledRuns++;
}).run(codingTask(), settingsOn(), BUILTIN_CODING_WORKFLOW_IR);
// The browser-verification step ran exactly once when enabled, never when off.
// The browser-verification step ran exactly once when enabled, carrying the
// browser capability flag; the disabled task never materializes the inner step.
expect(enabledRuns).toBe(1);
expect(disabledRuns).toBe(0);

View File

@@ -0,0 +1,267 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import type { WorkflowIrNode } from "@fusion/core";
import {
AGENT_BROWSER_NAVIGATION_SKILL_ID,
augmentSessionSkillsForBrowserStep,
formatAgentBrowserAvailabilityLog,
probeAgentBrowserAvailability,
TaskExecutor,
} from "../executor.js";
import { summarizeToolArgs } from "../agent-logger.js";
import {
createMockStore,
mockedCreateFnAgent,
mockedExecSync,
resetExecutorMocks,
} from "./executor-test-helpers.js";
type CapturedSession = {
skillSelection?: { requestedSkillNames?: string[]; projectRootDir?: string; sessionPurpose?: string };
};
function captureSession(output = '{"verdict":"APPROVE","notes":""}') {
const holder: { last?: CapturedSession } = {};
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
holder.last = { skillSelection: opts.skillSelection };
const listeners: Array<(event: any) => void> = [];
return {
session: {
state: {},
subscribe: (fn: (event: any) => void) => {
listeners.push(fn);
return () => {};
},
prompt: vi.fn(async () => {
for (const fn of listeners) {
fn({
type: "message_update",
assistantMessageEvent: {
type: "text_delta",
partial: output,
contentIndex: 0,
delta: output,
},
});
}
}),
dispose: vi.fn(),
},
};
});
return holder;
}
function baseTask(overrides: Record<string, unknown> = {}) {
return {
id: "FN-7130",
title: "Browser verification",
description: "exercise browser verification",
column: "in-progress" as const,
worktree: "/tmp/wt",
branch: "fusion/fn-7130",
baseCommitSha: "abc123",
dependencies: [],
steps: [{ name: "s", status: "in-progress" as const }],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
function makeExecutor(store: ReturnType<typeof createMockStore>) {
return new TaskExecutor(store as any, "/tmp/test", {
agentStore: { getAgent: vi.fn().mockResolvedValue(null), createAgent: vi.fn() },
} as any);
}
function browserVerificationStep(overrides: Record<string, unknown> = {}) {
return {
id: "graph:browser-verification-step",
name: "Browser Verification",
description: "",
mode: "prompt",
phase: "pre-merge",
gateMode: "advisory",
prompt: "Verify in browser.",
toolMode: "coding",
requiresBrowser: true,
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
describe("browser-verification workflow-step browser capability", () => {
beforeEach(() => {
resetExecutorMocks();
});
it("probes agent-browser availability as a bounded non-fatal helper", async () => {
await expect(probeAgentBrowserAvailability(async (command, options) => {
expect(command).toBe("agent-browser --version");
expect(options.timeout).toBeLessThanOrEqual(10_000);
return { stdout: "agent-browser 1.2.3\n", stderr: "" };
})).resolves.toEqual({ available: true, version: "agent-browser 1.2.3" });
await expect(probeAgentBrowserAvailability(async () => {
const err = new Error("spawn agent-browser ENOENT") as Error & { code: string };
err.code = "ENOENT";
throw err;
})).resolves.toEqual({ available: false, reason: "not installed" });
await expect(probeAgentBrowserAvailability(async () => {
const err = new Error("Command timed out") as Error & { code: string; killed: boolean };
err.code = "ETIMEDOUT";
err.killed = true;
throw err;
})).resolves.toEqual({ available: false, reason: "probe timed out" });
});
it("merges the agent-browser navigation skill idempotently", () => {
expect(augmentSessionSkillsForBrowserStep(undefined, "/repo")).toEqual({
projectRootDir: "/repo",
sessionPurpose: "executor",
requestedSkillNames: [AGENT_BROWSER_NAVIGATION_SKILL_ID],
});
expect(augmentSessionSkillsForBrowserStep({
projectRootDir: "/repo",
sessionPurpose: "executor",
requestedSkillNames: ["existing", AGENT_BROWSER_NAVIGATION_SKILL_ID],
}, "/fallback").requestedSkillNames).toEqual(["existing", AGENT_BROWSER_NAVIGATION_SKILL_ID]);
});
it("materializes requiresBrowser from graph prompt config and omits it when absent", async () => {
const store = createMockStore();
store.getTask.mockImplementation(async (id: string) => baseTask({ id }));
const executor = makeExecutor(store);
const captured: Array<Record<string, unknown>> = [];
vi.spyOn(executor as any, "executeWorkflowStep").mockImplementation(async (_task: unknown, step: Record<string, unknown>) => {
captured.push(step);
return { success: true, output: "ok" };
});
const browserNode: WorkflowIrNode = {
id: "custom-browser-step",
kind: "prompt",
config: { prompt: "Verify", toolMode: "coding", requiresBrowser: true },
};
const plainNode: WorkflowIrNode = { id: "plain-step", kind: "prompt", config: { prompt: "Review" } };
await (executor as any).runGraphCustomNode(browserNode, baseTask(), {}, undefined);
await (executor as any).runGraphCustomNode(plainNode, baseTask(), {}, undefined);
expect(captured[0]).toMatchObject({ id: "graph:custom-browser-step", requiresBrowser: true, toolMode: "coding" });
expect(captured[1]).not.toHaveProperty("requiresBrowser");
});
it("summarizes bash agent-browser commands for agent-log tool entries", () => {
expect(summarizeToolArgs("bash", { command: "agent-browser open http://localhost:5173" })).toBe(
"agent-browser open http://localhost:5173",
);
});
it("logs browser verification start, availability, and finish while augmenting session skills", async () => {
const store = createMockStore();
const executor = makeExecutor(store);
const cap = captureSession();
mockedExecSync.mockImplementation((command: string) => {
if (command === "agent-browser --version") return Buffer.from("agent-browser 9.9.9\n");
return Buffer.from("");
});
const result = await (executor as any).executeWorkflowStep(
baseTask(),
browserVerificationStep(),
"/tmp/wt",
{},
undefined,
undefined,
);
expect(result.success).toBe(true);
expect(cap.last?.skillSelection?.requestedSkillNames).toContain(AGENT_BROWSER_NAVIGATION_SKILL_ID);
expect(store.logEntry).toHaveBeenCalledWith(
"FN-7130",
"[browser-verification] agent-browser available — version agent-browser 9.9.9",
);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-7130",
expect.stringContaining("[browser-verification] starting browser verification"),
"text",
undefined,
"reviewer",
);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-7130",
"[browser-verification] agent-browser available — version agent-browser 9.9.9",
"text",
undefined,
"reviewer",
);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-7130",
"[browser-verification] finished browser verification for task FN-7130: verdict APPROVE",
"text",
undefined,
"reviewer",
);
});
it("logs an actionable warning and continues when agent-browser is missing", async () => {
const store = createMockStore();
const executor = makeExecutor(store);
captureSession();
mockedExecSync.mockImplementation((command: string) => {
if (command === "agent-browser --version") {
const err = new Error("spawn agent-browser ENOENT") as Error & { code: string };
err.code = "ENOENT";
throw err;
}
return Buffer.from("");
});
const result = await (executor as any).executeWorkflowStep(
baseTask(),
browserVerificationStep(),
"/tmp/wt",
{},
undefined,
undefined,
);
const warning = "[browser-verification] agent-browser not found on PATH — the step relies on the agent-browser CLI; install the agent-browser plugin/binary. Continuing; the step may fast-bail or fail.";
expect(result.success).toBe(true);
expect(formatAgentBrowserAvailabilityLog({ available: false, reason: "not installed" })).toBe(warning);
expect(store.logEntry).toHaveBeenCalledWith("FN-7130", warning);
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-7130", warning, "text", undefined, "reviewer");
});
it("keeps flag-absent prompt steps byte-inert for browser logging and skills", async () => {
const store = createMockStore();
const executor = makeExecutor(store);
const cap = captureSession();
mockedExecSync.mockImplementation((command: string) => {
if (command === "agent-browser --version") throw new Error("should not probe agent-browser");
return Buffer.from("");
});
const result = await (executor as any).executeWorkflowStep(
baseTask(),
browserVerificationStep({ id: "graph:plain", name: "Plain", toolMode: "readonly", requiresBrowser: undefined }),
"/tmp/wt",
{},
undefined,
undefined,
);
expect(result.success).toBe(true);
expect(cap.last?.skillSelection?.requestedSkillNames ?? []).not.toContain(AGENT_BROWSER_NAVIGATION_SKILL_ID);
expect(store.logEntry.mock.calls.some(([, message]: [string, string]) => message.includes("[browser-verification]"))).toBe(false);
expect(store.appendAgentLog.mock.calls.some(([, message]: [string, string]) => message.includes("[browser-verification]"))).toBe(false);
});
});

View File

@@ -5,6 +5,7 @@ import { setImmediate as setImmediateCb } from "node:timers";
// Internal git plumbing intentionally bypasses sandbox backends.
const execAsync = promisify(exec);
import { delimiter, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
import { existsSync, lstatSync, realpathSync } from "node:fs";
import { readFile, rm, writeFile } from "node:fs/promises";
@@ -76,6 +77,7 @@ import {
resolveExecutorSessionModel,
} from "./agent-session-helpers.js";
import { buildSessionSkillContext } from "./session-skill-context.js";
import type { SkillSelectionContext } from "./skill-resolver.js";
import { resolveMcpServersForStore } from "./mcp-resolution.js";
import { reviewStep, type ReviewVerdict, type ReviewResult } from "./reviewer.js";
import { selectUserCommentsForAgentContext } from "./agent-user-comments.js";
@@ -245,6 +247,89 @@ export {
taskLogParams,
} from "./agent-tools.js";
export const AGENT_BROWSER_NAVIGATION_SKILL_ID = "agent-browser-navigation";
export interface AgentBrowserAvailabilityProbeResult {
available: boolean;
version?: string;
reason?: string;
}
type AgentBrowserExec = (
command: string,
options: { encoding: BufferEncoding; timeout: number; maxBuffer: number; env?: NodeJS.ProcessEnv; cwd?: string },
) => Promise<{ stdout: string; stderr: string }>;
function isAgentBrowserNotFoundError(error: unknown): boolean {
const err = error as { code?: unknown; stderr?: unknown; message?: unknown } | null;
const code = typeof err?.code === "string" || typeof err?.code === "number" ? String(err.code) : undefined;
if (code === "ENOENT" || code === "127") return true;
const combined = `${typeof err?.stderr === "string" ? err.stderr : ""}\n${typeof err?.message === "string" ? err.message : ""}`.toLowerCase();
return combined.includes("agent-browser") && (combined.includes("not found") || combined.includes("command not found"));
}
function isAgentBrowserProbeTimeout(error: unknown): boolean {
const err = error as { code?: unknown; killed?: unknown; signal?: unknown; message?: unknown } | null;
return err?.code === "ETIMEDOUT"
|| err?.killed === true
|| err?.signal === "SIGTERM"
|| (typeof err?.message === "string" && err.message.toLowerCase().includes("timed out"));
}
/**
* Probe the agent-browser CLI without making browser verification fatal.
*
* FNXC:WorkflowBrowserVerification 2026-06-27-13:20:
* Browser Verification needs an actionable signal when `agent-browser` is absent or hung. Keep this async, bounded, and injectable so the executor logs availability without blocking or requiring the plugin at import time.
*/
export async function probeAgentBrowserAvailability(
execImpl: AgentBrowserExec = execAsync as AgentBrowserExec,
opts?: { timeoutMs?: number; maxBuffer?: number; env?: NodeJS.ProcessEnv; cwd?: string },
): Promise<AgentBrowserAvailabilityProbeResult> {
try {
const { stdout, stderr } = await execImpl("agent-browser --version", {
encoding: "utf-8",
timeout: Math.min(Math.max(opts?.timeoutMs ?? 5_000, 1_000), 10_000),
maxBuffer: opts?.maxBuffer ?? 64 * 1024,
...(opts?.env ? { env: opts.env } : {}),
...(opts?.cwd ? { cwd: opts.cwd } : {}),
});
const version = (stdout.trim() || stderr.trim() || "unknown").split("\n")[0]?.trim() || "unknown";
return { available: true, version };
} catch (error) {
if (isAgentBrowserNotFoundError(error)) {
return { available: false, reason: "not installed" };
}
if (isAgentBrowserProbeTimeout(error)) {
return { available: false, reason: "probe timed out" };
}
const reason = error instanceof Error ? error.message : String(error);
return { available: false, reason };
}
}
/** Merge the agent-browser navigation skill into a workflow-step session. */
export function augmentSessionSkillsForBrowserStep(
skillSelection: SkillSelectionContext | undefined,
projectRootDir: string,
): SkillSelectionContext {
const existing = skillSelection?.requestedSkillNames ?? [];
return {
projectRootDir: skillSelection?.projectRootDir ?? projectRootDir,
sessionPurpose: skillSelection?.sessionPurpose ?? "executor",
requestedSkillNames: [...new Set([...existing, AGENT_BROWSER_NAVIGATION_SKILL_ID])],
};
}
export function formatAgentBrowserAvailabilityLog(result: AgentBrowserAvailabilityProbeResult): string {
if (result.available) {
return `[browser-verification] agent-browser available — version ${result.version ?? "unknown"}`;
}
if (result.reason === "probe timed out") {
return "[browser-verification] agent-browser availability probe timed out — the step relies on the agent-browser CLI; continuing so the step can fast-bail or report its own failure.";
}
return "[browser-verification] agent-browser not found on PATH — the step relies on the agent-browser CLI; install the agent-browser plugin/binary. Continuing; the step may fast-bail or fail.";
}
const yieldEventLoop = (): Promise<void> => new Promise((resolve) => setImmediateCb(resolve));
function getPromptSection(prompt: string, heading: string): string {
@@ -6433,6 +6518,7 @@ export class TaskExecutor {
createdAt: now,
updatedAt: now,
...(stepSkillName ? { skillName: stepSkillName } : {}),
...(cfg.requiresBrowser === true ? { requiresBrowser: true } : {}),
...(modelProvider && modelId ? { modelProvider, modelId } : {}),
};
@@ -12869,6 +12955,20 @@ You have access to the file system to review changes.${verdictBlock}`;
);
}
const additionalSkillPaths = ceSkillsDir ? [ceSkillsDir] : undefined;
const logBrowserVerificationActivity = async (message: string) => {
await this.store.logEntry(task.id, message);
await this.store.appendAgentLog(task.id, message, "text", undefined, "reviewer");
};
if (workflowStep.requiresBrowser === true) {
effectiveSkillSelection = augmentSessionSkillsForBrowserStep(effectiveSkillSelection, this.rootDir);
await logBrowserVerificationActivity(`[browser-verification] starting browser verification for task ${task.id} using step '${workflowStep.name}'`);
const browserProbe = await probeAgentBrowserAvailability(execAsync as AgentBrowserExec, {
cwd: worktreePath,
env: stepEnv,
timeoutMs: 5_000,
});
await logBrowserVerificationActivity(formatAgentBrowserAvailabilityLog(browserProbe));
}
// (U8b) Coding-mode skill steps fan out to ce-<persona> subagents via
// fn_spawn_agent (read the persona def, pass its body as systemPromptOverride).
@@ -12987,6 +13087,9 @@ You have access to the file system to review changes.${verdictBlock}`;
task.id,
`Workflow step '${workflowStep.name}' ${attemptLabel === "primary" ? "primary" : "fallback"} model timed out after ${Math.round(timeoutMs / 1000)}s — aborting session`,
);
if (workflowStep.requiresBrowser === true) {
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: timed out`);
}
try { session.dispose(); } catch { /* best-effort */ }
await agentLogger.flush();
return { success: false, error: `workflow step timed out after ${timeoutMs}ms`, timedOut: true };
@@ -13004,6 +13107,9 @@ You have access to the file system to review changes.${verdictBlock}`;
const parsed = this.parseWorkflowStepOutput(output);
if (parsed.verdict) {
const revisionRequested = parsed.verdict === "REVISE";
if (workflowStep.requiresBrowser === true) {
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: verdict ${parsed.verdict}`);
}
return {
success: !revisionRequested,
revisionRequested,
@@ -13018,6 +13124,9 @@ You have access to the file system to review changes.${verdictBlock}`;
task.id,
`[pre-merge] Workflow step '${workflowStep.name}' produced malformed output — blocking gate success`,
);
if (workflowStep.requiresBrowser === true) {
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: malformed output`);
}
return {
success: false,
output: parsed.output,
@@ -13027,6 +13136,9 @@ You have access to the file system to review changes.${verdictBlock}`;
};
}
if (workflowStep.requiresBrowser === true) {
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: completed`);
}
return { success: true, output: parsed.output };
} catch (err: unknown) {
await agentLogger.flush();
@@ -13038,9 +13150,15 @@ You have access to the file system to review changes.${verdictBlock}`;
task.id,
`[readonly-violation] Workflow step '${workflowStep.name}' attempted denied tool '${deniedTool}'`,
);
if (workflowStep.requiresBrowser === true) {
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: readonly violation`);
}
return { success: false, error: `[readonly-violation] ${violation.message}` };
}
const errorMessage = err instanceof Error ? err.message : String(err);
if (workflowStep.requiresBrowser === true) {
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: failed — ${errorMessage}`);
}
return { success: false, error: errorMessage };
} finally {
if (timeoutHandle) clearTimeout(timeoutHandle);