Requirements: per-work-item model and effort choice, answered at the phase-selection gate
Phase 1 of 3 (requirements → design → tasks). Following the Kiro spec approach (https://kiro.dev/docs/specs/). This phase MUST be reviewed and approved by the required collaborators before moving to design.
Revision 6 — a model name may optionally declare the harnesses that support it, which narrows it without being authoritative: a declaration may restrict, only the probe may confirm. Revision 5 made
modelsa plain list of provider-named models not tied to a harness, with the relation measured by the availability probe (R7) rather than declared. Revision 4 brought the spawn-order change in scope as R8. Revision 3 made the declarations three top-level sections (harnesses,models,effort),modelsis a flat list whose rows name a harness, and the effort vocabulary is the-loop's own, translated per harness by the adapter rather than spelled out by the operator (owner's second round on PR #359). Revision 2 split model from effort and added the availability requirement (R7). The spawn-order point is answered indesign.md§ Why a session exists before the gate, and why it should not and proposed as a prerequisite work item.
Introduction
Issue-358. Today the model a spawned session runs on is routing.harnessArgs.<harness> — one list, every session. An operator who wants five of eighteen work items on a different model has no supported path: the reporter typed /model fable into each tmux pane by hand, which the-loop cannot see, does not record, and loses on the next respawn.
The ticket asked for a label-to-args map. The owner disagreed on that mechanism: a label rides GitHub's permission model, not the-loop's, so anyone who can label an issue could choose what argv an unattended agent runs on. That is the same argument that put phase selection on a comment rather than a label in the first place (issue-177, selection.py), and it applies with more force here, because this input reaches an argv. The owner named the venue instead: the phase-selection lifecycle, where an authorized human already answers three per-work-item questions with one signed reply.
So these requirements keep the reporter's five properties and move the channel:
| Reporter asked for | Kept as |
|---|---|
| a configurable label-to-args map | a declared, closed set of models (models[]) and a normalized set of effort levels (effort[]), both top-level (R2) |
| merge, do not replace | R3 |
| applied at spawn and at respawn | R4 |
| visible in the registry | R5 |
| unlabelled items unchanged | R6 — a work item that chooses nothing runs exactly as today |
And they answer what the owner's two review rounds added: a model the harness does not accept is never offered and never reaches a spawn (R7); the declarations live at the top level, because which harnesses an instance has and what they can run is installed-tooling configuration rather than event routing; a model row names its harness instead of being nested under one; and the effort vocabulary is the-loop's, normalized across harnesses, with the translation to each harness's own mechanism owned by its adapter.
Requirements
Requirement 1 — an authorized human picks the model and the effort for one work item
User story: As an operator running eighteen work items from Slack on a phone, I want to say which model this item's session runs on, and how hard it should think, without editing config or restarting the daemon, so that a small item can run cheaply and a hard one can run on the strongest model at full effort.
Acceptance criteria (EARS)
- WHEN the
phase-selectionchecklist is posted for a work item whose harness has declared models THEN the system SHALL render one row per declared model, in the order the operator declared them, in a section of its own, distinct from the phase rows. - WHEN effort levels are declared THEN the system SHALL render them as a second, independent section, using the-loop's own level names — a work item chooses a model and an effort separately, and choosing one SHALL NOT require or imply the other.
- WHEN an authorized user replies with the execute keyword THEN the system SHALL freeze both choices by the same authorization and the same freeze that already record the phase skips, the outer-loop surface and
sessionPerPr. - IF a section has exactly one ticked row THEN the system SHALL record that choice for this work item.
- IF a section has no ticked row, or more than one THEN the system SHALL record no choice for that section and SHALL name the outcome in the gate's confirmation comment — the same fail-closed resolution
pr-sessions-*already uses, resolved per section, so an ambiguous model never discards a valid effort. - WHEN the gate confirms a selection THEN the confirmation comment SHALL name the model and the effort the work item will run on, including when either is the operator's default.
- IF no models are declared for the work item's harness THEN the system SHALL render no model section, and likewise for effort — each section appears only if it has something to offer.
- WHEN an effort level cannot be expressed by the work item's harness THEN the system SHALL NOT render that level for that work item, so a human is never offered a level that would resolve to nothing.
Requirement 2 — the choices are the operator's, declared and closed
User story: As the operator, I want the pickable models and effort levels to be lists I wrote in my own config, so that a comment can never introduce a model, a flag or an argument I did not declare.
Acceptance criteria (EARS)
- The system SHALL read the harnesses this instance has from a top-level
harnessessection, the pickable models from a top-levelmodelssection, and the offered effort levels from a top-leveleffortsection — besiderepositoriesandcritics, not underrouting, becauseroutingconfigures how an event reaches a session while these configure the machine's installed tooling. The system SHALL NOT infer, discover or fetch either list from a harness, a vendor API or the network. models[]SHALL be a list of model names in each provider's own naming convention. An entry MAY be a bare name, or a mapping carrying that name and an optionalharnesseslist. The system SHALL NOT infer a name's provider or harness from its spelling.- A declared
harnesseslist SHALL narrow where that model is a candidate — the model SHALL be offered only on those harnesses, and SHALL be probed only against them. It SHALL NOT be authoritative: only the availability probe of R7 SHALL make a name offerable on a harness, so a declaration SHALL NOT assert support a harness refuses. A bare name SHALL be a candidate for every declared harness. - IF a
harnessesentry names a harness that is not declared THEN that link SHALL contribute nothing and SHALL be named at validation, while the rest of the entry stands — a fault SHALL only ever shrink the offered set, never widen it. - The effort vocabulary SHALL be the-loop's own fixed enum, identical across harnesses, and
effort[]SHALL declare which of those levels this instance offers. The system SHALL NOT accept a per-harness effort flag from the operator's config. - WHEN a reply names a token that is not a declared choice for this work item's harness THEN the system SHALL ignore it and record no choice — no text from a comment SHALL ever reach an argv.
- The system SHALL resolve a model name to argv as its harness adapter's model flag followed by the name, verbatim. The system SHALL NOT accept operator-declared arguments on a model, and IF a harness has no model flag THEN no model section SHALL be offered for a work item on that harness — a harness that cannot be handed a model SHALL NOT be handed a hand-written flag.
- The system SHALL resolve an effort level to argv through its harness adapter (
effort_args(level)), never from the operator's config. IF an adapter cannot express a level THEN it SHALL return no arguments and the level SHALL NOT be offered for that harness (R1.8). - The system SHALL validate every effort mapping it asserts against the harness itself, by the same probe R7 applies to models — a mapping the-loop claims about a harness CLI SHALL be checked, never trusted.
- WHEN more choices are declared in a section than the checklist renders THEN the system SHALL render the first N and SHALL say how many were not shown, rather than truncating silently (the
CANDIDATE_LIMITconvention kickoff already uses). - IF no models and no effort levels are declared THEN the system SHALL behave exactly as it does today, everywhere.
- WHEN
harnesses[].argsis present for a harness THEN it SHALL be that harness's launch arguments; otherwiserouting.harnessArgs.<harness>SHALL be read with a deprecation warning — the warn-never-fail shimrouting.runner(issue-156) and the repository list (issue-348) already established. An un-migrated config SHALL NOT fail to start.
Requirement 3 — the choices are merged onto the operator's arguments, never a replacement
User story: As the operator, I want my global --dangerously-skip-permissions to keep applying to a session that chose a model, so that choosing a model is not also, silently, a permission change.
Acceptance criteria (EARS)
- WHEN a work item has a frozen choice THEN the effective arguments SHALL be the harness's launch arguments (R2.12), then the model's resolved arguments, then the effort level's resolved arguments — in that order, each contributing nothing when there is no choice.
- The system SHALL NOT remove, rewrite or reorder any argument the operator declared as that harness's launch arguments.
- The system SHALL NOT add a permission-widening argument that the operator did not declare, under any selection — a choice SHALL only ever contribute the arguments its declaration carries.
- WHEN the operator's global arguments already carry the same flag as a declared choice THEN config validation SHALL warn, naming both, because whether the appended flag wins is the harness's argument-parsing rule and not the-loop's to assert.
Requirement 4 — the choice survives the session
User story: As the operator, I want a work item to come back on the model I chose after a crash, a restart or a the-loop sessions reset, so that I never re-type it.
Acceptance criteria (EARS)
- WHEN a session is spawned for a work item with a frozen choice THEN the system SHALL launch the harness with the effective arguments of R3.1.
- WHEN a session is respawned (the recorded session was dead, or was reset) THEN the system SHALL re-resolve the effective arguments from the frozen record rather than from the arguments the previous session happened to be launched with.
- WHILE a session is running with arguments that differ from the currently resolved effective arguments, the system SHALL re-launch it — resuming its conversation — on the next event delivered to that work item, rather than delivering into a session running on the wrong model. (With R8 in place no ordinary path reaches this: the first spawn already carries the frozen choice. It still covers a choice changed after the gate — an operator re-freezing, a declaration withdrawn, a verdict turning
refused— and every session launched before this change, which carries no recorded arguments at all.) - WHEN such a re-launch happens THEN the system SHALL emit an event-log record naming the work item, the previous choice and the new one, so the change is auditable.
- IF the frozen record is missing, unreadable, or names a choice no longer declared THEN the system SHALL use
routing.harnessArgs.<harness>unchanged, and SHALL log that it did.
Requirement 5 — a human can see what a work item is running on
User story: As the operator, I want the-loop sessions list to tell me which model each work item is running, so that I can tell without attaching to a pane or reading /proc.
Acceptance criteria (EARS)
- WHEN a session is registered or re-registered THEN the system SHALL record on the session record the model and effort in force and the effective arguments the harness was launched with.
- WHEN
the-loop sessions listrenders its table THEN it SHALL show the model in force for each session, and-for a session that has none recorded. - The JSON form of a session (
--format json, the control-plane API and the SDK) SHALL carry the model, the effort and the effective arguments. - IF a session record was written before this change THEN it SHALL still parse, and SHALL render as
-rather than failing the listing.
Requirement 6 — a work item that chose nothing is unchanged
User story: As an operator who never wanted this feature, I want every work item to run exactly as it does today, so that upgrading costs me nothing.
Acceptance criteria (EARS)
- IF a work item has no frozen choice THEN its sessions SHALL be launched with
routing.harnessArgs.<harness>and nothing else. - IF the CLI config declares no models and no effort levels THEN the phase-selection checklist SHALL be byte-identical to the one the same work item gets today.
- The system SHALL NOT require any repository to create a label, and SHALL NOT read a label, to resolve a choice.
Requirement 7 — a model the harness will not accept is never offered and never spawned
User story: As the operator, I want a model my harness rejects — a typo, a model retired by the vendor, one my account has no access to — to be caught before a human picks it, so that a work item never dies in a pane nobody is watching.
Acceptance criteria (EARS)
- The system SHALL provide a command (
the-loop models check) that, for every declared model name and every offered effort level against every declared harness, runs that harness's own cheapest non-interactive invocation with the resolved arguments and records the verdict asok,refusedorunknown(the harness could not be run at all). The result SHALL be a matrix of name × harness, and SHALL be the only place the model-to-harness relation is established. - The system SHALL cache each verdict in the machine-local state, keyed by harness, id and the arguments probed, with a bounded lifetime, and SHALL refresh a verdict whose declaration has changed.
- WHEN the checklist is rendered THEN the system SHALL offer only choices whose cached verdict is
okorunknown, and SHALL NOT render one cached asrefused. - WHEN a choice cached as
refusedis nonetheless resolved — a frozen record older than the verdict, a hand-edited record — THEN the system SHALL launch the session onrouting.harnessArgs.<harness>unchanged, SHALL post one comment on the work item naming the refused choice and what it ran instead, and SHALL NOT retry that choice until its verdict changes. - WHEN a session whose arguments carried a non-default choice is found dead with no conversation to resume THEN the system SHALL re-probe that choice once before respawning, so a verdict that went stale becomes
refusedrather than an unbounded respawn loop. - IF a verdict is
unknownTHEN the system SHALL treat the choice as offerable and SHALL NOT block a spawn on it — an unprobeable harness (no binary on this machine, no network) SHALL NOT remove a capability the operator declared. - WHEN
the-loop diagnoseruns THEN it SHALL report each declared choice and its cached verdict, so an operator can see what their config actually buys them.
Requirement 8 — the session is spawned after the gate is answered, not before
User story: As the operator, I want a work item to get its harness session only once I have said what it should do, so that the first session already runs on the model I chose and no tmux session sits waiting at a gate.
In scope on the owner's instruction ("implement in this same PR"). It is what makes R4.3's re-launch a safety net rather than an ordinary path.
Acceptance criteria (EARS)
- WHEN a work item is armed and the graph's pointer is placed on a start node that is a human gate THEN the system SHALL enter the graph, let that node's hooks post their comment, and SHALL NOT spawn a session.
- WHEN that gate is answered by an authorized user and the pointer advances past it THEN the system SHALL spawn the work item's session, with the effective arguments of R3.1 resolved from the record the gate has just frozen.
- The system SHALL defer a spawn only while the pointer has never advanced past the graph's start node AND that start node is a human gate. A work item whose pointer has moved SHALL always be able to spawn and respawn.
- WHEN a spawn is deferred THEN the system SHALL emit an event-log record naming the work item and the node it is parked on, and
the-loop checkSHALL report it as waiting there. - The arming event SHALL still be handed to that first gate (issue-199), so a
the-loop contributecomment's goal still arrives with the command that arms the item. - IF graph linkage is disabled, the work item has no spec-id convention, the checkout does not belong to the work item, or the spec directory resolves outside the checkout THEN the system SHALL spawn exactly as it does today — every existing skip path SHALL mean "nothing is deferred".
- The workspace SHALL still be prepared before the graph is entered, because the pointer is written under the checkout's spec directory. Deferral SHALL save the harness session, not the checkout.
- WHILE a work item's spawn is deferred, the system SHALL NOT report it as having a session —
sessions listSHALL not invent one.
Non-functional requirements
- No new I/O on the dispatch path. The frozen choice is read from the portable record the dispatcher already reads for
sessionPerPr(_tmux_for), and availability is read from a cache — resolving a choice SHALL add no network call and no harness invocation per event. Probing happens inmodels check, at daemon start, and on the one stale-verdict path of R7.5; never inline in a delivery. - Observability. The resolved model and effort appear in the spawn/respawn event-log records that already name the harness, so an operator can answer "what did this session actually run on" from
the-loop eventsalone. - Documentation. The two choices are described where the other per-work-item questions are: the configuration reference, the interactive-sessions capability doc, and the phase-selection section of the operating model.
Security considerations
This work item creates one new path from comment text to an argv of an unattended agent. That is the whole of its risk, and every criterion below exists to keep the path a lookup into operator-owned configuration rather than a passthrough.
- Actors & trust:
- Untrusted: anyone who can comment on, or edit a comment on, a work item; anyone who can add or remove a label; any webhook payload.
- Semi-trusted:
routing.authorizedUsers— named humans who may direct the-loop. - Trusted: the operator's
cli-config.yaml, which is executable configuration on the machine that runs the daemon and is not editable by a pull request to a repository (the posturecritics[]took in decision-123).
- Trust boundaries & data: the boundary is the gate's parse. A reply yields, at most, a token matched against the declared set; the argv is then built from the operator's own declaration. No sensitive data is stored or moved: a model id is not a secret, and nothing here reads or writes credentials.
- The probe is an egress, and it is bounded. R7 runs the harness non-interactively. It SHALL use a fixed, the-loop-authored prompt — never any text from a work item — and SHALL run only the declared arguments. It is the one new outbound action in this work item, it is not on the delivery path, and it carries no repository content.
- Why not a label (the rejected mechanism): GitHub's label permission is
triageon the repository and is not the-loop'sauthorizedUsersboundary. Accepting a label here would let anyone with triage rights choose the argv of an unattended agent — and would do it silently, because GitHub does not attribute a label to a person in the payload the-loop already authorizes on. The label channel is therefore refused, not merely unsupported. - Abuse cases (EARS):
- WHEN an unauthorized user ticks a row on the checklist and asks for execution THEN the system SHALL ignore the reply entirely, exactly as it does for phase ticks today.
- WHEN an authorized user's reply names
--dangerously-skip-permissions, a shell metacharacter, a path, or any string that is not a declared id THEN the system SHALL treat it as no choice and run the operator's arguments unchanged. - WHEN a declared entry's own
argswould widen permissions THEN the system SHALL apply them as declared and SHALL NOT be considered at fault — the operator wrote them, in the same file that already carriesharnessArgs— and the-loop SHALL NOT add such an argument on its own initiative in any other circumstance. - WHEN the portable record for a work item has been hand-edited to name an undeclared choice, or to carry an
argslist of its own THEN the system SHALL re-validate against the declared set on read and SHALL fall back to the operator's arguments (the portable tree is agent-writable; it is re-validated on the way in, as_tmux_foralready re-validatessessionPerPr). - WHEN a label named after a model is present on the work item THEN the system SHALL ignore it.
- WHEN the gate cannot read its own checklist comment (an outage, a deleted comment) THEN the system SHALL resolve to the operator's arguments unchanged.
- WHEN the availability cache file is hand-edited to mark an undeclared choice
okTHEN the system SHALL still resolve only against the declared set, so a forged verdict can withhold a choice but SHALL NOT introduce one.
- Fail closed: every ambiguity — no tick, several ticks, an unknown token, an unreadable record, a
refusedverdict, a missing declaration — resolves torouting.harnessArgs.<harness>unchanged. The closed direction here is the operator's own configuration, never the narrowest or cheapest model: a malformed reply must not silently downgrade a tier-5 work item.
Out of scope
- Folding
routing.harnessTrust,routing.harnessPluginsandrouting.defaultHarnessintoharnesses[]. They belong there by the same argument that moved these three sections out ofrouting, but it is a breaking config migration with its own tests. This work item takes only what it needs —name,defaultandargs— and leaves the rest to a follow-up. - A model choice for standing sessions. They already have per-entry
harnessArgs(issue-277) and are not work items. - Changing an already-running session's model in place. No harness exposes that; R4.3 re-launches instead.
- Model choice for the inner (per-PR) loops independently of their work item. A PR's session inherits its work item's choice.
- A control keyword (
the-loop model: …). One surface for a per-work-item decision, and the gate is it. - Enumerating what a harness can run. R7 validates what the operator declared; it does not discover a catalogue.
Open questions
Is the prerequisite ordering ticket wanted?Answered: the owner asked for it in this PR. It is R8.- What lifetime should an availability verdict have? Proposed: 24 hours, plus invalidation whenever the declaration's arguments change, plus the one re-probe of R7.5.
IsAnswered from both harnesses' own pickers:low | medium | highthe right effort enum?low | medium | high | xhigh | max | ultra— the union of what Claude Code and Codex express. Two rows carry the argument:xhighis one concept with two spellings (Claude'sxhigh, Codex's "Extra high"), andultrais Codex's alone, so it is offered only where it exists. Neither name was guessed.- Does the
harnesses[]consolidation follow-up get raised now?
Review comments
2026-09-13 — @MadaraUchiha-314, review of PR #359
| Feedback | Disposition |
|---|---|
| "handle the error condition when the user chooses a model that's not available in the harness" | R7, new: probe, cache, do not offer a refused choice, fall back visibly, re-probe once on a dead session, report in diagnose. |
| "Should we let the user specify the models per harness or overall for all harnesses?" | First answered per harness; superseded twice. Revision 5 made it one flat list, because R7's probe measures the name × harness relation rather than the-loop attributing it; revision 6 added an optional harnesses list that narrows but cannot confirm (R2.2–R2.4). |
| "this couples model and effort … keep model and effort as 2 separate inputs" | Accepted — two declarations (models[], effort[] — top-level since revision 3), two checklist sections, two frozen keys, resolved independently (R1.2, R1.5, R2.1, R3.1). No cross product. |
"just models" (on the harnessModels vs models question) | Accepted — models, with effort beside it. |
| "Why do we start a session before the phase selection is complete? … We ideally shouldn't" | Agreed. Answered in design.md § Why a session exists before the gate, and why it should not; proposed as a prerequisite work item, with R4.3 keeping this work item correct under either ordering. |
2026-09-13 — @MadaraUchiha-314, second round on PR #359
| Feedback | Disposition |
|---|---|
| "separate section for harnesses, a separate section for models, a separate list for efforts" | Accepted — three top-level sections (R2.1). Revision 5 then dropped the per-row harness entirely; revision 6 reintroduced it as an optional, non-authoritative harnesses list (R2.3). |
| "the-loop should take care of normalizing" the effort enums | Accepted — the enum is the-loop's, the translation is adapter.effort_args(level), an inexpressible level is not offered (R1.8, R2.5, R2.8), and every mapping the-loop asserts is probe-validated (R2.9) rather than invented. |
| "putting it under routing key doesn't make any sense to me" | Agreed — routing configures event delivery; these configure installed tooling. harnesses[].args takes over from routing.harnessArgs.<harness> behind a warn-never-fail shim (R2.12); the other three per-harness keys are a named follow-up, out of scope here. |
| "Let's not tie model to harness … just keep it models … use whatever naming convention each of the model providers follow" | Accepted, and it retires my objection rather than working around it (R2.2–R2.4, R2.7, R7.1). I had argued twice that an untied list forces the-loop to attribute a name to a harness; R7's probe measures the name × harness matrix instead, so the relation is observed, resolution reads one cell, and a name the harness cannot run is already refused. Names stay the provider's, passed verbatim — the deliberate opposite of effort, where the-loop owns the vocabulary. |
| "for each model, we can link it to supported harnesses" | Accepted as an optional, non-authoritative link (R2.3, R2.4): harnesses: on a name restricts where it is offered and shrinks the probe matrix to match, and the probe still decides what is offerable — so a declaration can narrow but never assert support a harness refuses. |
| "implement in this same PR" (the spawn-order change) | Accepted, now in scope as R8. The graph is entered when the work item is armed; the spawn is deferred while the pointer is parked on a daemon-serviced start gate; the deferral rule is narrow enough that nothing mid-graph can be stranded. One earlier claim is struck: the checkout is still needed at arm time, so deferral saves the harness session, not the clone. |