Skip to content

Commit 601c01c

Browse files
authored
Merge pull request #105 from AxmeAI/fix/audit-worker-claude-path-20260414
fix(audit): set pathToClaudeCodeExecutable on all direct SDK call sites (B-006)
2 parents 930d251 + 3ed48c9 commit 601c01c

4 files changed

Lines changed: 98 additions & 2 deletions

File tree

src/agents/memory-extractor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import type { Memory } from "../types.js";
1313
import { extractCostFromResult, zeroCost, type CostInfo } from "../utils/cost-extractor.js";
1414
import { toMemorySlug } from "../storage/memory.js";
15+
import { findClaudePath } from "../utils/agent-options.js";
1516

1617
export interface MemoryExtractionResult {
1718
memories: Memory[];
@@ -68,9 +69,11 @@ export async function runMemoryExtraction(opts: {
6869
const startTime = Date.now();
6970
const model = opts.model ?? "claude-haiku-4-5";
7071

72+
const claudePath = findClaudePath();
7173
const queryOpts = {
7274
cwd: opts.projectPath,
7375
model,
76+
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
7477
permissionMode: "bypassPermissions" as const,
7578
allowDangerouslySkipPermissions: true,
7679
allowedTools: [] as string[],

src/agents/session-auditor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { basename, relative } from "node:path";
2121
import type { Memory, Decision, SessionHandoff, WorkspaceInfo } from "../types.js";
2222
import { DEFAULT_AUDITOR_MODEL } from "../types.js";
2323
import { extractCostFromResult, zeroCost, type CostInfo } from "../utils/cost-extractor.js";
24+
import { findClaudePath } from "../utils/agent-options.js";
2425
import { toMemorySlug } from "../storage/memory.js";
2526
import { toSlug, listDecisions } from "../storage/decisions.js";
2627
import { listMemories } from "../storage/memory.js";
@@ -624,12 +625,14 @@ async function runSingleAuditCall(opts: {
624625
}> {
625626
const sdk = await import("@anthropic-ai/claude-agent-sdk");
626627

628+
const claudePath = findClaudePath();
627629
const queryOpts = {
628630
cwd: opts.sessionOrigin,
629631
model: opts.model,
630632
systemPrompt: AUDIT_SYSTEM_PROMPT,
631633
settingSources: [],
632634
mcpServers: {},
635+
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
633636
permissionMode: "bypassPermissions" as const,
634637
allowDangerouslySkipPermissions: true,
635638
allowedTools: ["Read", "Grep", "Glob"],
@@ -883,12 +886,14 @@ JSON SCHEMA:
883886
ANALYSIS TO FORMAT:
884887
${freeTextAnalysis}`;
885888

889+
const claudePath = findClaudePath();
886890
const queryOpts = {
887891
cwd: sessionOrigin,
888892
model,
889893
systemPrompt: "You are a JSON formatting assistant. Output only a ```json code fence with the structured data. No other text.",
890894
settingSources: [] as any[],
891895
mcpServers: {},
896+
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
892897
permissionMode: "bypassPermissions" as const,
893898
allowDangerouslySkipPermissions: true,
894899
allowedTools: [] as string[],

src/utils/agent-options.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ import { execSync } from "node:child_process";
66

77
type Options = import("@anthropic-ai/claude-agent-sdk").Options;
88

9-
/** Find claude binary path. Cached after first lookup. */
9+
/**
10+
* Find claude binary path. Cached after first lookup.
11+
*
12+
* Exported because the SDK resolves its own path via `import.meta.url`, which
13+
* returns undefined inside the bundled CJS build and crashes with
14+
* `fileURLToPath(undefined)` (B-006 / D-121). Every direct `sdk.query()` call
15+
* site must set `pathToClaudeCodeExecutable` to the result of this function.
16+
*/
1017
let _claudePath: string | undefined;
11-
function findClaudePath(): string | undefined {
18+
export function findClaudePath(): string | undefined {
1219
if (_claudePath !== undefined) return _claudePath || undefined;
1320
try {
1421
_claudePath = execSync("which claude", { encoding: "utf-8" }).trim();

test/agent-sdk-paths.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Regression guard for B-006 / D-121.
3+
*
4+
* The Claude Agent SDK resolves its own executable via `import.meta.url`,
5+
* which is undefined inside the bundled CJS build and crashes with
6+
* `fileURLToPath(undefined)`. Every direct `sdk.query()` call site must
7+
* therefore either:
8+
* a) build its options through `buildAgentQueryOptions()` (which sets
9+
* `pathToClaudeCodeExecutable` via `findClaudePath()`), or
10+
* b) import `findClaudePath` and set `pathToClaudeCodeExecutable`
11+
* explicitly on its own options object.
12+
*
13+
* This test greps every `src/agents/**.ts` file for `sdk.query(` and fails
14+
* if none of the two helpers are imported. It is static, so it does not
15+
* require the SDK, the `claude` binary, or any network access.
16+
*/
17+
18+
import { readFileSync, readdirSync, statSync } from "node:fs";
19+
import { join } from "node:path";
20+
import { test } from "node:test";
21+
import assert from "node:assert";
22+
23+
const AGENTS_DIR = new URL("../src/agents/", import.meta.url).pathname;
24+
25+
function walk(dir: string, out: string[] = []): string[] {
26+
for (const entry of readdirSync(dir)) {
27+
const full = join(dir, entry);
28+
const st = statSync(full);
29+
if (st.isDirectory()) walk(full, out);
30+
else if (entry.endsWith(".ts")) out.push(full);
31+
}
32+
return out;
33+
}
34+
35+
test("every src/agents file calling sdk.query imports buildAgentQueryOptions or findClaudePath", () => {
36+
const files = walk(AGENTS_DIR);
37+
const offenders: string[] = [];
38+
39+
for (const file of files) {
40+
const src = readFileSync(file, "utf-8");
41+
if (!src.includes("sdk.query(")) continue;
42+
43+
const hasBuilder = /import\s+[^;]*\bbuildAgentQueryOptions\b[^;]*from\s+["'][^"']*agent-options/.test(src);
44+
const hasFinder = /import\s+[^;]*\bfindClaudePath\b[^;]*from\s+["'][^"']*agent-options/.test(src);
45+
46+
if (!hasBuilder && !hasFinder) {
47+
offenders.push(file.replace(AGENTS_DIR, ""));
48+
}
49+
}
50+
51+
assert.deepStrictEqual(
52+
offenders,
53+
[],
54+
`The following files call sdk.query() but import neither buildAgentQueryOptions ` +
55+
`nor findClaudePath from utils/agent-options. Without pathToClaudeCodeExecutable ` +
56+
`the bundled CJS build will crash with fileURLToPath(undefined) (B-006 / D-121):\n` +
57+
` ${offenders.join("\n ")}`,
58+
);
59+
});
60+
61+
test("session-auditor.ts manual queryOpts include pathToClaudeCodeExecutable", () => {
62+
const src = readFileSync(new URL("../src/agents/session-auditor.ts", import.meta.url), "utf-8");
63+
const queryOptsBlocks = src.split("const queryOpts = {").slice(1);
64+
assert.ok(queryOptsBlocks.length >= 2, "expected at least 2 queryOpts blocks in session-auditor.ts");
65+
66+
for (const block of queryOptsBlocks) {
67+
const untilClose = block.slice(0, block.indexOf("};"));
68+
assert.ok(
69+
untilClose.includes("pathToClaudeCodeExecutable"),
70+
`session-auditor.ts queryOpts block missing pathToClaudeCodeExecutable:\n${untilClose.slice(0, 300)}`,
71+
);
72+
}
73+
});
74+
75+
test("memory-extractor.ts manual queryOpts include pathToClaudeCodeExecutable", () => {
76+
const src = readFileSync(new URL("../src/agents/memory-extractor.ts", import.meta.url), "utf-8");
77+
assert.ok(
78+
src.includes("pathToClaudeCodeExecutable"),
79+
"memory-extractor.ts must set pathToClaudeCodeExecutable on its queryOpts",
80+
);
81+
});

0 commit comments

Comments
 (0)