Repository navigation
Conversation
This was referenced Aug 3, 2026
Contributor
Author
|
Superseded by #5 — same change, cherry-picked onto current The merge wrapper refused this PR on a genuine stale base: Two things changed while resolving, both toward this repo's stated convention:
73 tests still pass, and the consuming runner's gate was re-probed against the rebased branch: Branch deleted to close. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A consumer built an eval runner that replays real recorded prompts against this package. Some of those prompts are action-shaped — "ship it", "commit and merge" — so replay is only safe if each call runs confined to a throwaway fixture directory under a restrictive permission mode.
It could not do that.
call_agenttook only(agent_name, prompt, schema, budget_usd), built a fixed argv, and never passed a working directory or a permission mode to theclaudesubprocess. With no way to confine a replayed "ship it", the consumer refused to run replay mode at all, and its nightly evals were blocked on this package. This unblocks them.Summary
Two optional keyword-only arguments on
call_agent, both defaulting toNone:cwd: str | Path | None— runs the subprocess in that directory. Validated to be an existing directory before anything is dispatched; an unusable path raisesAgentError.AgentErroris deliberate rather than a bareValueError: insidefan_outit already converts to that unit's{"_error": ...}row, so one bad path is a visible coverage gap instead of a dead batch.permission_mode: str | None— appended as--permission-mode <mode>.Both are threaded through
fan_outand through the campaign entry pointdeliver.run, on the same optional-and-defaulted contract. Inside a campaign they apply to the worker, advisor, and grader alike — otherwise an escalation would step outside the sandbox its worker ran in, which is exactly the hole the consumer was worried about.agent_optionsis the small helper that builds those kwargs and drops whatever is unset; it is what makes the omitted path byte-identical, and it is re-exported for anyone writing their owncall=.Flag name is verified, not guessed
claude --helpon the CLI in this environment:The runner does not whitelist those values. It passes the string through, matching how
Tiersalready treats model-tier names as caller-supplied vocabulary — so a mode the CLI adds later works without a release here.Backward compatibility
Omitting both leaves behavior unchanged, and that is pinned by tests rather than asserted:
--permission-modein the argv;cwdkeyword on thesubprocess.runcall at all — not evencwd=None. The pre-existing test whose stub isdef fake_run(cmd, capture_output, text, timeout)wouldTypeErroron an unconditionalcwd=, so it enforces this by construction;call=functions with the four-positional signature are invoked exactly as before — the existingfan_outanddelivertests all use such stubs and were not touched.Docs land in the same change (README section + docstrings), per CONTRIBUTING.
One judgement call to flag: CONTRIBUTING says changelog entries go under
## Unreleased, but the brief called for a minor bump. I bumped0.1.0 -> 0.2.0inpyproject.tomland__version__and filed the entry under## 0.2.0so the changelog heading and the declared version agree. Say the word and I will move it back under## Unreleasedand revert the bump.Test plan
TDD: all eight new tests were written first and run before any implementation. Six went red (
TypeError: call_agent() got an unexpected keyword argument 'permission_mode', etc.); the other two are the backward-compatibility pins, which pass onmainby design and would only break if this change altered the omitted path. Then implemented to green (65 -> 73 tests). No test spawns a realclaudeprocess or touches the network —subprocess.runis monkeypatched and campaign calls are injected viacall=, per the CONTRIBUTING rule.Covered: default behavior unchanged with both omitted;
cwdreaches the subprocess call; a nonexistentcwdand a path that is a file both raise before any subprocess starts;permission_modeappears in the built argv; both together;fan_outforwards both and passes nothing when unset;deliver.runconfines worker + advisor + grader;deliver.runwithout the options still drives a four-argumentcall=.ruff checkoutput is byte-identical tomain's baseline (same 9 pre-existing findings, same 3 filesruff formatwould touch) — this branch adds none and no line exceeds the configured 100 columns. Diff is 209 insertions / 13 deletions.