Observed behavior
loadCliConfig always runs normalizeCliConfig (and through it normalizeAgentSpec) before applying the --dry-run override that replaces the configured agent with DRY_RUN_AGENT_SPEC. normalizeAgentSpec awaits expandIncludes for any systemPrompt on a claude-sdk or openai-sdk agent spec. If the referenced include file does not exist, --dry-run fails with a low-level ENOENT even though the agent spec is about to be thrown away.
Reproduction with a missing include and --dry-run:
FAILED: ENOENT: no such file or directory, open '/tmp/dryrun-test-XXXXXX/does-not-exist.md'
The failing config:
{
"name": "test",
"agent": ["claude-sdk", { "systemPrompt": "{{include:./does-not-exist.md}}" }],
"promptGenerator": ["test", { "prompts": ["hi"] }]
}
The order in loadCliConfig is:
return {
...(await normalizeCliConfig(config, resolvedPath)), // expands systemPrompt includes here
...(maxPrompts !== undefined ? { maxPrompts } : {}),
...(verbose === true || dryRun === true ? { logger: 'verbose' as const } : {}),
...(dryRun === true ? { agent: DRY_RUN_AGENT_SPEC } : {}), // agent thrown away here
};
Expected behavior
When --dry-run is set the agent is replaced with DRY_RUN_AGENT_SPEC, so the configured agent's systemPrompt is never sent to a backend. A user iterating on a config (where the include file may not yet exist, or the path is being edited) should be able to use --dry-run to validate the prompt generator without also having to make the agent's systemPrompt includes resolve.
Today --dry-run does noticeably more work than necessary (reads include files from disk) and, more importantly, spuriously fails when a path that will never be used is broken.
Minimal reproduction
mkdir /tmp/loop-dry && cd /tmp/loop-dry
cat > config.json <<'JSON'
{
"name": "test",
"agent": ["claude-sdk", { "systemPrompt": "{{include:./missing.md}}" }],
"promptGenerator": ["test", { "prompts": ["hi"] }]
}
JSON
loop-the-loop --dry-run config.json
Result: fails with ENOENT: no such file or directory, open '/tmp/loop-dry/missing.md' instead of running a dry run.
Suggested fix
Short-circuit the agent normalization when dryRun is true. The simplest approach is to skip normalizeAgentSpec (or pass through config.agent unchanged) when parsedArgs.dryRun === true, since the agent will be replaced by DRY_RUN_AGENT_SPEC immediately afterwards. For example, inline the dry-run branch in loadCliConfig before calling normalizeCliConfig, or have normalizeCliConfig accept an optional flag that skips the agent step.
Observed behavior
loadCliConfigalways runsnormalizeCliConfig(and through itnormalizeAgentSpec) before applying the--dry-runoverride that replaces the configured agent withDRY_RUN_AGENT_SPEC.normalizeAgentSpecawaitsexpandIncludesfor anysystemPrompton aclaude-sdkoropenai-sdkagent spec. If the referenced include file does not exist,--dry-runfails with a low-levelENOENTeven though the agent spec is about to be thrown away.Reproduction with a missing include and
--dry-run:The failing config:
{ "name": "test", "agent": ["claude-sdk", { "systemPrompt": "{{include:./does-not-exist.md}}" }], "promptGenerator": ["test", { "prompts": ["hi"] }] }The order in
loadCliConfigis:Expected behavior
When
--dry-runis set the agent is replaced withDRY_RUN_AGENT_SPEC, so the configured agent'ssystemPromptis never sent to a backend. A user iterating on a config (where the include file may not yet exist, or the path is being edited) should be able to use--dry-runto validate the prompt generator without also having to make the agent'ssystemPromptincludes resolve.Today
--dry-rundoes noticeably more work than necessary (reads include files from disk) and, more importantly, spuriously fails when a path that will never be used is broken.Minimal reproduction
Result: fails with
ENOENT: no such file or directory, open '/tmp/loop-dry/missing.md'instead of running a dry run.Suggested fix
Short-circuit the agent normalization when
dryRunis true. The simplest approach is to skipnormalizeAgentSpec(or pass throughconfig.agentunchanged) whenparsedArgs.dryRun === true, since the agent will be replaced byDRY_RUN_AGENT_SPECimmediately afterwards. For example, inline the dry-run branch inloadCliConfigbefore callingnormalizeCliConfig, or havenormalizeCliConfigaccept an optional flag that skips the agent step.