Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .agents/skills/quota-array-dispatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ Grok prepaid `credits` are unrelated to paid-window headroom; never read them as
The selector is the mechanical owner of dispatch capacity and of ranking among remaining eligible candidates.
It does not replace reasoning-class fit: keep only candidates that meet the required reasoning class before passing the set, and never use `spendPriority` or remaining quota to silently replace that class.
When every remaining candidate is tight, dispatch inside the strongest-reasoning class if one of those candidates can proceed, or stop and report that the strongest-class choice cannot proceed rather than downgrading it to spend or conserve quota.
That rule governs this selection, which is the initial dispatch decision.
The separate in-run `modelFallback` response to a model that depletes after dispatch (`AGENTS.md` section 4) walks the configured chain for the class already dispatched, so it never re-opens class choice.
An exhausted chain for the required class stops and reports there too, rather than relaunching beneath that class.

Providers exposed by quota-axi, including Claude, Codex, Grok, and Cursor, require fresh telemetry within the configured maximum age and a tightest live percentage strictly above `reservePercent`.
Providers exposed by quota-axi, including Claude, Codex, Grok, Cursor, and agy, require fresh telemetry within the configured maximum age and a tightest live percentage strictly above `reservePercent`.
Stale, unavailable, malformed, or windowless telemetry makes that provider ineligible for a new dispatch.
A provider whose pools are billed separately would be priced by its worst pool under that rule, so a profile may declare the one window it draws on with `quotaWindow`; `docs/configuration.md` owns that field's semantics.
Confirm the declared window against the provider's live telemetry before relying on it, because a declared window the telemetry does not carry blocks that candidate rather than repricing it.
Expand All @@ -84,7 +87,8 @@ Apply only among candidates satisfying required fit and strongest reasoning clas
2. Pass that exact object or array to `FM_HOME=<active-home> bin/fm-dispatch-select.mjs select`.
3. Read its sanitized per-provider diagnostics and selected JSON profile.
4. Pass the selected `harness`, `provider`, `model`, and `effort` axes to `fm-spawn.sh`; it records `provider` as routing evidence without forwarding it to the harness CLI.
Its `--provider` accepts `claude`, `codex`, and `grok` only, so a selected profile whose provider is native to its harness and outside that set, such as `cursor`, is spawned without the redundant flag; the recorded harness still establishes that provider for a later `record-failure`.
Its `--provider` accepts every routable provider, including `cursor` and `agy`, and a native harness refuses any provider but its own; `docs/configuration.md` owns which adapters are native.
Omitting the field on a native harness is equally safe, because the recorded harness alone establishes that provider for a later `record-failure`.
5. If it exits 3, stop and report that no candidate has current dispatch-capacity evidence rather than choosing manually around the reserve, cooldown, or telemetry refusal.
6. If a running task with recorded routing-provider metadata records provider rate-limit or quota-exhaustion evidence in its status log, run `fm-dispatch-select.mjs record-failure --provider <provider> --task <id>` before retrying the candidate set.
7. Use `clear --provider <provider>` only after the credential or provider condition is known to be corrected; it clears the cooldown, not dispatch history.
Expand Down
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,7 @@ A silent bootstrap section needs no action; for any printed actionable diagnosti
## 4. Harness and runtime dispatch

Load `harness-adapters` before every spawn or recovery and before trust handling, skill invocation, interrupt, exit, resume, or adapter verification.
The verified harnesses are `claude`, `codex`, `opencode`, `pi`, `pi-signed`, `grok`, `kimi`, `cline`, `cursor-agent`, `cursor`, and `copilot`, plus `muse` for crewmates and scouts only and `agy` for crewmate launches only; never dispatch on an unverified adapter.
`agy` is verified for crewmate launches only, and a `--secondmate` spawn refuses it.
The verified harnesses are `claude`, `codex`, `opencode`, `pi`, `pi-signed`, `grok`, `kimi`, `cursor-agent`, and `cursor`, plus `muse`, `agy`, `cline`, and `copilot` for crewmate and scout launches only; never dispatch on an unverified adapter, and never select one of those four for a secondmate (`docs/configuration.md` "Harness support" owns the per-kind verified set).
If static `config/crew-harness` or `config/secondmate-harness` names an unverified adapter, report it and fall back only to a verified adapter rather than launching it.

`docs/configuration.md` owns dispatch-profile and runtime-backend schemas, `bin/fm-harness.sh` owns static resolution, and `bin/fm-spawn.sh` owns launch flags and fail-closed validation.
Expand All @@ -209,7 +208,15 @@ Do not add model-specific versions of that policy.
`secondmate-provisioning` owns secondmate harness pins and inherited local material, while `harness-adapters` owns the harness consequences.
Dispatch only on a backend that `fm-spawn` validates as spawn-capable; pass an explicit per-spawn `--backend` only under that exact task's own authority, never as later-task precedent (selection contract: [`docs/configuration.md`](docs/configuration.md) "Runtime backend").
A missing dependency, authentication failure, unsupported backend, or version refusal is a blocker; never silently retry on another backend.
When an active ship or scout session is blocked due to token/quota exhaustion or harness limits, Firstmate may adopt and relaunch the blocked session in place using `bin/fm-runtime-handoff.sh <task-id> --harness <name> [--model <name>] [--effort <level>] [--progress-note <text>]`. This cleanly exits the blocked agent, preserves the existing worktree, lease, PR metadata, and work-in-progress without loss, and relaunches the replacement agent in the same worktree to continue execution seamlessly.
When an active ship or scout session is blocked due to token/quota exhaustion or harness limits, Firstmate may adopt and relaunch the blocked session in place using `bin/fm-runtime-handoff.sh <task-id> --harness <name> [--model <name>] [--effort <level>] [--progress-note <text>]`.
This cleanly exits the blocked agent, preserves the existing worktree, lease, PR metadata, and work-in-progress without loss, and relaunches the replacement agent in the same worktree to continue execution seamlessly.
When a worker's model depletes mid-run, switch models within the same harness automatically (relaunch in place via `bin/fm-runtime-handoff.sh` with `--model`) instead of blocking, parking, or escalating a routine depletion.
Read model fallback chains from `config/crew-dispatch.json` `modelFallback` (legacy alias `_model_fallback`) without hardcoding a duplicate copy, and move work to the next harness lane only when a harness's whole model chain is exhausted.
Depletion detection for a provider quota-axi exposes is the recorded `record-failure` telemetry contract above; live 429, limit, or quota errors in the pane or status log are the trigger of record only for runtimes without quota telemetry (such as ClinePass).
Every automatic model switch must be logged and visible in status reporting rather than silently downgrading reasoning class.
The fail-closed capacity contract (reserve, cooldown, and telemetry freshness) remains enforced.
The strongest-reasoning-class rule governs which candidate is dispatched in the first place, so it is never traded away to conserve quota at selection time; model fallback is the separate in-run response to a model that depleted after dispatch, and it walks the configured chain rather than choosing a class.
If the chain for the required class is exhausted, stop and report that the strongest-class choice cannot proceed rather than relaunching beneath it.

## 5. Recovery

