idle-draft: per-work-type and per-provider model/effort settings
--effort/--model on child argv; provider entries accept object form
{provider, model, effort}; precedence entry > work-type > profile.
Effort levels validated against the CLI's enumerated set.
Claude-Session: https://claude.ai/code/session_01YQDoWNM7XPPii28khFWoMc
This commit is contained in:
@@ -239,14 +239,21 @@ For the selected `(item_or_dossier, work_type, provider)`:
|
||||
invoked** (it has interactive pickers and terminal theming unsuitable for headless
|
||||
cron use).
|
||||
4. Build the argv: `claude -p --max-turns <N>` (`N` = 25 unless overridden), plus
|
||||
`--model <id>` if `provider.env` sets `MODEL_ID`, plus `--add-dir <dir>` for each
|
||||
entry in `config["evidence_dirs"]` **only** for `research` and `topic_ideas` work
|
||||
types (the only ones that cite external evidence), plus `--allowedTools
|
||||
`--model <id>` and `--effort <level>` resolved through the full precedence chain —
|
||||
per-provider override on the selected provider's `providers`-list entry, then
|
||||
work-type-level `model`/`effort`, then (for `model` only) `provider.env`'s
|
||||
`MODEL_ID`; the flag is omitted at each layer where nothing resolves (see
|
||||
"Per-work-type effort and model" and "Per-provider effort and model overrides"
|
||||
below) — plus `--add-dir <dir>` for each entry in `config["evidence_dirs"]`
|
||||
**only** for `research` and `topic_ideas`
|
||||
work types (the only ones that cite external evidence), plus `--allowedTools
|
||||
"<comma-joined list>"` if `config["work_types"][work_type]["allowed_tools"]` is
|
||||
present and non-empty (see Config schema below). Headless `claude -p` runs cannot
|
||||
answer permission prompts, so any tool beyond the CLI's defaults must be granted
|
||||
this way or the child run stalls/fails; when the key is absent or an empty list,
|
||||
the flag is omitted entirely (unchanged pre-existing behaviour).
|
||||
this way or the child run stalls/fails; when the `allowed_tools` key is absent or an
|
||||
empty list, that flag is omitted entirely (unchanged pre-existing behaviour). Same for
|
||||
`effort`/`model`: absent key -> no corresponding flag, argv unchanged from before
|
||||
these keys existed.
|
||||
5. Run the subprocess: `cwd=<repo>`, `env=<built env>`, prompt piped via **stdin**
|
||||
(not as an argv element — avoids `ARG_MAX` on large rendered prompts, same lesson
|
||||
`claude-profile` already applies to its system-prompt injection), timeout = per-task
|
||||
@@ -401,6 +408,87 @@ config validation failure — exit **2** at config load, same as a missing requi
|
||||
before writing its Subjective stories section; the topic-ideas template requires one as
|
||||
a lighter-touch check); `draft` and `review` carry no `allowed_tools` key.
|
||||
|
||||
### Per-work-type effort and model
|
||||
|
||||
Each entry in `work_types` may also carry two further optional keys, both consumed by
|
||||
§11 step 4 when building the child's argv:
|
||||
|
||||
- **`effort`** (string): appends `--effort <value>` to the child argv. Validated at
|
||||
config-load time against the fixed set of levels the `claude` CLI itself accepts:
|
||||
`low`, `medium`, `high`, `xhigh`, `max`. This set was determined empirically on
|
||||
2026-08-02 by running `claude --effort obviously-bogus-level -p ""` — an invalid
|
||||
`--effort` value is rejected by the CLI's own argument parsing before any network
|
||||
call is made, so the probe is safe to run offline — and reading the resulting error,
|
||||
which enumerated the valid values verbatim (cross-checked against `claude --help`'s
|
||||
`--effort <level>` description, which lists the same five). The set is hard-coded as
|
||||
`CLAUDE_EFFORT_LEVELS` in `scripts/idle-draft`; if a future CLI version changes it,
|
||||
re-run the same probe and update the constant (with a fresh date in the comment).
|
||||
A value that is not a non-empty string, or not a member of this set, is a config
|
||||
validation failure — exit **2**, same as a malformed `allowed_tools`.
|
||||
- **`model`** (string): appends `--model <value>` to the child argv, **replacing** —
|
||||
not adding to — the `--model <id>` that would otherwise come from the provider
|
||||
profile's `provider.env` `MODEL_ID` (§11 step 4). Only type-checked at config-load
|
||||
time (must be a non-empty string) — model identifiers are not enumerated, since new
|
||||
ones ship independently of this tool and hard-coding a set would go stale. A
|
||||
non-string or empty value is a config validation failure — exit **2**.
|
||||
|
||||
Both keys are optional and independent of each other and of `allowed_tools`. Absent on
|
||||
a given work type -> the corresponding flag is omitted / the profile's model is used
|
||||
unchanged, matching the tool's behaviour before these keys existed.
|
||||
|
||||
### Per-provider effort and model overrides (within a work type)
|
||||
|
||||
`model`/`effort` can be pinned even more narrowly than per-work-type: per
|
||||
`(work_type, provider)` pair. Each entry in a `work_types[*].providers` list may be
|
||||
**either**:
|
||||
|
||||
- a plain provider-name string (the original, unchanged form) — no per-provider
|
||||
override, resolution falls through to the work-type-level `model`/`effort` (above);
|
||||
or
|
||||
- an object `{"provider": "<name>", "model": "<optional>", "effort": "<optional>"}` —
|
||||
`model`/`effort` here apply only when *this* provider is the one actually selected
|
||||
(§9) for a dispatch of this work type.
|
||||
|
||||
Both forms may be mixed freely within the same `providers` list; list order still
|
||||
encodes provider preference exactly as before (§9) — the object form does not change
|
||||
*which* provider is tried first, only what argv that provider gets if chosen.
|
||||
`config.example.json` demonstrates this: `research`'s `providers` is
|
||||
`["anthropic", {"provider": "minimax", "effort": "medium"}]` — `anthropic` stays a
|
||||
plain string (falls through to `research`'s work-type-level `effort`), `minimax` pins
|
||||
its own `effort` via the object form.
|
||||
|
||||
**Full precedence chain**, evaluated once a `(work_type, provider)` pair has been
|
||||
selected (§9), for each of `model` and `effort` independently:
|
||||
|
||||
1. The per-provider override on the matching `providers`-list entry for that provider
|
||||
(object form only; a plain-string entry has none).
|
||||
2. The work-type-level `model`/`effort` (the keys directly under
|
||||
`work_types[work_type]`, previous section).
|
||||
3. Ultimate fallback: for `model`, the profile's `provider.env` `MODEL_ID` (or no
|
||||
`--model` flag if unset); for `effort`, no `--effort` flag (the CLI's own default).
|
||||
|
||||
**Validation** (config-load time, exit **2** on any failure, same style as every other
|
||||
config check):
|
||||
|
||||
- Every `providers`-list entry — string **or** object form — must name a provider that
|
||||
exists as a key in the top-level `providers` map. An entry (of either form) naming an
|
||||
unrecognised provider is rejected; this closes a gap that predates the object form —
|
||||
a typo'd plain-string provider name was previously silently ineligible (never
|
||||
selected, no error) rather than rejected at load time.
|
||||
- An object-form entry must have a `"provider"` key whose value is a non-empty string.
|
||||
- An object-form entry's keys are restricted to `provider`, `model`, `effort`; any other
|
||||
key is rejected.
|
||||
- An object-form entry's `model`, if present, must be a non-empty string (same rule as
|
||||
the work-type-level `model`).
|
||||
- An object-form entry's `effort`, if present, must be a non-empty string and a member
|
||||
of `CLAUDE_EFFORT_LEVELS` (same rule as the work-type-level `effort`).
|
||||
- A `providers`-list entry that is neither a string nor an object is rejected.
|
||||
|
||||
`--dryrun` prints the fully resolved argv (§11 step 4's `--model`/`--effort` included,
|
||||
after the full precedence chain above), so a preview reveals exactly which provider,
|
||||
model, and effort level a given work type would route to before anything is dispatched
|
||||
for real.
|
||||
|
||||
## Failure classes
|
||||
|
||||
| Class | Examples | `attempts` effect | Item outcome |
|
||||
@@ -438,6 +526,16 @@ a lighter-touch check); `draft` and `review` carry no `allowed_tools` key.
|
||||
| `--once` with no eligible work | Exits 0 immediately, no task dispatched |
|
||||
| Config missing a required key | Exit 2 |
|
||||
| `work_types[*].allowed_tools` present but not a list of strings | Exit 2 |
|
||||
| `work_types[*].effort` present but not one of `low`/`medium`/`high`/`xhigh`/`max` | Exit 2 |
|
||||
| `work_types[*].effort` present but empty string / not a string | Exit 2 |
|
||||
| `work_types[*].model` present but empty string / not a string | Exit 2 |
|
||||
| `work_types[*].model` set alongside a profile whose `provider.env` also sets `MODEL_ID` | Work-type `model` wins (absent a per-provider override); argv carries exactly one `--model` flag |
|
||||
| `work_types[*].providers[*]` (string or object form) names a provider not present in the top-level `providers` map | Exit 2 |
|
||||
| `work_types[*].providers[*]` object form missing/empty `"provider"` key | Exit 2 |
|
||||
| `work_types[*].providers[*]` object form has an unknown key (anything besides `provider`/`model`/`effort`) | Exit 2 |
|
||||
| `work_types[*].providers[*]` object form `model`/`effort` invalid (same rules as the work-type-level keys) | Exit 2 |
|
||||
| `work_types[*].providers[*]` entry is neither a string nor an object | Exit 2 |
|
||||
| A provider's `providers`-list entry sets its own `model`/`effort`, and the work type also sets a work-type-level `model`/`effort` | Per-provider entry wins for that provider; a sibling provider in the same list with a plain-string entry still falls through to the work-type-level value |
|
||||
| `agent-subscriptions` subprocess fails entirely | Exit 1, loud log line, no dispatch attempted |
|
||||
|
||||
## Examples
|
||||
|
||||
Reference in New Issue
Block a user