diff --git a/infra/worker-host/env.example b/infra/worker-host/env.example index 11e850409e..69b9ca969b 100644 --- a/infra/worker-host/env.example +++ b/infra/worker-host/env.example @@ -21,8 +21,13 @@ CLAUDE_CODE_OAUTH_TOKEN= # autodetect this path since 2026-07-12; pinning it here is belt-and-suspenders. SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -# Engine defaults (label > WORKER_ENGINE > codex). -WORKER_ENGINE=codex +# Engine / model route. WORKER_ENGINE is deliberately NOT set here: each lane +# carries its own default (implement + spike -> claude; review + land -> codex), +# and an `engine:*` ticket/PR label overrides per item. Everything in this file +# is exported wholesale (`set -a`) and §2.6 copies it into the Actions runner +# env, so a value pinned here would apply to BOTH lanes at once — in particular +# `WORKER_ENGINE=codex` would put implement/spike workers back on the clodex +# gateway. Override per invocation, not here. CODEX_MODEL=gpt-5.6-sol CODEX_EFFORT=high diff --git a/skills/implementing-tickets/scripts/implement-dispatch.sh b/skills/implementing-tickets/scripts/implement-dispatch.sh index 84beeaf940..48196dc6e5 100755 --- a/skills/implementing-tickets/scripts/implement-dispatch.sh +++ b/skills/implementing-tickets/scripts/implement-dispatch.sh @@ -26,8 +26,9 @@ # BOARD_REPO owner/name (default: resolved from LOCAL_REPO via gh) # IMPLEMENT_MAX_CONCURRENT implement/spike worker slot cap (default 5); # review-pr-*/land-pr-* workers never count against it -# WORKER_ENGINE model route codex|claude (default codex); an engine:* -# ticket label wins over the env +# WORKER_ENGINE model route codex|claude (default claude); an engine:* +# ticket label wins over the env, so `engine:codex` opts a +# single ticket back onto the gateway # CLODEX_SETTINGS gateway settings file for the codex route # (default ~/.claude/clodex-settings.json) # CLODEX_EFFORT reasoning effort for the codex route (default xhigh) @@ -175,7 +176,7 @@ dispatch_one() { fi engine="${T_ENGINE_LABEL:-}" - [ -n "$engine" ] || engine="${WORKER_ENGINE:-codex}" + [ -n "$engine" ] || engine="${WORKER_ENGINE:-claude}" if [ "$T_CATEGORY" = "spike" ]; then role="SPIKE"; protocol_file="$SPIKE_PROTOCOL" @@ -217,7 +218,13 @@ PY "${IMPLEMENT_MODEL:-fable}")" \ || { echo "#$n: worker spawn failed" >&2; return 1; } else - spawn_out="$("$DAEMON_SCRIPTS/daemon-spawn.sh" --no-wait "$name" "$prompt" "$LOCAL_REPO" "$name" \ + # Cleared, not merely unset by us: this dispatcher can itself run inside a + # gateway-routed daemon whose environment exports these, and daemon-spawn.sh + # would inherit them, apply the flags AND persist them into the registry + # meta — so every later resume would keep riding the gateway while the log + # said engine=claude. + spawn_out="$(DAEMON_CLAUDE_SETTINGS='' DAEMON_CLAUDE_EFFORT='' \ + "$DAEMON_SCRIPTS/daemon-spawn.sh" --no-wait "$name" "$prompt" "$LOCAL_REPO" "$name" \ "${IMPLEMENT_MODEL:-}")" \ || { echo "#$n: worker spawn failed" >&2; return 1; } fi diff --git a/skills/issue-tracker/SKILL.md b/skills/issue-tracker/SKILL.md index 2db0ff4fd5..aee1c0c48f 100644 --- a/skills/issue-tracker/SKILL.md +++ b/skills/issue-tracker/SKILL.md @@ -149,10 +149,12 @@ pick by repo visibility: normal. Derived from GitHub PR state on every snapshot — never a label, never auto-closed. 2. Resolve the ENGINE — ticket label `engine:claude`/`engine:codex` → - `$WORKER_ENGINE` → default `codex`. Every worker is ONE species — a + `$WORKER_ENGINE` → default `claude`. Every worker is ONE species — a Claude-harness daemon; the engine names only its model route (`codex` = the clodex gateway settings, GPT models through the local proxy; - `claude` = plain Claude models). Render the spawn bootstrap + `claude` = plain Claude models). Label `engine:codex` to put one ticket + back on the gateway; `engine:claude` is redundant now but still valid. + Render the spawn bootstrap (`doperpowers:implementing-tickets` `references/worker-bootstrap.md` — the worker opens its protocol from the dispatcher-pinned file the bootstrap names, then reads its own ticket and the repo's @@ -170,13 +172,15 @@ pick by repo visibility: a spike). 3. Spawn via `daemon-spawn.sh "-" "" ` from `orchestrating-daemons` — always a worktree; workers write code. - The codex route prefixes the gateway env and pins the gateway's model - alias as arg 5: + The claude route — the default — passes no gateway env and no model + arg: the model inherits unless `IMPLEMENT_MODEL` pins it, and since + daemon-spawn writes no settings/effort into the meta, its resumes stay + plain too. The codex route prefixes the gateway env and pins the + gateway's model alias as arg 5: `DAEMON_CLAUDE_SETTINGS="${CLODEX_SETTINGS:-$HOME/.claude/clodex-settings.json}" DAEMON_CLAUDE_EFFORT="${CLODEX_EFFORT:-xhigh}" daemon-spawn.sh … fable` (daemon-spawn persists settings/effort into the registry meta; daemon-resume restores them on every fork — without that a gateway - worker silently reverts to plain models on its first resume). The - claude route passes no gateway env; the model inherits unless pinned. + worker silently reverts to plain models on its first resume). 4. `board-bind.sh `. Write NOTHING else: the worker's first board write is its gate verdict — `in-progress` (+ a `[gate]` comment) means the gate passed; a park state means it failed. diff --git a/skills/issue-tracker/references/sweep-setup.md b/skills/issue-tracker/references/sweep-setup.md index b54c1ab080..9ff014e44e 100644 --- a/skills/issue-tracker/references/sweep-setup.md +++ b/skills/issue-tracker/references/sweep-setup.md @@ -100,7 +100,7 @@ actually run before trusting a cron arming. | `IMPLEMENT_MAX_CONCURRENT` | 5 | implement/spike worker slots (review/land workers never count) | | `SWEEP_STALL_MINUTES` | 45 | a live worker silent this long is resumed with a nudge | | `SWEEP_RECOVERY_CAP` | 3 | lifetime sweep-initiated resumes per daemon, then park `needs-human` | -| `WORKER_ENGINE` | codex | default model route; an `engine:*` ticket/PR label wins | +| `WORKER_ENGINE` | claude (implement/spike) · codex (review/land) | overrides the lane's default model route; an `engine:*` ticket/PR label wins over it. Setting it applies to BOTH lanes — `WORKER_ENGINE=codex` puts implement workers back on the gateway too | | `AUTO_MERGE_ENABLED` | false | review worker's trivial-tier self-merge | | `LAND_ENABLED` | false (dry-run) | land worker merges for real | diff --git a/tests/implementing-tickets/test-implement-dispatch.sh b/tests/implementing-tickets/test-implement-dispatch.sh index b71136b7b0..e807a8b271 100755 --- a/tests/implementing-tickets/test-implement-dispatch.sh +++ b/tests/implementing-tickets/test-implement-dispatch.sh @@ -49,6 +49,13 @@ export PROMPT_DIR="$TEST_ROOT/prompts"; mkdir -p "$PROMPT_DIR" export STUB_COUNT="$TEST_ROOT/count" export BOARD_REPO="test/repo" export BOARD_SCRIPTS="$REPO_ROOT/skills/issue-tracker/scripts" +# Ambient gateway route, as a gateway-routed daemon (or an operator shell that +# sourced one) exports it. Present for the WHOLE suite on purpose: it makes every +# "no gateway env" assertion below a regression test — the claude route must hand +# daemon-spawn.sh nothing, since daemon-spawn.sh persists what it inherits into +# the registry meta and every later resume would silently ride the gateway. +export DAEMON_CLAUDE_SETTINGS="$TEST_ROOT/ambient-gateway.json" +export DAEMON_CLAUDE_EFFORT="high" # real git: bare origin + clone whose main carries a repo-facts manifest ORIGIN="$TEST_ROOT/origin.git" @@ -98,7 +105,7 @@ chmod +x "$STUB_DAEMONS/daemon-spawn.sh" "$STUB_DAEMONS/daemon-retire.sh" # ---- board seed --------------------------------------------------------------- # 1 ELIGIBLE P1 impl · 2 blocked-by-1 · 3 ELIGIBLE P0 spike · 4 in-progress · -# 5 ELIGIBLE P2 engine:claude +# 5 ELIGIBLE P2 engine:claude · 7 ELIGIBLE unprioritized engine:codex python3 - <<'PY' import json, os def issue(num, title, labels, body="body of #%s", blocked=None): @@ -109,7 +116,7 @@ def issue(num, title, labels, body="body of #%s", blocked=None): "closesPRs": [], "xrefPRs": [], "comments": [], "createdAt": "2026-07-18T00:00:00Z", "updatedAt": "2026-07-18T00:00:00Z", "url": "https://github.com/test/repo/issues/%d" % num} -s = {"next": 6, "labels": [], "issues": { +s = {"next": 8, "labels": [], "issues": { "1": issue(1, "Fix the report builder pipeline", ["status:ready-for-agent", "priority:P1", "bug"], body="Repro: the report build fails on BUILD-MARKER."), @@ -118,6 +125,7 @@ s = {"next": 6, "labels": [], "issues": { "4": issue(4, "Mid-flight work", ["status:in-progress"]), "5": issue(5, "Tune the copy", ["status:ready-for-agent", "priority:P2", "engine:claude"]), "6": issue(6, "Delivered, awaiting review", ["status:in-review"]), + "7": issue(7, "Port the legacy adapter", ["status:ready-for-agent", "engine:codex"]), }} json.dump(s, open(os.environ["MOCK_GH_STATE"], "w")) PY @@ -130,10 +138,13 @@ out="$(run 1)" assert_contains "$out" "dispatched #1" "triggered dispatch reports the ticket" assert_contains "$(cat "$SPAWN_LOG")" "spawn:--no-wait 1-fix-the-report-builder-pipeline" \ "spawn is --no-wait with the - name" -assert_contains "$(cat "$SPAWN_LOG")" "spawn-env:settings=$HOME/.claude/clodex-settings.json;effort=xhigh" \ - "default engine codex rides the gateway env" -assert_contains "$(grep '^spawn:' "$SPAWN_LOG" | head -1)" "model=fable" \ - "codex route pins the gateway model alias" +assert_contains "$(grep '^spawn-env:' "$SPAWN_LOG" | head -1)" "settings=;effort=" \ + "default engine claude passes no gateway env" +# bracketed so the fixed-string match is exact: "[model=]" would otherwise be a +# substring of "[model=fable]" and prove nothing about an EMPTY model arg. +first_spawn="$(grep '^spawn:' "$SPAWN_LOG" | head -1)" +assert_contains "[${first_spawn##* }]" "[model=]" \ + "claude route pins no model — it inherits unless IMPLEMENT_MODEL says otherwise" PROMPT="$PROMPT_DIR/1-fix-the-report-builder-pipeline.prompt" assert_file_contains "$PROMPT" "IMPLEMENT worker for ticket #1" "prompt carries the IMPLEMENT role" assert_file_not_contains "$PROMPT" "BUILD-MARKER" "prompt carries no inlined issue body (the worker reads its ticket via gh)" @@ -168,7 +179,30 @@ assert_file_contains "$PROMPT3" "(none — spike lane)" "spike gets the literal out="$(run 5)" assert_contains "$(grep 'spawn:--no-wait 5-' "$SPAWN_LOG")" "5-tune-the-copy" "claude-engine ticket dispatches" last_env="$(grep '^spawn-env:' "$SPAWN_LOG" | tail -1)" -assert_contains "$last_env" "settings=;effort=" "engine:claude label suppresses the gateway env" +assert_contains "$last_env" "settings=;effort=" "engine:claude label (redundant since the default flipped) still suppresses the gateway env" + +out="$(run 7)" +assert_contains "$(grep 'spawn:--no-wait 7-' "$SPAWN_LOG")" "7-port-the-legacy-adapter" "codex-engine ticket dispatches" +last_env="$(grep '^spawn-env:' "$SPAWN_LOG" | tail -1)" +assert_contains "$last_env" "settings=$HOME/.claude/clodex-settings.json;effort=xhigh" \ + "engine:codex label opts back into the gateway route" +assert_contains "$(grep '^spawn:' "$SPAWN_LOG" | tail -1)" "model=fable" \ + "label-selected codex route pins the gateway model alias" + +echo "implement-dispatch: WORKER_ENGINE env override" + +rm -f "$DAEMON_HOME"/*.json; : > "$SPAWN_LOG"; echo 0 > "$STUB_COUNT" +out="$(WORKER_ENGINE=codex run 1)" +assert_contains "$(grep '^spawn-env:' "$SPAWN_LOG" | tail -1)" "settings=$HOME/.claude/clodex-settings.json;effort=xhigh" \ + "WORKER_ENGINE=codex overrides the claude default on a label-less ticket" + +rm -f "$DAEMON_HOME"/*.json; : > "$SPAWN_LOG"; echo 0 > "$STUB_COUNT" +out="$(WORKER_ENGINE=codex run 5)" +assert_contains "$(grep '^spawn-env:' "$SPAWN_LOG" | tail -1)" "settings=;effort=" \ + "engine:claude label still wins over WORKER_ENGINE=codex" + +rm -f "$DAEMON_HOME"/*.json; : > "$SPAWN_LOG"; echo 0 > "$STUB_COUNT" +out="$(run 1)" echo "implement-dispatch: idle owner does not block" @@ -192,6 +226,9 @@ assert_contains "$order" "spawn:--no-wait3-,spawn:--no-wait1-,spawn:--no-wait5-" "sweep dispatches in priority order (P0, P1, P2)" assert_not_contains "$(cat "$SPAWN_LOG")" "spawn:--no-wait 2-" "sweep skips blocked tickets" assert_not_contains "$(cat "$SPAWN_LOG")" "spawn:--no-wait 4-" "sweep skips non-ready tickets" +n_gateway="$(grep -c "settings=$HOME/.claude/clodex-settings.json" "$SPAWN_LOG" || true)" +assert_contains "$n_gateway" "1" \ + "sweep sends only the engine:codex ticket through the gateway — every other worker rides the claude default" out="$(run --sweep)" assert_contains "$out" "skip #3: bound worker" "consecutive sweep re-dispatches nothing"