Skip to content

loadCliConfig: --dry-run still resolves systemPrompt {{include:...}} macros even though the agent is replaced #86

@joewalker

Description

@joewalker

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    S3Edge cases, confusing APIs, maintainability issues, or moderate performance problemsbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions