diff --git a/.flow/memory/bug/integration/dispatch-settings-must-reach-the-2026-10-17.md b/.flow/memory/bug/integration/dispatch-settings-must-reach-the-2026-10-17.md new file mode 100644 index 000000000..827a4ccc0 --- /dev/null +++ b/.flow/memory/bug/integration/dispatch-settings-must-reach-the-2026-10-17.md @@ -0,0 +1,25 @@ +--- +title: "Dispatch settings must reach the bridged implementer, not its native wrapper" +date: "2026-10-17" +track: bug +category: integration +module: plugins/flow-next/scripts/flowctl.py +tags: [judge, tier, effort, bridge, dispatch] +problem_type: integration +symptoms: Tier line claims an effort the bridged implementer never received +root_cause: effort routed to native spawn; routing-block bridge resolved later in the worker +resolution_type: fix +related_to: [bug/integration/a-new-terminal-status-needs-every-2026-10-05, bug/integration/bundled-snapshot-pr-listing-truncation-2026-09-26, bug/integration/review-backend-adapters-move-2026-10-05] +--- + +## Problem +A dispatch setting computed by the conductor (worker reasoning effort) was routed to the native worker spawn whenever no explicit implementer was passed. When the project routing block named a bridged implementer, worker Phase 1b launched the bridge without that setting, so the native wrapper ran at the chosen effort and the real implementer did not, while the Tier line claimed it was applied. A role-pinned effort also short-circuited the bridged case and dropped the requested effort. + +## What Didn't Work +Deciding "bridged" only from the conductor's own inputs (explicit model or mechanical fast model): the routing-block implementer is resolved later, inside the worker, and the conductor never passes it to the judge. + +## Solution +A role pin applies only to the native spawn (`if role_effort and not bridged` in `judge_tier_effort`, flowctl.py); bridged implementers carry the effort in ` at `. worker-bridge.md takes the effort from IMPLEMENTER, else the routing block, else the level the TIER_LINE applied. + +## Prevention +For any per-dispatch setting, trace it to the process that actually does the work on every reach path (native spawn, explicit bridge, routing-block bridge resolved by the worker), and add a test case per path that asserts where the value lands. diff --git a/.flow/specs/fn-299-worker-reasoning-effort-follows-the.json b/.flow/specs/fn-299-worker-reasoning-effort-follows-the.json index 7d9716864..e6d70aa8d 100644 --- a/.flow/specs/fn-299-worker-reasoning-effort-follows-the.json +++ b/.flow/specs/fn-299-worker-reasoning-effort-follows-the.json @@ -8,14 +8,223 @@ "default_sync": null, "depends_on_epics": [], "id": "fn-299-worker-reasoning-effort-follows-the", - "impl_review_rounds": {}, + "impl_review_rounds": { + "fn-299-worker-reasoning-effort-follows-the.1": 0 + }, "next_task": 1, + "no_plan": true, "plan_review_rounds": 0, "plan_review_status": "unknown", "plan_reviewed_at": null, "ready": true, + "review_attempts": [ + { + "artifact_sha256": "31e6758d83181b940195c00c43552351277ec04aaee076841cc27355c15f1500", + "backend": "codex", + "base_sha": "04a03c6d3685e3ee6bafe56cd9a9dbb576ebe4f8", + "counter_kind": "impl", + "effort": "high", + "failure_class": null, + "finalized": { + "digest": "complete", + "receipt": "complete", + "status": "not_applicable" + }, + "findings_digest": { + "backend": "codex", + "digest_truncated": false, + "items": [ + { + "chainRoot": "finding-6926e2a591c6a11b85400007e910addb", + "classification": "introduced", + "findingId": "finding-6926e2a591c6a11b85400007e910addb", + "firstSeenThisRound": true, + "severity": "P1", + "status": "open" + }, + { + "chainRoot": "finding-f704db02254192a87db666974a02833a", + "classification": "introduced", + "findingId": "finding-f704db02254192a87db666974a02833a", + "firstSeenThisRound": true, + "severity": "P2", + "status": "open" + }, + { + "chainRoot": "finding-b4d5cfe847157efd77f423ab616fd53c", + "classification": "introduced", + "findingId": "finding-b4d5cfe847157efd77f423ab616fd53c", + "firstSeenThisRound": true, + "severity": "P2", + "status": "open" + } + ], + "reviewKind": "implementation" + }, + "forced": false, + "hash_epoch": 0, + "head_sha": "bb9f1a8673f13ef6f8be7295e8609de76270a967", + "head_sha_observed": true, + "kind": "impl", + "model": "gpt-6-astra", + "outcome": "verdict", + "output_bytes": 2590, + "output_sha256": "c6559bcb29f935ca6746c853e81f0de0cd1dc925457c2bb624ca9dc00cf05316", + "reservation_id": "2918e9c11af24129b6acadfadad77f51", + "round_consumed": true, + "scope": "impl:fn-299-worker-reasoning-effort-follows-the.1", + "superseded_by": null, + "task": "fn-299-worker-reasoning-effort-follows-the.1", + "timestamp": "2026-10-16T20:37:16.791397Z", + "verdict": "NEEDS_WORK" + }, + { + "artifact_sha256": "24f32dad5523cff45ced96609eb4488d8dfe571f2775d76602203959a31e2363", + "backend": "codex", + "base_sha": "04a03c6d3685e3ee6bafe56cd9a9dbb576ebe4f8", + "counter_kind": "impl", + "effort": "high", + "failure_class": null, + "finalized": { + "digest": "complete", + "receipt": "complete", + "status": "not_applicable" + }, + "findings_digest": { + "backend": "codex", + "digest_truncated": false, + "items": [ + { + "chainRoot": "finding-6926e2a591c6a11b85400007e910addb", + "classification": "introduced", + "findingId": "finding-6926e2a591c6a11b85400007e910addb", + "firstSeenThisRound": false, + "severity": "P1", + "status": "fixed" + }, + { + "chainRoot": "finding-f704db02254192a87db666974a02833a", + "classification": "introduced", + "findingId": "finding-f704db02254192a87db666974a02833a", + "firstSeenThisRound": false, + "severity": "P2", + "status": "fixed" + }, + { + "chainRoot": "finding-b4d5cfe847157efd77f423ab616fd53c", + "classification": "introduced", + "findingId": "finding-b4d5cfe847157efd77f423ab616fd53c", + "firstSeenThisRound": false, + "severity": "P2", + "status": "not_fixed" + }, + { + "chainRoot": "finding-5571b6b18f4eec7e3e0464b298ce32a9", + "classification": "introduced", + "findingId": "finding-5571b6b18f4eec7e3e0464b298ce32a9", + "firstSeenThisRound": true, + "severity": "P2", + "status": "open" + } + ], + "reviewKind": "implementation" + }, + "forced": false, + "hash_epoch": 0, + "head_sha": "3fca663b35a42f7de00fb1af06019858a40efc01", + "head_sha_observed": true, + "kind": "impl", + "model": "gpt-6-astra", + "outcome": "verdict", + "output_bytes": 1938, + "output_sha256": "f1a79bb49d0c9bf1d7375706996e99df3d3cb02b06f090357c5d72153d0dbafa", + "reservation_id": "151748d85c9e4e2b987750b3d69878e2", + "round_consumed": true, + "scope": "impl:fn-299-worker-reasoning-effort-follows-the.1", + "superseded_by": null, + "task": "fn-299-worker-reasoning-effort-follows-the.1", + "timestamp": "2026-10-16T22:01:10.791794Z", + "verdict": "NEEDS_WORK" + }, + { + "artifact_sha256": "15695b6b22e0b8fe3db3a6ab621bf0fb2aaa6e6aa48d275061a43247f178723a", + "backend": "codex", + "base_sha": "04a03c6d3685e3ee6bafe56cd9a9dbb576ebe4f8", + "counter_kind": "impl", + "effort": "high", + "failure_class": null, + "finalized": { + "digest": "complete", + "receipt": "complete", + "status": "not_applicable" + }, + "findings_digest": { + "backend": "codex", + "digest_truncated": false, + "items": [ + { + "chainRoot": "finding-6926e2a591c6a11b85400007e910addb", + "classification": "introduced", + "findingId": "finding-6926e2a591c6a11b85400007e910addb", + "firstSeenThisRound": false, + "severity": "P1", + "status": "fixed" + }, + { + "chainRoot": "finding-f704db02254192a87db666974a02833a", + "classification": "introduced", + "findingId": "finding-f704db02254192a87db666974a02833a", + "firstSeenThisRound": false, + "severity": "P2", + "status": "fixed" + }, + { + "chainRoot": "finding-b4d5cfe847157efd77f423ab616fd53c", + "classification": "introduced", + "findingId": "finding-b4d5cfe847157efd77f423ab616fd53c", + "firstSeenThisRound": false, + "severity": "P2", + "status": "fixed" + }, + { + "chainRoot": "finding-5571b6b18f4eec7e3e0464b298ce32a9", + "classification": "introduced", + "findingId": "finding-5571b6b18f4eec7e3e0464b298ce32a9", + "firstSeenThisRound": false, + "severity": "P2", + "status": "fixed" + } + ], + "reviewKind": "implementation" + }, + "forced": false, + "hash_epoch": 0, + "head_sha": "8f3b80b770ae9e34e09e9ba47bd358f904132fad", + "head_sha_observed": true, + "kind": "impl", + "model": "gpt-6-astra", + "outcome": "verdict", + "output_bytes": 1768, + "output_sha256": "4b49fd07a50ddaa6268e52064cb231febaf6d94658b7db01efefacd89227dc80", + "reservation_id": "9d8cda3e86f14b908774c9fdf4a5b415", + "round_consumed": true, + "scope": "impl:fn-299-worker-reasoning-effort-follows-the.1", + "superseded_by": null, + "task": "fn-299-worker-reasoning-effort-follows-the.1", + "timestamp": "2026-10-17T01:15:57.439025Z", + "verdict": "SHIP" + } + ], + "review_hash_epoch": { + "impl:fn-299-worker-reasoning-effort-follows-the.1": 1 + }, + "review_pending_rounds": {}, + "review_reservations": {}, + "review_transport_failures": { + "impl:fn-299-worker-reasoning-effort-follows-the.1": 0 + }, "spec_path": ".flow/specs/fn-299-worker-reasoning-effort-follows-the.md", - "status": "open", + "status": "done", "title": "Worker reasoning effort follows the dispatch tier", "tracker": { "baseHashFlow": null, @@ -28,5 +237,5 @@ "mergeBaseTracker": null, "url": null }, - "updated_at": "2026-10-15T09:52:38.487878Z" + "updated_at": "2026-10-17T01:20:12.328932Z" } diff --git a/.flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.json b/.flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.json new file mode 100644 index 000000000..9f17e7f82 --- /dev/null +++ b/.flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.json @@ -0,0 +1,15 @@ +{ + "assignee": null, + "claim_note": "", + "claimed_at": null, + "created_at": "2026-10-16T20:25:47.344205Z", + "depends_on": [], + "id": "fn-299-worker-reasoning-effort-follows-the.1", + "implicit_owner": true, + "priority": null, + "spec": "fn-299-worker-reasoning-effort-follows-the", + "spec_path": ".flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.md", + "status": "done", + "title": "Implement Worker reasoning effort follows the dispatch tier", + "updated_at": "2026-10-16T20:25:47.344205Z" +} diff --git a/.flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.md b/.flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.md new file mode 100644 index 000000000..f2cae557a --- /dev/null +++ b/.flow/tasks/fn-299-worker-reasoning-effort-follows-the.1.md @@ -0,0 +1,31 @@ +--- +satisfies: [R1, R2, R3, R4, R5, R6] +--- +# fn-299-worker-reasoning-effort-follows-the.1 Implement Worker reasoning effort follows the dispatch tier + +## Description +TBD + +## Acceptance +Every R-ID in the parent spec's ## Acceptance Criteria is satisfied; judge this task against the spec's criteria directly. + +## Done summary +Worker reasoning effort now follows the dispatch tier: `flowctl judge --preset tier --task` takes the host's effort levels and default (`--effort-levels`, `--default-effort`) plus `--explicit-effort` / `--role-effort`, returns `spawn_effort` (one level below the default for a confident mechanical answer, one above for a confident intelligent one, the default otherwise and without a judge), carries it as ` at ` for a bridged implementer, and names the effort applied or why not on the `Tier:` line; scouts are unchanged. The judge-tier recipe, multi-task 3c (and its Codex SECTION3C copy), worker-bridge, judge/flowctl docs, the Claude Code and Codex reach pages, and the changelog state the behaviour. + +Tests: `test_judge_consumers.py` covers R1 bounds and floors, R2 precedence (requested, role pin, explicit model keeps tier effort), R3 no-judge default, R4 not-applied lines and bridge transport, R5 line text, and the CLI path without a key. + +Decisions: the 0.8 confidence floor that gates the model tier gates effort in both directions (an intelligent answer below 0.8 keeps the default). A requested effort covers the invocation and, below it, the routing block's implementer line; the conductor resolves which, per routing precedence. A role-pinned effort applies only to a native spawn. A rejected level is retried once without effort (conductor prose); flowctl cannot observe the rejection. + +OPEN_ITEM: R6 evidence is partial. Worker replay (claude-opus-5-5, 3 judge-confident mechanical fixtures x 5 reps per arm): pass rate 15/15 at low vs 15/15 at medium; mechanical medians 55.0/43.0/130.9 s vs 76.1/67.2/138.0 s, but the pre-registered paired verdict is INCONCLUSIVE (pooled gap 10.0 s against a 9.7 s floor, within-cell spread 29-88 s). End-to-end large-feature control (3 draws each, same time): review rounds median 2 vs 2, wall median 1,886 vs 1,992 s, all hidden checks pass, but null by construction: no draw spawned a worker or made a tier call. Only about 1% of historical tasks are judge-confident mechanical and 2% intelligent. Whether to accept this or run a multi-task `--auto` study on Claude Code 2.1.296+ is the maintainer's call. + +Follow-up: the end-to-end harness pins Claude Code 2.1.283, whose Agent spawn has no effort parameter; an opt-in `FN271_CLAUDE_DIR` override was added there, uncommitted. + +stage: impl-review - ran (codex, 3 rounds: NEEDS_WORK -> NEEDS_WORK (R6 only) -> SHIP) + +Tier: session (jev intelligent 0.63) (model: claude-opus-5-5) + +stage: plan-sync - skipped(config: planSync.enabled != true) +## Evidence +- Commits: bb9f1a8673f13ef6f8be7295e8609de76270a967, 3fca663b35a42f7de00fb1af06019858a40efc01, 8f3b80b770ae9e34e09e9ba47bd358f904132fad, bdbafacbe99588d3c98038ac17dc61b49597969f +- Tests: python3 -m unittest plugins/flow-next/tests/test_judge_consumers.py, python3 -m unittest plugins/flow-next/tests/test_work_reached_path_routes.py plugins/flow-next/tests/test_tracker_caller_execution.py plugins/flow-next/tests/test_review_convergence_journal.py plugins/flow-next/tests/test_startup_bootstrap.py, ./scripts/sync-codex.sh --check, uvx ruff@0.16.0 check plugins/flow-next/scripts/flowctl.py plugins/flow-next/tests/test_judge_consumers.py +- PRs: \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c7a289e..bd0adb804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Flow-Next changed shape with 5.0.0. One command, `/flow-next:flow`, reads whatev ### Changed +- **Workers think less on routine tasks and more on hard ones.** The tier call that already runs before each worker spawn now sets the worker's reasoning effort as well as its model: a confidently mechanical task runs one level below the host's default effort, a confidently intelligent one a level above, and everything else at the default. An effort you name in the invocation or the routing block, or one the worker role pins, still wins, and naming only a model keeps the tier's effort. Without a TypeSafe key every worker runs at the default, as before. The effort reaches the worker through the host's spawn effort parameter (Claude Code, Codex) or a bridged implementer's effort setting; elsewhere the worker runs as before. The `Tier:` line, kept in each task's done summary, names the effort applied or why it was not. - **Defect fixes remove the fault, not its symptom, and prove it where it was reported.** Before designing a fix, the defect route now decides whether the state that triggered the crash may legitimately occur; when it should never exist, the fix goes where that state was produced instead of a guard, retry or swallowed error where it surfaced. A failure that appears only after a restart suspects persisted state (config, caches, locks, serialized records) before code. Base and head observations run through the surface the report came from (the command, the request, the call or the app), so a unit test of the changed branch is supporting evidence, not the proof. - **A measured number says what bounds it and what else it could be measuring.** When flow speeds something up once, climbs a metric, or measures a prompt change, the number now passes a short set of questions before anyone acts on it: what bounds it (from a profile or system counters, not a guess from the code), what else it could be measuring (fast errors, a warm cache on one side, work that never ran, an untuned side), whether the gain is possible given that piece's share of the run, and whether the work happened. Each side runs at least five times, alternated, and the claim reads `faster`, `slower`, `no measurable difference` or `inconclusive`, with the run count, range and limiter. The hill-climb harness prints error and work counts next to the metric and rejects a faster number with less work done, proves its case reproduces the complaint against easier workloads, and tries the cheapest ideas first (do not do the work, do not do it again, do less of it) before concurrency or rewrites. A prompt eval's verdict carries how many draws on each side finished. Other routes load nothing new. diff --git a/plugins/flow-next/codex/docs/flow-next/flowctl.md b/plugins/flow-next/codex/docs/flow-next/flowctl.md index fe74adf45..6ac5c9e61 100644 --- a/plugins/flow-next/codex/docs/flow-next/flowctl.md +++ b/plugins/flow-next/codex/docs/flow-next/flowctl.md @@ -1236,7 +1236,12 @@ over 32k tokens at four characters per token is rejected without sending. The command never writes state, answers, or credentials to disk. `--task` assembles tier inputs from the task and returns `tier_line`, -`spawn_model` and `implementer`. Unknown tasks fail. +`spawn_model`, `spawn_effort` and `implementer`. Unknown tasks fail. For a worker +the caller passes the host facts `--effort-levels ` and +`--default-effort `, plus `--explicit-effort` or `--role-effort` when one +applies; `spawn_effort` is the level for the spawn's effort parameter, or null +for the host default. A bridged implementer carries the effort as +` at ` in `implementer`. `--spec` assembles route facts from the live spec and repository. A failed PR probe returns `pr_probe_failed: true` and no `decision`, so the caller diff --git a/plugins/flow-next/codex/docs/flow-next/judge.md b/plugins/flow-next/codex/docs/flow-next/judge.md index f5ad36169..7f9b855bb 100644 --- a/plugins/flow-next/codex/docs/flow-next/judge.md +++ b/plugins/flow-next/codex/docs/flow-next/judge.md @@ -85,7 +85,7 @@ default path and names the reason, `jev-unavailable()`. | `route` | None; code only, never sent | The live spec's lifecycle, decided in code (see [Decision order](#decision-order)). Intake routing is the host's. | | `fork-gate` | Fork-kind Choice on the host's fork sentence | `observable` or `product_or_preference` at confidence >= 0.5 is a hint; otherwise `host`. Never `none`. | | `memory-rerank` | One Score per BM25 hit, up to 15 | Reorder by score, descending; ties keep BM25 order; none dropped. | -| `tier` | Tier Choice and two Nouls | `mechanical` at confidence >= 0.8 selects the configured fast tier; `long_running` at >= 0.8 recommends a bridge. All other answers retain the current model. | +| `tier` | Tier Choice and two Nouls | `mechanical` at confidence >= 0.8 selects the configured fast tier; `long_running` at >= 0.8 recommends a bridge. All other answers retain the current model. A worker's effort goes one level below the host default for `mechanical` and one above for `intelligent`, both at >= 0.8; every other answer keeps the default. | No preset predicts whether review, QA, or landing will pass, and none decides whether QA runs. @@ -186,6 +186,17 @@ unreachable. A mechanical decision changes the spawn-model parameter as well as the prompt's `IMPLEMENTER:` field; a missing fast-scout model, or a host with neither model steering nor a suitable bridge, leaves the model unchanged and says so. A long-running decision is a recommendation only. + +A worker's reasoning effort follows the same answer, one level on the host's own +scale and never past its lowest or highest level: below the default for a +confident mechanical task, above it for a confident intelligent one, the default +for everything else and whenever the judge is unavailable. An effort the +invocation or routing block names, or one the worker role pins, wins; an explicit +model without an effort still takes the tier's effort. The effort reaches the +worker through the host's spawn effort parameter or, for a bridged implementer, +the bridge's effort setting. Where neither exists, or the spawn rejects the level +and the one retry at the default runs, the `Tier:` line says effort was not +applied and why. Scouts keep their own effort. Done summaries read the model that actually ran from the worker's return, or say `(model: not reported)`. @@ -220,10 +231,12 @@ fork-gate: observable (host) fork-gate: preference (host, jev hint product_or_preference 0.71) memory: reranked (jev, 15 entries) memory: bm25 (jev-unavailable(no_key)) -Tier: mechanical (jev 0.88) -> -Tier: long_running (jev 0.86) - bridge recommended -Tier: session (jev moderate 0.61) -Tier: implementer - (explicit invocation) +Tier: long_running (jev 0.86) - bridge recommended at default effort +Tier: session (jev moderate 0.61) at default effort +Tier: session (jev intelligent 0.84) at high effort (host default medium) +Tier: mechanical (jev 0.91) -> at low effort (host default medium) +Tier: implementer - (explicit invocation) at xhigh effort (requested) +Tier: mechanical (jev 0.88) -> ; effort not applied (host spawn takes no effort) ``` ## Evaluation numbers of record diff --git a/plugins/flow-next/codex/docs/flow-next/reach/claude-code.md b/plugins/flow-next/codex/docs/flow-next/reach/claude-code.md index af8ed7740..95421c9a7 100644 --- a/plugins/flow-next/codex/docs/flow-next/reach/claude-code.md +++ b/plugins/flow-next/codex/docs/flow-next/reach/claude-code.md @@ -10,7 +10,7 @@ How this harness obtains a model for a [tier](../orchestration.md#tiers-what-kin | Mechanism | Here | |---|---| | In-session model | **Yes** - chosen in the harness; it is the default executor for every unset tier. | -| In-host subagent | **Yes** - subagents are spawned in-host, and an agent definition's model field is honored, so a tier can execute on a different model without leaving the session. | +| In-host subagent | **Yes** - subagents are spawned in-host, and an agent definition's model field is honored, so a tier can execute on a different model without leaving the session. On current builds the spawn also takes an `effort` (`low`, `medium`, `high`, `xhigh`, `max`, lowest first; observed on 2.1.296, absent on 2.1.283, so check the spawn's parameters before passing effort facts), which overrides an agent definition's `effort` field; a level the model lacks runs as the highest level it supports below it. A spawn without one runs at the session's effort: `--effort` or settings, else the model's default (documented 2026-10-16). | | Shell out to another CLI | **Yes** - the harness runs shell commands, so any CLI installed and authenticated on the machine is reachable, in either direction. | ## What is unavailable diff --git a/plugins/flow-next/codex/docs/flow-next/reach/codex.md b/plugins/flow-next/codex/docs/flow-next/reach/codex.md index 28f456755..99e3500d4 100644 --- a/plugins/flow-next/codex/docs/flow-next/reach/codex.md +++ b/plugins/flow-next/codex/docs/flow-next/reach/codex.md @@ -10,7 +10,7 @@ How this harness obtains a model for a [tier](../orchestration.md#tiers-what-kin | Mechanism | Here | |---|---| | In-session model | **Yes** - chosen in the harness; the default executor for every unset tier. | -| In-host subagent | **Yes.** Model and effort steering on `spawn_agent` works on both paths since codex-cli 0.146.0 (measured from the child thread's rollout `turn_context`, never from the child's self-report). Precedence: a role's declared `model` / `model_reasoning_effort` wins over an explicit spawn parameter; where the role declares no model, the explicit parameter applies; with no `agent_type`, the explicit parameter applies and `reasoning_effort` may be set alone; the explicit `model` parameter accepts a shorter list than a role can pin. Two dispatch gotchas: `agent_type` takes the role's `name` (hyphenated), not the `[agents.]` table key; `agent_type` requires `fork_turns: "none"`. A role's `sandbox_mode` is not enforced - a child inherits its parent's sandbox in both directions, so read-only is prompt-only here and containment comes from the parent launch flag (`codex -s read-only`). There is no in-band receipt of the effective model; pin, record what ran, and re-probe out of band. | +| In-host subagent | **Yes.** Model and effort steering on `spawn_agent` works on both paths since codex-cli 0.146.0 (measured from the child thread's rollout `turn_context`, never from the child's self-report). Precedence: a role's declared `model` / `model_reasoning_effort` wins over an explicit spawn parameter; where the role declares no model, the explicit parameter applies; with no `agent_type`, the explicit parameter applies and `reasoning_effort` may be set alone; the explicit `model` parameter accepts a shorter list than a role can pin. Two dispatch gotchas: `agent_type` takes the role's `name` (hyphenated), not the `[agents.]` table key; `agent_type` requires `fork_turns: "none"`. A role's `sandbox_mode` is not enforced - a child inherits its parent's sandbox in both directions, so read-only is prompt-only here and containment comes from the parent launch flag (`codex -s read-only`). There is no in-band receipt of the effective model; pin, record what ran, and re-probe out of band. `reasoning_effort` takes the model's own levels (ask the CLI for them); a spawn without one runs at the session's `model_reasoning_effort`. | | Shell out to another CLI | **Yes.** A fresh non-interactive run of a CLI takes its model and effort on the command line, so nothing can strip them. It works with this harness's own CLI (same family, different model) and with another vendor's. One condition: the parent sandbox must allow spawning a process and reaching the network. Watch on the child's own fan-out: openai/codex#33267, a `codex exec` parent unable to decode the result of a child that spawned subagents, is open upstream and reported on codex-cli 0.144 to 0.145 with the gpt-5.6 family; its minimal repro runs clean on codex-cli 0.153.4 with gpt-6-astra. A bridged child on a current build may fan out; on a build in the reported range, keep the child prompt flat. | ## What is unavailable diff --git a/plugins/flow-next/codex/skills/flow-next-work/references/judge-tier.md b/plugins/flow-next/codex/skills/flow-next-work/references/judge-tier.md index 8c451ff63..83014499a 100644 --- a/plugins/flow-next/codex/skills/flow-next-work/references/judge-tier.md +++ b/plugins/flow-next/codex/skills/flow-next-work/references/judge-tier.md @@ -1,9 +1,9 @@ # Judge at dispatch -Before each worker spawn, call `$FLOWCTL judge --preset tier --task --json` once. Pass the explicit invocation model with `--explicit-model`, the routing block's fast model with `--fast-model`, and the role's pinned model with `--role-model` when present. Pass `--can-spawn-model` / `--can-bridge` only for host-verified reach. Scouts still use `--state-file` for their actual bounded assignment (`task_title`, `task_body`, `acceptance`, `touches_count`, `has_quick_commands`, `repo`), with the same reach flags. Keep this result for the admission; never reroute a running worker or repeat the rolling decision at Phase 3c. +Before each worker spawn, call `$FLOWCTL judge --preset tier --task --json` once. Pass the explicit invocation model with `--explicit-model`, the routing block's fast model with `--fast-model`, and the role's pinned model with `--role-model` when present. Pass `--can-spawn-model` / `--can-bridge` only for host-verified reach. For a worker, also pass the effort facts from this host's reach page: `--effort-levels` (the levels the worker's spawn parameter accepts, lowest first; when the implementer goes over a bridge, including a routing-block implementer the worker will bridge, the bridge's levels) and `--default-effort` (the level a spawn without one runs at), `--explicit-effort` for an effort the invocation names (else one the routing block's implementer line names), and `--role-effort` when the worker role pins one, never for a routing-block implementer the worker will bridge (a role pin configures the in-host spawn, never a bridged child). Omit the levels when the spawn takes no effort parameter. Scouts still use `--state-file` for their actual bounded assignment (`task_title`, `task_body`, `acceptance`, `touches_count`, `has_quick_commands`, `repo`), with the same reach flags. Keep this result for the admission; never reroute a running worker or repeat the rolling decision at Phase 3c. -Use the returned `tier_line`, `spawn_model`, and `implementer`. Set `spawn_model_args` to `{"model": result["spawn_model"]}` when non-null, otherwise `{}`. An explicit invocation model becomes `spawn_model` when the host can spawn it, goes over the bridge otherwise, and the `Tier:` line names it either way, with or without a judge key; pinned roles keep their model; without an explicit model, unavailable judge answers keep the session tier. +Use the returned `tier_line`, `spawn_model`, `spawn_effort`, and `implementer`. Set `spawn_model_args` to `{"model": result["spawn_model"]}` when non-null, otherwise `{}`, and add `"effort": result["spawn_effort"]` when that is non-null. The tier sets the effort: one level below the default for a confident mechanical answer, one above for a confident intelligent one, the default otherwise and without a judge answer; a requested or role-pinned effort wins, and an explicit model without an effort still takes the tier's effort. An explicit invocation model becomes `spawn_model` when the host can spawn it, goes over the bridge otherwise, and the `Tier:` line names it either way, with or without a judge key; pinned roles keep their model; without an explicit model, unavailable judge answers keep the session tier. -Expand `spawn_model_args` into the Task invocation so the selected model reaches its **`model` parameter**; on another host map that field to its equivalent spawn-model parameter. Also pass `IMPLEMENTER: ` in the worker prompt when set. The prompt alone never selects a native model. For bridge-only reach, leave the native spawn unchanged and let worker Phase 1b resolve the `IMPLEMENTER` bridge. Scouts use the same selection with the host's read-only restrictions preserved and `role_model` set from the scout's role definition: a pinned role keeps its model and the `Tier:` line names it, since a spawn parameter would not change what runs; on bridge-only reach without a scout bridge, retain the existing scout model and state that limitation. +Expand `spawn_model_args` into the Task invocation so the selected model reaches its **`model` parameter** and the effort its **`effort` parameter**; on another host map each field to its equivalent spawn parameter. If the spawn rejects the effort, spawn once more without it and append `; effort rejected, ran at default effort` to the `Tier:` line; never try a third level. A bridged implementer's effort arrives inside `implementer` as ` at `. Also pass `IMPLEMENTER: ` in the worker prompt when set. The prompt alone never selects a native model. For bridge-only reach, leave the native spawn unchanged and let worker Phase 1b resolve the `IMPLEMENTER` bridge. Scouts use the same selection with the host's read-only restrictions preserved and `role_model` set from the scout's role definition: a pinned role keeps its model and the `Tier:` line names it, since a spawn parameter would not change what runs; on bridge-only reach without a scout bridge, retain the existing scout model and state that limitation. Print `tier_line` at dispatch and retain it in the task's done summary (or scout findings). After return append `(model: )` only when the returned model is backed by host execution metadata or the worker's actual bridge command. Without that evidence append `(model: not reported)`, so a run on another model than requested stays visible; never copy the requested model into the actual-model field. A long-running recommendation is advisory and creates no bridge or approval requirement. diff --git a/plugins/flow-next/codex/skills/flow-next-work/references/multi-task.md b/plugins/flow-next/codex/skills/flow-next-work/references/multi-task.md index d9816bb8b..00f6b4743 100644 --- a/plugins/flow-next/codex/skills/flow-next-work/references/multi-task.md +++ b/plugins/flow-next/codex/skills/flow-next-work/references/multi-task.md @@ -168,7 +168,7 @@ When the sentinel prints, read [references/tracker-touchpoints.md](tracker-touch Implementation is the **implementer** tier: absent any preference, the worker runs on the session model. **Routing precedence, highest first: an explicit argument in the invocation, then the project routing block in the instruction file, then the agent definition's own default, then the session model.** How this harness reaches a non-session model — and what the degradation is when it cannot — lives in its reach page (`plugins/flow-next/docs/reach/`), never here. -Before spawning, apply [judge-tier.md](judge-tier.md) once for this task. Use its selected model in the host spawn-model parameter as well as the `IMPLEMENTER:` line; an explicit invocation always wins. +Before spawning, apply [judge-tier.md](judge-tier.md) once for this task. Use its selected model and effort in the host spawn parameters as well as the `IMPLEMENTER:` line; an explicit invocation always wins. **When the implementer tier resolves to a model this harness reaches only over a CLI bridge, the worker bridges and the conductor never does.** The dispatch below is unchanged: the worker resolves the tier itself (worker Phase 1b), hands the task to the bridged child with the usage guide's brief, and reviews the child's commit range before its own review dispatch. The bridged child owns the task and its own delegation; a conductor that composed a brief, ran a bridge call, or fanned out on the implementer's behalf has broken this. @@ -239,7 +239,7 @@ point — the worker's return, the host's own tool timeout or error, or a lost result — where 3d's side-effects rule classifies whatever the lane left behind. -`IMPLEMENTER` carries an explicit invocation model first; otherwise it may carry the confident mechanical tier's reachable fast-scout model from `references/judge-tier.md`. Omit it when neither applies. Pass the native model through the spawn-model parameter too. The conductor never bridges or composes the worker's brief. +`IMPLEMENTER` carries an explicit invocation model first; otherwise it may carry the confident mechanical tier's reachable fast-scout model from `references/judge-tier.md`. Omit it when neither applies; when the implementer goes over a bridge, it carries the effort as ` at `. Pass the native model and effort through the spawn parameters too. The conductor never bridges or composes the worker's brief. `BASELINE_HANDOFF` is optional. The conductor MAY pass it only when ALL hold: the prior task in this run reached done with its Phase 5 Verify green over the SAME Quick commands, HEAD has not moved since except by commits changing only `.flow/` paths, and the new task's declared Touches do not intersect files changed since that verification. Conductor judgment on stated facts; when in doubt, omit the line. On the wave route the first task receives no handoff. The rolling route instead runs a green spec-base baseline before its first admission and may hand that baseline to the first batch under the same `.flow/`-only rule. Check every intervening commit with `git log --format= --name-only ..HEAD`; any non-`.flow/` path invalidates the handoff, including changes later reverted. diff --git a/plugins/flow-next/codex/skills/flow-next-work/references/worker-bridge.md b/plugins/flow-next/codex/skills/flow-next-work/references/worker-bridge.md index 31648c5ab..c6b529e52 100644 --- a/plugins/flow-next/codex/skills/flow-next-work/references/worker-bridge.md +++ b/plugins/flow-next/codex/skills/flow-next-work/references/worker-bridge.md @@ -25,7 +25,7 @@ Everything below in this phase, its Done-when included, binds only the bridged b **On return:** 1. Commit any dirty remainder with the standard staging (`git add -- .flow/ && git commit -m "(): "`) — this is the sandbox-denied-commit case the brief tells the child to report; a dirty tree never reaches range review. -2. Record `stage: implement - ran (model: ; delegated: )` for Phase 5. `model:` is the model and effort you passed on the bridge command line — the command line is the record (nothing can strip it), and a child's self-report of its model is not evidence (reach page). `delegated:` is the number of subagents the child's digest reports; a digest without a count records `delegated: unknown`. +2. Record `stage: implement - ran (model: ; delegated: )` for Phase 5. `model:` is the model and effort you passed on the bridge command line (the effort from the `IMPLEMENTER` line's `at `, else the routing block's, else the level your `TIER_LINE` applied; a bridge that rejects the level runs once more at its default and the line says `effort rejected, ran at default effort`; a bridge CLI with no effort setting runs without one and the line says `effort not applied (bridge takes no effort)`) — the command line is the record (nothing can strip it), and a child's self-report of its model is not evidence (reach page). `delegated:` is the number of subagents the child's digest reports; a digest without a count records `delegated: unknown`. 3. Review `$(cat .flow/tmp/base_commit)..HEAD` against every acceptance criterion the task names and against Phase 2's rules (no weakened tests or gates, no rename drift, no scope past the ACs, every enumerated error case tested), on a defect task against defect-route.md's steps and record, and on a hill-climb task against hill-climb.md's verify step. A gap against an AC is yours to close under Phase 2's rules before Phase 3; this is the worker's range check, not a review verdict — Phase 4 still owns that. 4. Run the focused Quick commands for the code under change, then continue at Phase 3 (the child's checkpoints stay; your Phase 3 commit covers what you added). diff --git a/plugins/flow-next/docs/flowctl.md b/plugins/flow-next/docs/flowctl.md index 47aa00b1c..d09e0bd86 100644 --- a/plugins/flow-next/docs/flowctl.md +++ b/plugins/flow-next/docs/flowctl.md @@ -1233,7 +1233,12 @@ over 32k tokens at four characters per token is rejected without sending. The command never writes state, answers, or credentials to disk. `--task` assembles tier inputs from the task and returns `tier_line`, -`spawn_model` and `implementer`. Unknown tasks fail. +`spawn_model`, `spawn_effort` and `implementer`. Unknown tasks fail. For a worker +the caller passes the host facts `--effort-levels ` and +`--default-effort `, plus `--explicit-effort` or `--role-effort` when one +applies; `spawn_effort` is the level for the spawn's effort parameter, or null +for the host default. A bridged implementer carries the effort as +` at ` in `implementer`. `--spec` assembles route facts from the live spec and repository. A failed PR probe returns `pr_probe_failed: true` and no `decision`, so the caller diff --git a/plugins/flow-next/docs/judge.md b/plugins/flow-next/docs/judge.md index 83774c524..b15fc847e 100644 --- a/plugins/flow-next/docs/judge.md +++ b/plugins/flow-next/docs/judge.md @@ -82,7 +82,7 @@ default path and names the reason, `jev-unavailable()`. | `route` | None; code only, never sent | The live spec's lifecycle, decided in code (see [Decision order](#decision-order)). Intake routing is the host's. | | `fork-gate` | Fork-kind Choice on the host's fork sentence | `observable` or `product_or_preference` at confidence >= 0.5 is a hint; otherwise `host`. Never `none`. | | `memory-rerank` | One Score per BM25 hit, up to 15 | Reorder by score, descending; ties keep BM25 order; none dropped. | -| `tier` | Tier Choice and two Nouls | `mechanical` at confidence >= 0.8 selects the configured fast tier; `long_running` at >= 0.8 recommends a bridge. All other answers retain the current model. | +| `tier` | Tier Choice and two Nouls | `mechanical` at confidence >= 0.8 selects the configured fast tier; `long_running` at >= 0.8 recommends a bridge. All other answers retain the current model. A worker's effort goes one level below the host default for `mechanical` and one above for `intelligent`, both at >= 0.8; every other answer keeps the default. | No preset predicts whether review, QA, or landing will pass, and none decides whether QA runs. @@ -183,6 +183,17 @@ unreachable. A mechanical decision changes the spawn-model parameter as well as the prompt's `IMPLEMENTER:` field; a missing fast-scout model, or a host with neither model steering nor a suitable bridge, leaves the model unchanged and says so. A long-running decision is a recommendation only. + +A worker's reasoning effort follows the same answer, one level on the host's own +scale and never past its lowest or highest level: below the default for a +confident mechanical task, above it for a confident intelligent one, the default +for everything else and whenever the judge is unavailable. An effort the +invocation or routing block names, or one the worker role pins, wins; an explicit +model without an effort still takes the tier's effort. The effort reaches the +worker through the host's spawn effort parameter or, for a bridged implementer, +the bridge's effort setting. Where neither exists, or the spawn rejects the level +and the one retry at the default runs, the `Tier:` line says effort was not +applied and why. Scouts keep their own effort. Done summaries read the model that actually ran from the worker's return, or say `(model: not reported)`. @@ -217,10 +228,12 @@ fork-gate: observable (host) fork-gate: preference (host, jev hint product_or_preference 0.71) memory: reranked (jev, 15 entries) memory: bm25 (jev-unavailable(no_key)) -Tier: mechanical (jev 0.88) -> -Tier: long_running (jev 0.86) - bridge recommended -Tier: session (jev moderate 0.61) -Tier: implementer - (explicit invocation) +Tier: long_running (jev 0.86) - bridge recommended at default effort +Tier: session (jev moderate 0.61) at default effort +Tier: session (jev intelligent 0.84) at high effort (host default medium) +Tier: mechanical (jev 0.91) -> at low effort (host default medium) +Tier: implementer - (explicit invocation) at xhigh effort (requested) +Tier: mechanical (jev 0.88) -> ; effort not applied (host spawn takes no effort) ``` ## Evaluation numbers of record diff --git a/plugins/flow-next/docs/reach/claude-code.md b/plugins/flow-next/docs/reach/claude-code.md index 73254e6fa..b1354b22d 100644 --- a/plugins/flow-next/docs/reach/claude-code.md +++ b/plugins/flow-next/docs/reach/claude-code.md @@ -7,7 +7,7 @@ How this harness obtains a model for a [tier](../orchestration.md#tiers-what-kin | Mechanism | Here | |---|---| | In-session model | **Yes** - chosen in the harness; it is the default executor for every unset tier. | -| In-host subagent | **Yes** - subagents are spawned in-host, and an agent definition's model field is honored, so a tier can execute on a different model without leaving the session. | +| In-host subagent | **Yes** - subagents are spawned in-host, and an agent definition's model field is honored, so a tier can execute on a different model without leaving the session. On current builds the spawn also takes an `effort` (`low`, `medium`, `high`, `xhigh`, `max`, lowest first; observed on 2.1.296, absent on 2.1.283, so check the spawn's parameters before passing effort facts), which overrides an agent definition's `effort` field; a level the model lacks runs as the highest level it supports below it. A spawn without one runs at the session's effort: `--effort` or settings, else the model's default (documented 2026-10-16). | | Shell out to another CLI | **Yes** - the harness runs shell commands, so any CLI installed and authenticated on the machine is reachable, in either direction. | ## What is unavailable diff --git a/plugins/flow-next/docs/reach/codex.md b/plugins/flow-next/docs/reach/codex.md index 3315e9f22..66442d4d1 100644 --- a/plugins/flow-next/docs/reach/codex.md +++ b/plugins/flow-next/docs/reach/codex.md @@ -7,7 +7,7 @@ How this harness obtains a model for a [tier](../orchestration.md#tiers-what-kin | Mechanism | Here | |---|---| | In-session model | **Yes** - chosen in the harness; the default executor for every unset tier. | -| In-host subagent | **Yes.** Model and effort steering on `spawn_agent` works on both paths since codex-cli 0.146.0 (measured from the child thread's rollout `turn_context`, never from the child's self-report). Precedence: a role's declared `model` / `model_reasoning_effort` wins over an explicit spawn parameter; where the role declares no model, the explicit parameter applies; with no `agent_type`, the explicit parameter applies and `reasoning_effort` may be set alone; the explicit `model` parameter accepts a shorter list than a role can pin. Two dispatch gotchas: `agent_type` takes the role's `name` (hyphenated), not the `[agents.]` table key; `agent_type` requires `fork_turns: "none"`. A role's `sandbox_mode` is not enforced - a child inherits its parent's sandbox in both directions, so read-only is prompt-only here and containment comes from the parent launch flag (`codex -s read-only`). There is no in-band receipt of the effective model; pin, record what ran, and re-probe out of band. | +| In-host subagent | **Yes.** Model and effort steering on `spawn_agent` works on both paths since codex-cli 0.146.0 (measured from the child thread's rollout `turn_context`, never from the child's self-report). Precedence: a role's declared `model` / `model_reasoning_effort` wins over an explicit spawn parameter; where the role declares no model, the explicit parameter applies; with no `agent_type`, the explicit parameter applies and `reasoning_effort` may be set alone; the explicit `model` parameter accepts a shorter list than a role can pin. Two dispatch gotchas: `agent_type` takes the role's `name` (hyphenated), not the `[agents.]` table key; `agent_type` requires `fork_turns: "none"`. A role's `sandbox_mode` is not enforced - a child inherits its parent's sandbox in both directions, so read-only is prompt-only here and containment comes from the parent launch flag (`codex -s read-only`). There is no in-band receipt of the effective model; pin, record what ran, and re-probe out of band. `reasoning_effort` takes the model's own levels (ask the CLI for them); a spawn without one runs at the session's `model_reasoning_effort`. | | Shell out to another CLI | **Yes.** A fresh non-interactive run of a CLI takes its model and effort on the command line, so nothing can strip them. It works with this harness's own CLI (same family, different model) and with another vendor's. One condition: the parent sandbox must allow spawning a process and reaching the network. Watch on the child's own fan-out: openai/codex#33267, a `codex exec` parent unable to decode the result of a child that spawned subagents, is open upstream and reported on codex-cli 0.144 to 0.145 with the gpt-5.6 family; its minimal repro runs clean on codex-cli 0.153.4 with gpt-6-astra. A bridged child on a current build may fan out; on a build in the reported range, keep the child prompt flat. | ## What is unavailable diff --git a/plugins/flow-next/scripts/flowctl.py b/plugins/flow-next/scripts/flowctl.py index d12c0034f..cd8b439ec 100755 --- a/plugins/flow-next/scripts/flowctl.py +++ b/plugins/flow-next/scripts/flowctl.py @@ -24407,7 +24407,43 @@ def judge_tier_dispatch(result: dict, args: argparse.Namespace) -> dict: line = f"Tier: long_running (jev {confidence:.2f}) - bridge recommended" else: line = f"Tier: session (jev {answer['choice']} {confidence:.2f})" - return {"tier_line": line, "spawn_model": selected_model, "implementer": implementer} + bridged = bool(implementer) and not selected_model and can_bridge + effort, suffix = judge_tier_effort(result, args, bridged) + spawn_effort = None + if effort and bridged: + implementer = f"{implementer} at {effort}" + elif effort: + spawn_effort = effort + return {"tier_line": line + suffix, "spawn_model": selected_model, "implementer": implementer, + "spawn_effort": spawn_effort} + + +def judge_tier_effort(result: dict, args: argparse.Namespace, bridged: bool) -> tuple: + """Worker effort from the tier answer, one step on the host's ladder; requested and role efforts win.""" + if not getattr(args, "task", None): + return None, "" # Scouts keep their own effort selection. + levels = [level.strip() for level in (getattr(args, "effort_levels", None) or "").split(",") if level.strip()] + default = getattr(args, "default_effort", None) + requested = getattr(args, "explicit_effort", None) + role_effort = getattr(args, "role_effort", None) + if role_effort and not bridged: # A role pin configures the native spawn, never a bridged child. + return None, f" at {role_effort} effort (role pins effort)" + if requested: + if not levels and not bridged: + return None, f"; effort {requested} not applied (host spawn takes no effort)" + return requested, f" at {requested} effort (requested)" + answer = result["answers"]["tier"] if result.get("available") else None + step = {"mechanical": -1, "intelligent": 1}.get(answer["choice"], 0) if answer and answer["confidence"] >= 0.8 else 0 + if not step: + return None, " at default effort" + if not levels: + return None, "; effort not applied (host spawn takes no effort)" + if default not in levels: + return None, "; effort not applied (host default effort unknown)" + level = levels[min(max(levels.index(default) + step, 0), len(levels) - 1)] + if level == default: + return None, " at default effort" + return level, f" at {level} effort (host default {default})" def cmd_judge(args: argparse.Namespace) -> None: @@ -55443,6 +55479,10 @@ def main() -> None: p_judge.add_argument("--role-model", help="Model pinned by the dispatched role") p_judge.add_argument("--can-spawn-model", action="store_true", help="Host supports native model selection") p_judge.add_argument("--can-bridge", action="store_true", help="Host verified bridge reach") + p_judge.add_argument("--effort-levels", help="Worker spawn's effort levels, lowest first, comma-separated") + p_judge.add_argument("--default-effort", help="Effort a worker spawn without one runs at") + p_judge.add_argument("--explicit-effort", help="Effort the invocation or routing block names") + p_judge.add_argument("--role-effort", help="Effort pinned by the dispatched role") p_judge.add_argument("--json", action="store_true", help="JSON output") p_judge.set_defaults(func=cmd_judge) diff --git a/plugins/flow-next/scripts/flowctl_tracker/MANIFEST.json b/plugins/flow-next/scripts/flowctl_tracker/MANIFEST.json index 78f57b890..ae742b111 100644 --- a/plugins/flow-next/scripts/flowctl_tracker/MANIFEST.json +++ b/plugins/flow-next/scripts/flowctl_tracker/MANIFEST.json @@ -2,7 +2,7 @@ "files": [ { "path": "flowctl.py", - "sha256": "8baf2a816b6933e84fbe56f9fee00062851344273149a08eecc6f704ef279973" + "sha256": "5723d1c2fe852028219eda356ad15d379825829fc49dae28f2520b889247da76" }, { "path": "flowctl_tracker/__init__.py", diff --git a/plugins/flow-next/skills/flow-next-work/references/judge-tier.md b/plugins/flow-next/skills/flow-next-work/references/judge-tier.md index 8c451ff63..83014499a 100644 --- a/plugins/flow-next/skills/flow-next-work/references/judge-tier.md +++ b/plugins/flow-next/skills/flow-next-work/references/judge-tier.md @@ -1,9 +1,9 @@ # Judge at dispatch -Before each worker spawn, call `$FLOWCTL judge --preset tier --task --json` once. Pass the explicit invocation model with `--explicit-model`, the routing block's fast model with `--fast-model`, and the role's pinned model with `--role-model` when present. Pass `--can-spawn-model` / `--can-bridge` only for host-verified reach. Scouts still use `--state-file` for their actual bounded assignment (`task_title`, `task_body`, `acceptance`, `touches_count`, `has_quick_commands`, `repo`), with the same reach flags. Keep this result for the admission; never reroute a running worker or repeat the rolling decision at Phase 3c. +Before each worker spawn, call `$FLOWCTL judge --preset tier --task --json` once. Pass the explicit invocation model with `--explicit-model`, the routing block's fast model with `--fast-model`, and the role's pinned model with `--role-model` when present. Pass `--can-spawn-model` / `--can-bridge` only for host-verified reach. For a worker, also pass the effort facts from this host's reach page: `--effort-levels` (the levels the worker's spawn parameter accepts, lowest first; when the implementer goes over a bridge, including a routing-block implementer the worker will bridge, the bridge's levels) and `--default-effort` (the level a spawn without one runs at), `--explicit-effort` for an effort the invocation names (else one the routing block's implementer line names), and `--role-effort` when the worker role pins one, never for a routing-block implementer the worker will bridge (a role pin configures the in-host spawn, never a bridged child). Omit the levels when the spawn takes no effort parameter. Scouts still use `--state-file` for their actual bounded assignment (`task_title`, `task_body`, `acceptance`, `touches_count`, `has_quick_commands`, `repo`), with the same reach flags. Keep this result for the admission; never reroute a running worker or repeat the rolling decision at Phase 3c. -Use the returned `tier_line`, `spawn_model`, and `implementer`. Set `spawn_model_args` to `{"model": result["spawn_model"]}` when non-null, otherwise `{}`. An explicit invocation model becomes `spawn_model` when the host can spawn it, goes over the bridge otherwise, and the `Tier:` line names it either way, with or without a judge key; pinned roles keep their model; without an explicit model, unavailable judge answers keep the session tier. +Use the returned `tier_line`, `spawn_model`, `spawn_effort`, and `implementer`. Set `spawn_model_args` to `{"model": result["spawn_model"]}` when non-null, otherwise `{}`, and add `"effort": result["spawn_effort"]` when that is non-null. The tier sets the effort: one level below the default for a confident mechanical answer, one above for a confident intelligent one, the default otherwise and without a judge answer; a requested or role-pinned effort wins, and an explicit model without an effort still takes the tier's effort. An explicit invocation model becomes `spawn_model` when the host can spawn it, goes over the bridge otherwise, and the `Tier:` line names it either way, with or without a judge key; pinned roles keep their model; without an explicit model, unavailable judge answers keep the session tier. -Expand `spawn_model_args` into the Task invocation so the selected model reaches its **`model` parameter**; on another host map that field to its equivalent spawn-model parameter. Also pass `IMPLEMENTER: ` in the worker prompt when set. The prompt alone never selects a native model. For bridge-only reach, leave the native spawn unchanged and let worker Phase 1b resolve the `IMPLEMENTER` bridge. Scouts use the same selection with the host's read-only restrictions preserved and `role_model` set from the scout's role definition: a pinned role keeps its model and the `Tier:` line names it, since a spawn parameter would not change what runs; on bridge-only reach without a scout bridge, retain the existing scout model and state that limitation. +Expand `spawn_model_args` into the Task invocation so the selected model reaches its **`model` parameter** and the effort its **`effort` parameter**; on another host map each field to its equivalent spawn parameter. If the spawn rejects the effort, spawn once more without it and append `; effort rejected, ran at default effort` to the `Tier:` line; never try a third level. A bridged implementer's effort arrives inside `implementer` as ` at `. Also pass `IMPLEMENTER: ` in the worker prompt when set. The prompt alone never selects a native model. For bridge-only reach, leave the native spawn unchanged and let worker Phase 1b resolve the `IMPLEMENTER` bridge. Scouts use the same selection with the host's read-only restrictions preserved and `role_model` set from the scout's role definition: a pinned role keeps its model and the `Tier:` line names it, since a spawn parameter would not change what runs; on bridge-only reach without a scout bridge, retain the existing scout model and state that limitation. Print `tier_line` at dispatch and retain it in the task's done summary (or scout findings). After return append `(model: )` only when the returned model is backed by host execution metadata or the worker's actual bridge command. Without that evidence append `(model: not reported)`, so a run on another model than requested stays visible; never copy the requested model into the actual-model field. A long-running recommendation is advisory and creates no bridge or approval requirement. diff --git a/plugins/flow-next/skills/flow-next-work/references/multi-task.md b/plugins/flow-next/skills/flow-next-work/references/multi-task.md index 08793afe5..a2d7819c6 100644 --- a/plugins/flow-next/skills/flow-next-work/references/multi-task.md +++ b/plugins/flow-next/skills/flow-next-work/references/multi-task.md @@ -168,7 +168,7 @@ When the sentinel prints, read [references/tracker-touchpoints.md](tracker-touch Implementation is the **implementer** tier: absent any preference, the worker runs on the session model. **Routing precedence, highest first: an explicit argument in the invocation, then the project routing block in the instruction file, then the agent definition's own default, then the session model.** How this harness reaches a non-session model — and what the degradation is when it cannot — lives in its reach page (`plugins/flow-next/docs/reach/`), never here. -Before spawning, apply [references/judge-tier.md](judge-tier.md) once for this task. Use its selected model in the host spawn-model parameter as well as the `IMPLEMENTER:` line; an explicit invocation always wins. +Before spawning, apply [references/judge-tier.md](judge-tier.md) once for this task. Use its selected model and effort in the host spawn parameters as well as the `IMPLEMENTER:` line; an explicit invocation always wins. **When the implementer tier resolves to a model this harness reaches only over a CLI bridge, the worker bridges and the conductor never does.** The dispatch below is unchanged: the worker resolves the tier itself (worker Phase 1b), hands the task to the bridged child with the usage guide's brief, and reviews the child's commit range before its own review dispatch. The bridged child owns the task and its own delegation; a conductor that composed a brief, ran a bridge call, or fanned out on the implementer's behalf has broken this. @@ -250,7 +250,7 @@ point — the worker's return, the host's own tool timeout or error, or a lost result — where 3d's side-effects rule classifies whatever the lane left behind. -`IMPLEMENTER` carries an explicit invocation model first; otherwise it may carry the confident mechanical tier's reachable fast-scout model from `references/judge-tier.md`. Omit it when neither applies. Pass the native model through the spawn-model parameter too. The conductor never bridges or composes the worker's brief. +`IMPLEMENTER` carries an explicit invocation model first; otherwise it may carry the confident mechanical tier's reachable fast-scout model from `references/judge-tier.md`. Omit it when neither applies; when the implementer goes over a bridge, it carries the effort as ` at `. Pass the native model and effort through the spawn parameters too. The conductor never bridges or composes the worker's brief. `BASELINE_HANDOFF` is optional. The conductor MAY pass it only when ALL hold: the prior task in this run reached done with its Phase 5 Verify green over the SAME Quick commands, HEAD has not moved since except by commits changing only `.flow/` paths, and the new task's declared Touches do not intersect files changed since that verification. Conductor judgment on stated facts; when in doubt, omit the line. On the wave route the first task receives no handoff. The rolling route instead runs a green spec-base baseline before its first admission and may hand that baseline to the first batch under the same `.flow/`-only rule. Check every intervening commit with `git log --format= --name-only ..HEAD`; any non-`.flow/` path invalidates the handoff, including changes later reverted. diff --git a/plugins/flow-next/skills/flow-next-work/references/worker-bridge.md b/plugins/flow-next/skills/flow-next-work/references/worker-bridge.md index 31648c5ab..c6b529e52 100644 --- a/plugins/flow-next/skills/flow-next-work/references/worker-bridge.md +++ b/plugins/flow-next/skills/flow-next-work/references/worker-bridge.md @@ -25,7 +25,7 @@ Everything below in this phase, its Done-when included, binds only the bridged b **On return:** 1. Commit any dirty remainder with the standard staging (`git add -- .flow/ && git commit -m "(): "`) — this is the sandbox-denied-commit case the brief tells the child to report; a dirty tree never reaches range review. -2. Record `stage: implement - ran (model: ; delegated: )` for Phase 5. `model:` is the model and effort you passed on the bridge command line — the command line is the record (nothing can strip it), and a child's self-report of its model is not evidence (reach page). `delegated:` is the number of subagents the child's digest reports; a digest without a count records `delegated: unknown`. +2. Record `stage: implement - ran (model: ; delegated: )` for Phase 5. `model:` is the model and effort you passed on the bridge command line (the effort from the `IMPLEMENTER` line's `at `, else the routing block's, else the level your `TIER_LINE` applied; a bridge that rejects the level runs once more at its default and the line says `effort rejected, ran at default effort`; a bridge CLI with no effort setting runs without one and the line says `effort not applied (bridge takes no effort)`) — the command line is the record (nothing can strip it), and a child's self-report of its model is not evidence (reach page). `delegated:` is the number of subagents the child's digest reports; a digest without a count records `delegated: unknown`. 3. Review `$(cat .flow/tmp/base_commit)..HEAD` against every acceptance criterion the task names and against Phase 2's rules (no weakened tests or gates, no rename drift, no scope past the ACs, every enumerated error case tested), on a defect task against defect-route.md's steps and record, and on a hill-climb task against hill-climb.md's verify step. A gap against an AC is yours to close under Phase 2's rules before Phase 3; this is the worker's range check, not a review verdict — Phase 4 still owns that. 4. Run the focused Quick commands for the code under change, then continue at Phase 3 (the child's checkpoints stay; your Phase 3 commit covers what you added). diff --git a/plugins/flow-next/tests/test_judge_consumers.py b/plugins/flow-next/tests/test_judge_consumers.py index 95429fe0c..a427f08de 100644 --- a/plugins/flow-next/tests/test_judge_consumers.py +++ b/plugins/flow-next/tests/test_judge_consumers.py @@ -192,5 +192,65 @@ def test_tier_preserves_unreachable_and_unavailable(self): self.assertIn("bridge recommended", out["tier_line"]) + def worker(self, choice="moderate", confidence=0.88, **overrides): + args = dict(task="fn-1.1", effort_levels="low,medium,high,xhigh,max", default_effort="medium", + explicit_effort=None, role_effort=None) + args.update(overrides) + return self.tier(choice, confidence, **args) + + def test_worker_effort_follows_tier_within_host_levels(self): + unavailable = {"result": {"available": False, "reason": "no_key"}} + cases = (({"choice": "mechanical"}, "low", " at low effort (host default medium)"), + ({"choice": "intelligent"}, "high", " at high effort (host default medium)"), + ({"choice": "moderate"}, None, " at default effort"), + ({"choice": "long_running"}, None, " at default effort"), + ({"choice": "intelligent", "confidence": 0.79}, None, " at default effort"), + ({"choice": "mechanical", "default_effort": "low"}, None, " at default effort"), + ({"choice": "intelligent", "default_effort": "max"}, None, " at default effort"), + (unavailable, None, " at default effort"), + ({"choice": "intelligent", "explicit_effort": "low"}, "low", " at low effort (requested)"), + ({"choice": "mechanical", "role_effort": "high", "explicit_effort": "low"}, None, + " at high effort (role pins effort)"), + ({"choice": "mechanical", "effort_levels": None}, None, + "; effort not applied (host spawn takes no effort)"), + ({"choice": "mechanical", "default_effort": None}, None, + "; effort not applied (host default effort unknown)"), + ({"explicit_effort": "xhigh", "effort_levels": None}, None, + "; effort xhigh not applied (host spawn takes no effort)")) + for overrides, spawn_effort, suffix in cases: + with self.subTest(**{k: str(v) for k, v in overrides.items()}): + out = self.worker(**overrides) + self.assertEqual(out["spawn_effort"], spawn_effort) + self.assertTrue(out["tier_line"].endswith(suffix), out["tier_line"]) + + def test_explicit_model_keeps_tier_effort_and_bridge_carries_it(self): + out = self.worker("mechanical", explicit_model="claude-sonnet-5-5") + self.assertEqual((out["spawn_model"], out["spawn_effort"]), ("claude-sonnet-5-5", "low")) + self.assertEqual(out["tier_line"], "Tier: implementer - claude-sonnet-5-5 (explicit invocation)" + " at low effort (host default medium)") + for overrides in ({"explicit_model": "gpt-6.1-sol"}, {}, {"role_effort": "high"}): + with self.subTest(**overrides): + out = self.worker("mechanical", can_spawn_model=False, can_bridge=True, **overrides) + self.assertIsNone(out["spawn_effort"]) + self.assertEqual(out["implementer"], overrides.get("explicit_model", "fast-test-model") + " at low") + # Scouts (no --task) keep their own effort selection. + out = self.tier(effort_levels="low,medium,high", default_effort="medium") + self.assertEqual(out["tier_line"], "Tier: mechanical (jev 0.88) -> fast-test-model") + + def test_worker_effort_reaches_the_cli_result_without_a_key(self): + with tempfile.TemporaryDirectory() as tmp: + for args in (["init"], ["spec", "create", "--title", "Demo"], + ["task", "create", "--spec", "fn-1-demo", "--title", "Rename a constant"]): + subprocess.run([*FLOWCTL_CMD, *args, "--json"], cwd=tmp, check=True, capture_output=True) + env = {k: v for k, v in os.environ.items() if k != "TYPESAFE_API_KEY"} + run = subprocess.run([*FLOWCTL_CMD, "judge", "--preset", "tier", "--task", "fn-1-demo.1", "--can-spawn-model", + "--effort-levels", "low,medium,high", "--default-effort", "medium", "--json"], + cwd=tmp, env=env, capture_output=True, text=True) + self.assertEqual(run.returncode, 0, run.stdout + run.stderr) + out = json.loads(run.stdout) + self.assertEqual((out["tier_line"], out["spawn_effort"]), + ("Tier: session (jev-unavailable(no_key)) at default effort", None)) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/sync-codex.sh b/scripts/sync-codex.sh index 6f70f0b87..efa0017c4 100755 --- a/scripts/sync-codex.sh +++ b/scripts/sync-codex.sh @@ -563,7 +563,7 @@ if [ -f "$phases" ]; then Implementation is the **implementer** tier: absent any preference, the worker runs on the session model. **Routing precedence, highest first: an explicit argument in the invocation, then the project routing block in the instruction file, then the agent definition's own default, then the session model.** How this harness reaches a non-session model — and what the degradation is when it cannot — lives in its reach page (`plugins/flow-next/docs/reach/`), never here. -Before spawning, apply [judge-tier.md](judge-tier.md) once for this task. Use its selected model in the host spawn-model parameter as well as the `IMPLEMENTER:` line; an explicit invocation always wins. +Before spawning, apply [judge-tier.md](judge-tier.md) once for this task. Use its selected model and effort in the host spawn parameters as well as the `IMPLEMENTER:` line; an explicit invocation always wins. **When the implementer tier resolves to a model this harness reaches only over a CLI bridge, the worker bridges and the conductor never does.** The dispatch below is unchanged: the worker resolves the tier itself (worker Phase 1b), hands the task to the bridged child with the usage guide's brief, and reviews the child's commit range before its own review dispatch. The bridged child owns the task and its own delegation; a conductor that composed a brief, ran a bridge call, or fanned out on the implementer's behalf has broken this. @@ -634,7 +634,7 @@ point — the worker's return, the host's own tool timeout or error, or a lost result — where 3d's side-effects rule classifies whatever the lane left behind. -`IMPLEMENTER` carries an explicit invocation model first; otherwise it may carry the confident mechanical tier's reachable fast-scout model from `references/judge-tier.md`. Omit it when neither applies. Pass the native model through the spawn-model parameter too. The conductor never bridges or composes the worker's brief. +`IMPLEMENTER` carries an explicit invocation model first; otherwise it may carry the confident mechanical tier's reachable fast-scout model from `references/judge-tier.md`. Omit it when neither applies; when the implementer goes over a bridge, it carries the effort as ` at `. Pass the native model and effort through the spawn parameters too. The conductor never bridges or composes the worker's brief. `BASELINE_HANDOFF` is optional. The conductor MAY pass it only when ALL hold: the prior task in this run reached done with its Phase 5 Verify green over the SAME Quick commands, HEAD has not moved since except by commits changing only `.flow/` paths, and the new task's declared Touches do not intersect files changed since that verification. Conductor judgment on stated facts; when in doubt, omit the line. On the wave route the first task receives no handoff. The rolling route instead runs a green spec-base baseline before its first admission and may hand that baseline to the first batch under the same `.flow/`-only rule. Check every intervening commit with `git log --format= --name-only ..HEAD`; any non-`.flow/` path invalidates the handoff, including changes later reverted.