diff --git a/docs/reports/e2e-smoke-2026-10-25.md b/docs/reports/e2e-smoke-2026-10-25.md index 008334e..8e31dff 100644 --- a/docs/reports/e2e-smoke-2026-10-25.md +++ b/docs/reports/e2e-smoke-2026-10-25.md @@ -77,3 +77,11 @@ The spawned `claude` inherits the user's global configuration. In the rerun, the ### Still untested Tier-2 auto-resolution on a real conflict, three or more conflicting tasks, an agent that fails or times out, `gemini`/`codex`. + +## Third run: agents isolated from the operator's Claude Code setup (2026-10-26) + +Change: `claude` is started with `--setting-sources project,local` (opt back in with `FSC_CLAUDE_USER_SETTINGS=1`) and with stdin closed. + +- Trivial prompt, default vs isolated: 22 s vs 9 s. Default: the agent could recite the operator's tracker rules and the Stop hook; isolated: it knew neither. +- Same two-task repo as the first run, real `claude` agents: exec phase **20 s** (first run: 77 s). Both tasks merged at tier 1; `main` has the two implementations; agent logs contain no tracker or hook talk. +- **Correction:** this run took 15 min 28 s wall clock, not 20 s: analysis and execution finished within about a minute (file timestamps: 20 s + 27 s) and the process then idled for ~14 minutes. So the slow runs are not explained by a user-level hook (this run had them isolated). The cause is a different defect, found right after this report and fixed in a follow-up PR: the agent timeout timer was never cleared, so a successful run kept the process alive until the 15-minute timeout fired. Failing runs exited through `process.exit(1)` and never showed it. That also explains why only successful runs were slow. diff --git a/packages/core/src/analyzerNative.ts b/packages/core/src/analyzerNative.ts index c28aa72..5b4a263 100644 --- a/packages/core/src/analyzerNative.ts +++ b/packages/core/src/analyzerNative.ts @@ -6,6 +6,7 @@ import { analyzerPrompt } from './prompts/analyzerPrompt'; import { getCodingStyle } from './codingStyle'; import { getWorkStyleDescription, WorkStyle } from './workStyle'; import { trimJSONObjectArray } from './utils/trimJSON'; +import { claudeIsolationArgs } from './execution/types'; const ALLOWED_TOOLS = 'Bash,ReadEdit,Glob,Grep,WebFetch,WebSearch,Write,TodoWrite,SlashCommand'; const DEFAULT_TIMEOUT_SECONDS = 1200; @@ -91,9 +92,11 @@ export async function analyzeCodebaseNative(config: Config, repoPath: string): P ALLOWED_TOOLS, '--permission-mode', 'bypassPermissions', + ...claudeIsolationArgs(), ], cwd: scratch, env: { ...agentEnv(config), IS_SANDBOX: '1' }, + stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', }); diff --git a/packages/core/src/execution/local.ts b/packages/core/src/execution/local.ts index 1e77b06..8cf44c4 100644 --- a/packages/core/src/execution/local.ts +++ b/packages/core/src/execution/local.ts @@ -64,6 +64,8 @@ export class LocalExecution implements ExecutionStrategy { FSC_WORKTREE: worktreePath, ...(unit.context?.memorixUri ? { MEMORIX_URL: unit.context.memorixUri } : {}), }, + // No stdin: an inherited, silent stdin makes `claude -p` wait ~3 s before it starts. + stdin: "ignore", stdout: "pipe", stderr: "pipe", }); diff --git a/packages/core/src/execution/types.ts b/packages/core/src/execution/types.ts index 312e1e7..23b9054 100644 --- a/packages/core/src/execution/types.ts +++ b/packages/core/src/execution/types.ts @@ -63,9 +63,24 @@ export interface WorktreeInfo { } // Pre-built agent arg builders +/** + * Agents must not inherit the operator's personal Claude Code setup: user-level hooks (a Stop hook + * can block or slow a headless run) and the user's global CLAUDE.md both apply by default. Measured + * on a trivial prompt: default 22 s and the agent recited the operator's tracker rules; with + * `--setting-sources project,local` 9 s and none of it. Project and local settings still apply. + * Set FSC_CLAUDE_USER_SETTINGS=1 to opt back in. + */ +export function claudeIsolationArgs(): string[] { + return process.env.FSC_CLAUDE_USER_SETTINGS === "1" ? [] : ["--setting-sources", "project,local"]; +} + export const AGENT_BUILDERS: Record = { - claude: (prompt: string, cwd: string) => - ["-p", prompt, "--allowedTools", "Edit,Write,Bash,Read,Glob,Grep", "--permission-mode", "bypassPermissions"], + claude: (prompt: string, _cwd: string) => [ + "-p", prompt, + "--allowedTools", "Edit,Write,Bash,Read,Glob,Grep", + "--permission-mode", "bypassPermissions", + ...claudeIsolationArgs(), + ], codex: (prompt: string, cwd: string) => ["exec", "--approval-mode", "full-auto", "-w", cwd, prompt], gemini: (prompt: string, cwd: string) => diff --git a/packages/core/test/execution/mapping.test.ts b/packages/core/test/execution/mapping.test.ts index a36ea7f..d1299f7 100644 --- a/packages/core/test/execution/mapping.test.ts +++ b/packages/core/test/execution/mapping.test.ts @@ -83,3 +83,33 @@ describe("normalizeAgentType", () => { expect(args).toContain("--permission-mode"); }); }); + +describe("claude isolation", () => { + const { toWorkUnitForAgent } = require("../../src/execution/mapping"); + const task = { ID: "t", title: "x", description: "y", priority: 1 }; + + test("agents ignore the operator's user-level settings by default", () => { + const prev = process.env.FSC_CLAUDE_USER_SETTINGS; + delete process.env.FSC_CLAUDE_USER_SETTINGS; + try { + const args = toWorkUnitForAgent(task, "claude-code", ".").agentConfig.buildArgs("p", "/wt"); + const i = args.indexOf("--setting-sources"); + expect(i).toBeGreaterThan(-1); + expect(args[i + 1]).toBe("project,local"); + } finally { + if (prev !== undefined) process.env.FSC_CLAUDE_USER_SETTINGS = prev; + } + }); + + test("FSC_CLAUDE_USER_SETTINGS=1 opts back in", () => { + const prev = process.env.FSC_CLAUDE_USER_SETTINGS; + process.env.FSC_CLAUDE_USER_SETTINGS = "1"; + try { + const args = toWorkUnitForAgent(task, "claude-code", ".").agentConfig.buildArgs("p", "/wt"); + expect(args).not.toContain("--setting-sources"); + } finally { + if (prev === undefined) delete process.env.FSC_CLAUDE_USER_SETTINGS; + else process.env.FSC_CLAUDE_USER_SETTINGS = prev; + } + }); +});