diff --git a/src/config.ts b/src/config.ts index f499b58f..7036aab1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; -import type { AgentId } from "./agents/types.js"; +import type { AgentId, PermissionMode, SandboxMode } from "./agents/types.js"; export const DEFAULT_RUN_TIMEOUT_MS = 6 * 60 * 60 * 1000; export const DEFAULT_DASHBOARD_THEME: DashboardTheme = "dark"; @@ -12,6 +12,9 @@ export type DashboardTheme = "light" | "dark"; export interface AgentModelDefaults { model?: string; reasoningEffort?: string; + permissionMode?: PermissionMode; + sandbox?: SandboxMode; + extraArgs?: string[]; } export type AgentModelDefaultsByAgent = Partial>; @@ -155,7 +158,7 @@ function parseSimpleYaml( const values: Partial> = { agentDefaults: {} }; - let section: "agents" | "workflow" | "workflow.softEnforcement" | "workflow.scripts" | "workflow.scripts.allow" | undefined; + let section: "agents" | "workflow" | "workflow.softEnforcement" | "workflow.scripts" | "workflow.scripts.allow" | "agents.extraArgs" | undefined; let currentAgent: AgentId | undefined; for (const line of contents.split("\n")) { @@ -176,6 +179,26 @@ function parseSimpleYaml( continue; } + // agent extraArgs list items (e.g., ` - --skip-git-repo-check`) + if (section === "agents.extraArgs" && indent >= 6 && currentAgent !== undefined && trimmed.startsWith("- ")) { + const item = trimmed.slice(2).trim().replace(/^["']|["']$/g, ""); + if (item.length > 0) { + const defaults = values.agentDefaults?.[currentAgent] ?? {}; + defaults.extraArgs = [...(defaults.extraArgs ?? []), item]; + values.agentDefaults = { + ...values.agentDefaults, + [currentAgent]: defaults + }; + } + continue; + } + + // extraArgs list is done — reset section so sibling fields (model, + // permissionMode, sandbox) under the same agent are not silently ignored + if (section === "agents.extraArgs" && indent >= 4 && currentAgent !== undefined && !trimmed.startsWith("- ")) { + section = "agents"; + } + const separatorIndex = trimmed.indexOf(":"); if (separatorIndex < 0) { continue; @@ -233,8 +256,9 @@ function parseSimpleYaml( section = "workflow.scripts.allow"; continue; } - if (section === "agents" && indent === 2) { + if ((section === "agents" || section === "agents.extraArgs") && indent === 2) { currentAgent = key === "codex" || key === "claude" ? key : undefined; + section = "agents"; continue; } if (section === "agents" && indent >= 4 && currentAgent !== undefined && value.length > 0) { @@ -245,6 +269,25 @@ function parseSimpleYaml( if (key === "reasoningEffort") { defaults.reasoningEffort = value; } + if (key === "permissionMode") { + defaults.permissionMode = value as PermissionMode; + } + if (key === "sandbox") { + defaults.sandbox = value as SandboxMode; + } + values.agentDefaults = { + ...values.agentDefaults, + [currentAgent]: defaults + }; + } + if (section === "agents" && indent >= 4 && currentAgent !== undefined && key === "extraArgs") { + const defaults = values.agentDefaults?.[currentAgent] ?? {}; + if (value === "[]" || value.length === 0) { + defaults.extraArgs = []; + section = "agents.extraArgs"; + } else { + defaults.extraArgs = parseCommaList(value); + } values.agentDefaults = { ...values.agentDefaults, [currentAgent]: defaults @@ -262,11 +305,13 @@ function mergeAgentDefaults( return { codex: { ...defaults.codex, - ...overrides?.codex + ...overrides?.codex, + extraArgs: overrides?.codex?.extraArgs ?? defaults.codex?.extraArgs }, claude: { ...defaults.claude, - ...overrides?.claude + ...overrides?.claude, + extraArgs: overrides?.claude?.extraArgs ?? defaults.claude?.extraArgs } }; } diff --git a/src/hub-server.ts b/src/hub-server.ts index 05b39864..11386839 100644 --- a/src/hub-server.ts +++ b/src/hub-server.ts @@ -30,7 +30,8 @@ async function main(): Promise { jsonRpcUrl, store, initialRuns, - defaultRunTimeoutMs: config.defaultRunTimeoutMs + defaultRunTimeoutMs: config.defaultRunTimeoutMs, + agentDefaults: config.agentDefaults }); const workflowCoordinator = new WorkflowCoordinator(coordinator, { store: workflowStore, diff --git a/src/hub/runs.ts b/src/hub/runs.ts index 09b5dade..c3d6e96c 100644 --- a/src/hub/runs.ts +++ b/src/hub/runs.ts @@ -147,6 +147,9 @@ export class AgentRunCoordinator { ...input, model: input.model ?? defaults.model, reasoningEffort: input.reasoningEffort ?? defaults.reasoningEffort, + permissionMode: input.permissionMode ?? defaults.permissionMode, + sandbox: input.sandbox ?? defaults.sandbox, + extraArgs: input.extraArgs ?? defaults.extraArgs, cwd: input.cwd ?? parentRun?.cwd, workflowContext: input.workflowContext ?? inheritedWorkflowContext }; diff --git a/tests/config.test.ts b/tests/config.test.ts index aefc78d9..c57a2fec 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -93,6 +93,69 @@ describe("humanize2 user config", () => { expect(config.defaultTheme).toBe("light"); }); + it("loads agent model fields after a prior agent's multiline extraArgs list", async () => { + const home = await tempDirectory(); + const configPath = join(home, "multiline-extraargs-config.yaml"); + + const contents = [ + "version: 1", + "cacheDir: /tmp/humanize2-cache", + "defaultRunTimeoutMs: 12345", + "defaultTheme: dark", + "agents:", + " codex:", + " model: gpt-5.4", + " extraArgs:", + " - --temperature", + " - 0.7", + " - --max-tokens", + " - 4096", + " claude:", + " model: claude-opus-4-7", + " reasoningEffort: xhigh", + "" + ].join("\n"); + await writeFile(configPath, contents, "utf8"); + + const config = await loadHumanizeConfig({ HUMANIZE2_CONFIG: configPath }, home); + + expect(config.agentDefaults.codex).toMatchObject({ + model: "gpt-5.4", + extraArgs: ["--temperature", "0.7", "--max-tokens", "4096"] + }); + expect(config.agentDefaults.claude).toMatchObject({ + model: "claude-opus-4-7", + reasoningEffort: "xhigh" + }); + }); + + it("loads sibling fields after empty extraArgs under the same agent", async () => { + const home = await tempDirectory(); + const configPath = join(home, "sibling-after-extraargs-config.yaml"); + + const contents = [ + "version: 1", + "cacheDir: /tmp/humanize2-cache", + "defaultRunTimeoutMs: 12345", + "defaultTheme: dark", + "agents:", + " codex:", + " extraArgs: []", + " model: gpt-5.5", + " reasoningEffort: xhigh", + "" + ].join("\n"); + await writeFile(configPath, contents, "utf8"); + + const config = await loadHumanizeConfig({ HUMANIZE2_CONFIG: configPath }, home); + + expect(config.agentDefaults.codex).toMatchObject({ + model: "gpt-5.5", + reasoningEffort: "xhigh", + extraArgs: [] + }); + }); + it("loads workflow retry and script allowlist settings from the user config", async () => { const home = await tempDirectory(); const configPath = join(home, "workflow-config.yaml");