Expand Down
20 changes: 18 additions & 2 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,18 @@ crew_dispatch_validate() {
or ($items | any(has("quotaWindow") and (((.quotaWindow | type) != "string") or (.quotaWindow | length) == 0)));
def malformed_provider($items):
($items | any(has("provider") and (((.provider | type) != "string") or (.provider | length) == 0)));
def model_fallback: (.modelFallback // ._model_fallback);
def bad_fallback_harnesses:
(model_fallback // {}) | keys | map(select(. as $h | verified($h) | not)) | unique;
def bad_fallback_chains:
(model_fallback // {})
| to_entries
| map(select(
((.value | type) != "array")
or ((.value | length) == 0)
or (.value | any((type != "string") or (length == 0)))))
| map(.key)
| unique;
def routing_setting_ok($key; $value):
if ($value | type) != "number" or ($value | floor) != $value then false
elif $key == "reservePercent" then $value >= 0 and $value <= 99
Expand All @@ -1050,6 +1062,10 @@ crew_dispatch_validate() {
"subscriptionRouting has unknown field: " + ([.subscriptionRouting | keys[] | . as $key | select((["reservePercent","telemetryMaxAgeSeconds","cooldownSeconds"] | index($key)) == null)] | sort | join(", "))
elif has("subscriptionRouting") and ([.subscriptionRouting | to_entries[] | select(. as $entry | routing_setting_ok($entry.key; $entry.value) | not)] | length) > 0 then
"subscriptionRouting setting is out of range: " + ([.subscriptionRouting | to_entries[] | select(. as $entry | routing_setting_ok($entry.key; $entry.value) | not) | .key] | sort | join(", "))
elif has("modelFallback") and has("_model_fallback") then "modelFallback and its legacy alias _model_fallback cannot both be declared"
elif (has("modelFallback") or has("_model_fallback")) and (model_fallback | type) != "object" then "modelFallback must be an object mapping a harness to its ordered model chain"
elif (bad_fallback_harnesses | length) > 0 then "modelFallback has an unverified harness: " + (bad_fallback_harnesses | join(", "))
elif (bad_fallback_chains | length) > 0 then "modelFallback chain must be a non-empty array of non-empty model ids: " + (bad_fallback_chains | join(", "))
elif has("rules") and (.rules | type) != "array" then "rules must be an array"
elif [(.rules // [])[]? | select(type != "object")] | length > 0 then "each rule must be an object"
elif [(.rules // [])[]? | select((.when? | type) != "string" or (.when | length) == 0)] | length > 0 then "each rule needs non-empty when"
Expand All @@ -1074,9 +1090,9 @@ crew_dispatch_validate() {
| map(select(. != null))
| map(select(. as $h | verified($h) | not))
| unique) as $bad_harnesses
| (configured_profiles | map(.provider? // empty) | map(. as $provider | select((["claude","codex","grok"] | index($provider)) == null)) | unique) as $bad_providers
| (configured_profiles | map(.provider? // empty) | map(. as $provider | select((["claude","codex","grok","cursor","agy"] | index($provider)) == null)) | unique) as $bad_providers
| (configured_profiles | map(select(.harness == "kimi" or .provider == "kimi")) | length) as $bad_kimi_routes
| (configured_profiles | map(select((.harness == "claude" or .harness == "codex" or .harness == "grok") and .provider? != null and .provider != .harness) | "\(.harness):\(.provider)") | unique) as $mismatched_native_providers
| (configured_profiles | map(select((.harness == "claude" or .harness == "codex" or .harness == "grok" or .harness == "cursor" or .harness == "agy") and .provider? != null and .provider != .harness) | "\(.harness):\(.provider)") | unique) as $mismatched_native_providers
| if ($bad_harnesses | length) > 0 then "unverified harness: " + ($bad_harnesses | join(", "))
elif $bad_kimi_routes > 0 then "Kimi is unsupported for subscription dispatch"
elif ($mismatched_native_providers | length) > 0 then "native harness/provider mismatch: " + ($mismatched_native_providers | join(", "))
Expand Down
26 changes: 14 additions & 12 deletions bin/fm-control-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fm_control_verb_allowed() { # <verb>
# than guessed at, exactly as a spawn on it would be.
fm_control_harness_supported() { # <harness>
case "${1-}" in
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse|cline) return 0 ;;
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse|cline|copilot|agy) return 0 ;;
esac
return 1
}
Expand All @@ -88,21 +88,23 @@ fm_control_harness_family() { # <recorded-harness>
cursor*) printf 'cursor' ;;
muse*) printf 'muse' ;;
cline*) printf 'cline' ;;
copilot*) printf 'copilot' ;;
agy*) printf 'agy' ;;
*) return 1 ;;
esac
}

# Which task kinds an adapter is verified to run. muse and cline are
# crewmate/scout adapters only: neither has a primary supervision protocol, and
# bin/fm-spawn.sh refuses a --secondmate launch on either. The control plane
# Which task kinds an adapter is verified to run. muse, cline, copilot, and agy
# are crewmate/scout adapters only: none has a primary supervision protocol, and
# bin/fm-spawn.sh refuses a --secondmate launch on any of them. The control plane
# asks this BEFORE it stops anything, so an incompatible relaunch target is
# refused while the current agent is still running rather than after it has
# been stopped.
fm_control_harness_supports_kind() { # <harness> <kind>
local harness=${1-} kind=${2-}
fm_control_harness_supported "$harness" || return 1
case "$harness" in
muse|cline) [ "$kind" != secondmate ] || return 1 ;;
muse|cline|copilot|agy) [ "$kind" != secondmate ] || return 1 ;;
esac
return 0
}
Expand All @@ -114,8 +116,8 @@ fm_control_harness_supports_kind() { # <harness> <kind>
# borrowing grok's interrupt key here would stop the agent instead of its turn.
fm_control_interrupt_key() { # <harness>
case "${1-}" in
claude|codex|opencode|pi|pi-signed|kimi|cursor|muse|cline) printf 'Escape' ;;
grok) printf 'C-c' ;;
claude|codex|opencode|pi|pi-signed|kimi|cursor|muse|cline|agy) printf 'Escape' ;;
grok|copilot) printf 'C-c' ;;
*) return 1 ;;
esac
}
Expand All @@ -125,7 +127,7 @@ fm_control_interrupt_key() { # <harness>
fm_control_interrupt_repeat() { # <harness>
case "${1-}" in
opencode) printf '2' ;;
claude|codex|pi|pi-signed|grok|kimi|cursor|muse|cline) printf '1' ;;
claude|codex|pi|pi-signed|grok|kimi|cursor|muse|cline|copilot|agy) printf '1' ;;
*) return 1 ;;
esac
}
Expand All @@ -143,7 +145,7 @@ fm_control_interrupt_repeat() { # <harness>
fm_control_interrupt_clear_key() { # <harness>
case "${1-}" in
muse) printf 'C-u' ;;
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|cline) ;;
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|cline|copilot|agy) ;;
*) return 1 ;;
esac
}
Expand All @@ -155,7 +157,7 @@ fm_control_interrupt_ack_source() { # <harness>
# after an interrupt was measured as variable - sometimes seconds, sometimes
# not within 20 - so a cancellation claim built on it would be unreliable.
# Normal turn completion is prompt, which is what the busy fold depends on.
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|cline) printf 'none' ;;
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|cline|copilot|agy) printf 'none' ;;
*) return 1 ;;
esac
}
Expand All @@ -171,7 +173,7 @@ fm_control_interrupt_ack_source() { # <harness>
# nothing for an adapter that exits on a key instead.
fm_control_exit_command() { # <harness>
case "${1-}" in
claude|opencode|grok|kimi|cursor|muse) printf '/exit' ;;
claude|opencode|grok|kimi|cursor|muse|copilot|agy) printf '/exit' ;;
codex|pi|pi-signed) printf '/quit' ;;
cline) ;;
*) return 1 ;;
Expand All @@ -186,7 +188,7 @@ fm_control_exit_command() { # <harness>
fm_control_exit_key() { # <harness>
case "${1-}" in
cline) printf 'C-c' ;;
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse) ;;
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse|copilot|agy) ;;
*) return 1 ;;
esac
}
Expand Down
Loading
Loading