feat(bin): add fleet admission control stages 0 and 1 - #1266
Closed
sbracewell64 wants to merge 21 commits into
Closed
feat(bin): add fleet admission control stages 0 and 1#1266sbracewell64 wants to merge 21 commits into
sbracewell64 wants to merge 21 commits into
Conversation
Closed
5 tasks
sbracewell64
force-pushed
the
fm/admission-control-stage01
branch
3 times, most recently
from
July 31, 2026 02:47
0ac5a71 to
9c61437
Compare
A fleet launcher will soon open PRIMARY firstmate sessions alongside the crewmate sessions fm-spawn.sh opens, so both need the same verified launch commands. Today that knowledge lives only inside bin/fm-spawn.sh, and the drift a second copy causes is not hypothetical: a downstream registry hand-copied claude's command as `claude --dangerously-skip-permissions`, dropping CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false - the ghost-text suppression that keeps firstmate from reading predicted-prompt text as real typed input when it captures a pane. Extract launch_template, model_flag_for_harness, and effort_flag_for_harness (plus the shell_quote both flag resolvers depend on) into a new sourced bin/fm-launch-lib.sh, and have fm-spawn.sh source it. Every crewmate, scout, and secondmate template is byte-identical to before, so spawn behavior is unchanged on all six verified adapters. launch_template also gains a `primary` kind for the launcher. A primary session has no task, no worktree, no brief, and no status file, so it launches bare and is greeted by the session-start adapters already installed in the home; each primary template keeps its adapter's verified autonomy flag and claude's ghost-text prefix. An unrecognized kind still resolves to the crewmate shape, and an unverified adapter still returns non-zero for every kind. tests/fm-launch-lib.test.sh pins both arms directly, including a proof that fm-spawn.sh redefines none of the functions and that no other script under bin/ hand-writes a launch command. Existing suites that read the template bytes now read them from their new owner.
…ighten launch-lib ownership
bin/fm-launch.sh is the captain's front door: it renders a five-entry harness menu, starts one firstmate primary session in this home, and attaches to it. The menu is derived and probed, never declared. An entry is available only when its harness binary resolves on PATH, or - for a Pi-routed entry - when the provider named in its model appears in pi's local auth record. Unavailable entries stay visible and dim, each with one actionable line, so the menu never changes shape under the captain's muscle memory. Both probes are local file reads, so the menu touches no network and executes no binary at all. Menu entries carry no launch command. They name a harness plus an optional model and effort, and the command is resolved through bin/fm-launch-lib.sh at launch time - the single owner a downstream registry has already drifted from once by hand-copying a launch string and dropping claude's ghost-text suppression prefix. The launcher states on every render, before the choice, that the session it starts runs without permission prompts. That discharges the consumer obligation bin/fm-launch-lib.sh's header binds on every consumer of a primary template. Herdr is mandatory with no silent fallback to a bare shell, and the gate runs after selection so no socket round trip sits on the critical path. Before creating anything the launcher looks for a primary already running in this home and offers to reattach, so two sessions can never contend for one home's session lock. Selection is one keypress. A human who mistypes gets a redrawn prompt; a scripted caller keeps the refuse-don't-reprompt behavior, and a blank line or EOF refuses rather than launching whatever the default happens to be - taking the default there once started an unattended session nobody chose. Presets live in gitignored config/launch-presets.json and the built-in five need no configuration. They are deliberately not inherited into secondmate homes: a secondmate is provisioned and launched by the primary through bin/fm-spawn.sh, never through this front door, so there would be no consumer for an inherited menu. The Windows entry point and WSL bridge are out of scope here and land separately.
…coverage tests/fm-launch-lib.test.sh's one-owner guards grepped bin/fm-spawn.sh for function definitions and its literal source line, and git-grepped bin/ for launch-command markers - implementation-source assertions the coding guidelines now forbid. Prove the same guarantee behaviorally instead: a sandboxed copy of bin/ shows fm-spawn's launch decision follows a swapped fm-launch-lib.sh in both directions and that fm-spawn cannot take a launch decision without the library, so the launch knowledge has exactly one live owner. The byte-for-byte template pins already go through the public launch_template interface and stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esh evidence anchors
…#1288) feat(bin): add fleet launcher menu backed by a single-owner launch library
… briefs to read the marker (#9) * feat(bin): add verified pi-signed runtime adapter (kunchenguid#1145) * feat: add verified pi-signed adapter * no-mistakes(review): Correct pi-signed maintainer verification date * no-mistakes(review): Correct remaining pi-signed verification dates * no-mistakes(review): Preserve authoritative pi-signed runtime identity * no-mistakes(document): Document pi-signed shared adapter semantics * no-mistakes: apply CI fixes * fix(pi): rearm watcher across session transitions (kunchenguid#1166) * fix(pi): rearm watcher across same-process session transitions Pi emits session_shutdown for ordinary /new, /resume, and /fork replacement as well as terminal quit. The primary watcher extension latched a module-level stopping flag on every shutdown, so a replacement session in the same process could not arm monitoring until Pi restarted. Own arm authority per session generation so only the active live generation may start, stop, or rearm the child. Replacement sessions can arm again without restarting Pi, stale prior-generation callbacks cannot mutate the active cycle, and real quit still blocks late rearm. * no-mistakes(review): Preserve Pi generation isolation and exit cleanup * no-mistakes(document): Correct Pi watcher transition documentation * feat: route crew dispatch using quota-window pace (kunchenguid#1172) * Consume quota-axi pace signals in dispatch profile array selection. Add quota-array-dispatch as the single owner of the pace-aware candidate choice, keep AGENTS.md to the intake boundary and load trigger, and cover the acceptance cases with sanitized schemaVersion 3 fixtures. * no-mistakes(review): Stop and report genuine quota dispatch ties * no-mistakes(document): Document quota pace freshness and uncertainty * fix: adapt Grok Stop continuation and harden endpoint cleanup (kunchenguid#1171) * fix(grok): adapt Stop continuation to runtime capability * no-mistakes(review): Reject ambiguous Grok Stop payloads * no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions * no-mistakes(review): Enforce exact tmux cleanup selectors * no-mistakes(test): Fix historical tmux fixture and validate Grok Stop * no-mistakes: apply CI fixes * fix: restore stock macOS Bash 3.2 brief scaffolding (kunchenguid#1093) * fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2 fm-brief.sh built each Definition-of-done block and the not-enabled Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS /bin/bash) the lexer scans for the command substitution's closing `)` textually and tracks quote state through the heredoc body, so a single apostrophe, unbalanced quote, or unbalanced paren in that prose breaks parsing of the whole script. Every ship-brief scaffold (no-mistakes, direct-PR, local-only) failed with `unexpected EOF while looking for matching )`. Bash 4+ parses it fine, so the breakage stayed invisible everywhere except stock macOS. Replace all four command-substitution heredocs with `IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)` wrapper and the entire defect class regardless of future prose, and preserves the variable expansion the direct-PR and local-only bodies need. `read` keeps the heredoc's trailing newline that `$(...)` used to strip, so trim one newline to keep every generated brief byte-identical to prior output. Guard the structure, not one historical phrase: a new test rejects any heredoc nested in a command substitution anywhere in fm-brief.sh, where the old assertion pinned a single apostrophe phrase and so missed the reintroduction. Extend the stock-macOS Bash CI job from parsing one script to the whole maintained shell surface (bin/*.sh, bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file set so parse scope and lint scope cannot drift apart. * no-mistakes(review): Captain: harden Bash structure and inventory guards * no-mistakes(document): Align stock macOS Bash contributor checks * no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings * test: stabilize tmux teardown conformance baseline (kunchenguid#1209) * fix(test): pin teardown tmux baseline to historical kill selectors merge-base HEAD main collapses to HEAD after the exact-selector change lands on the default branch, so the old teardown fixture was accidentally exercising current exact targets. Resolve a content-historical permissive tmux adapter from first-parent history and force that post-squash topology inside the conformance case so main and feature branches keep the same old-vs-new contract. * no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings * docs: slim quota-array-dispatch to the pace selection core (kunchenguid#1197) Cut the runtime skill to the compact pace-aware selection procedure plus minimum owner pointers. Keep every distinct decision rule and move expanded acceptance scenarios to deterministic fixture ownership assertions. Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction). * feat(bin): inherit backend config into secondmate homes (kunchenguid#1219) * Inherit config/backend into secondmate homes with deliberate-override preservation Add backend to the shared inheritable config allowlist so launch, locked bootstrap, and config-push converge a primary pin into secondmate homes as each home local future-spawn default. Track last-inherited bytes in a private state provenance marker so deliberate per-home overrides survive present and absent primary convergence, keep --backend and FM_BACKEND stronger, and extend the existing inheritance tests plus docs and skill claims. * no-mistakes(review): Preserve equal unprovenanced backend overrides * no-mistakes(review): Preserve symlink overrides and verify spawn precedence * no-mistakes(review): Snapshot backend inheritance for consistent provenance * no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence * no-mistakes(document): Document inherited backend override preservation * fix: restore primary-authoritative backend inheritance after document regression The document step reintroduced provenance and deliberate per-home override semantics after review had simplified config/backend to plain primary-authoritative allowlist membership. Restore the primary-always-wins path: present overwrites, absent removes, no provenance marker, and docs/tests match that contract. * no-mistakes(review): Add divergent backend precedence regression fixtures * no-mistakes(document): Document backend inheritance contract * fix(pi): remove Calm's upper version ceiling (kunchenguid#1226) * fix(pi): remove Calm's exclusive Pi upper-version ceiling tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs described that range as "supported" rather than verified evidence. The Calm CHANGELOG shows no API introduced at either version, so there is no evidence for a real minimum; the presentation adapters already probe the exact method they patch rather than checking a version. Replace the allowlist with dated version evidence that never rejects a newer Pi, and make each presentation adapter degrade independently with a diagnostic if a future Pi removes its API, instead of the whole Calm extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1 through 0.82.0" phrasing to state it as verified evidence, not a ceiling. * no-mistakes(review): Probe missing Calm adapter exports safely * no-mistakes(document): Document Calm's unbounded Pi compatibility * fix(bin): allow session-local todo tools in the subagent guard (kunchenguid#1204) * fix(guard): allow session-local todo tools in the primary The delegation-shape guard denied TaskCreate and TaskUpdate because their normalized names contain the `task` stem. Those tools write only the harness's session-local todo list, which has no executor: it spawns no agent, allocates no worktree, registers no schedule, and starts nothing that outlives the session. That is not the unaccounted work the guard exists to stop, so the stem match was a false positive, and the deny text told the primary to run bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry. Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only observe or stop existing work. Both lists stay exact-name so neither can widen by substring. Tests cover the two allowed names and six near-miss names that a substring or shortened-stem widening would release; both mutations were watched red. * no-mistakes(review): drop session-local todo tools from recommended deny list * no-mistakes: apply CI fixes * fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid (kunchenguid#1206) * fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid fm_harness_ancestry_pid() previously returned the first ancestor process whose command matched a verified harness name. Claude Code's Stop hook fires as a bg-spare worker several levels below the session's actual lock-owning claude process (hook shell -> claude bg-spare -> claude bg-pty-host -> claude -> claude(lock)), so the first match was the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self() then never matched state/.lock, and the Claude Stop auto-arm silently treated its own primary session as an unrelated live owner and never armed the watcher. The walk now keeps going past a claude-named match, looking for a still more ancestral claude-named match, and stops the instant a non-match follows an already-found match (bounding it to a contiguous run rather than the literal ancestry top, so an unrelated claude-named process further up the real process tree is never mistaken for part of this session's own nested chain). Every other harness keeps the original first-match-wins behavior, since e.g. Pi's shared signed-wrapper ancestry actually holds the session at the inner engine pid, not an outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper bg-spare chain. * no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim * no-mistakes: apply CI fixes * fix: conferma l'avvio del watcher su Windows/MSYS (kunchenguid#1212) * fix: confirm watcher startup on MSYS * no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test * no-mistakes(review): validate OpenCode ready timeout, make uname cache internal * fix(spawn): forward CLAUDE_CONFIG_DIR to claude crewmates (kunchenguid#1195) * fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates Crewmate panes are created by a long-lived tmux/herdr daemon that does not inherit firstmate's current environment. When firstmate runs under a non-default CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare `claude` in the crewmate pane fell back to the default ~/.claude store and launched unauthenticated, blocking the crewmate before it could do any work. fm-spawn now prefixes the claude launch with firstmate's own resolved CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config store firstmate is authenticated with. An unset value is the single-store default and adds no prefix; non-claude harnesses are unaffected. Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set, omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test helper so launch assertions no longer depend on the developer's environment. * no-mistakes: apply CI fixes * fix: preserve dispatch identity across authentication checks (kunchenguid#1233) * fix: preserve dispatch harness identity * no-mistakes(review): Fix Grok counterfactual tuple validation * no-mistakes(document): Scope dispatch authentication to selected tuple * fix: restore dispatch instruction budget * no-mistakes(review): Scope dispatch authentication after candidate selection * fix(bin): normalize relative durable paths (kunchenguid#1256) * fix(bin): handle dash-leading harness process names (#2) * fix: handle dash-leading harness process names * no-mistakes(review): Make dash-leading harness regression hermetic * fix: preserve secondmate reply routes across relative homes Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved. Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory. * no-mistakes(review): Preserve absolute overrides and normalize relative durable paths * no-mistakes(review): Normalize relative home before deriving durable paths * no-mistakes(document): Document relative durable-path normalization * no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization * no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck * refactor(skills): make Bearings chat-only by default (kunchenguid#1136) * Add internal status skill * no-mistakes(document): register /status skill in documentation-audiences inventory * no-mistakes(lint): replace grep|wc -l with grep -c in status skill test * test: silence literal status skill patterns * Refactor bearings default to chat-only --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> * Clarify follow-up routing during validation (kunchenguid#1277) * fix: honor concrete approval for project operations (kunchenguid#1272) * docs: add captain-approved project operation exception to hard rule 1 Firstmate stays read-only over projects by default, but when the captain clearly approves a concrete project operation and scope in the moment, firstmate may perform exactly that approved operation with its own tools. The approval is never inferred, broadened, or standing, and it does not relax the existing force, discard, unlanded-work, or merge-authority boundaries. * no-mistakes(review): Clarify captain-approved project operation boundaries * no-mistakes(document): Clarify captain-approved project operation scope * docs: cover directories and preserve the operation-or-scope alternative Widen the captain-approved project operation exception in AGENTS.md to files or directories, and restore the explicit operation-or-scope alternative that a prior pipeline auto-fix had collapsed into "and". Rework project-management SKILL.md's Remove section, which previously told firstmate to refuse project removal until a guarded helper existed; that helper was never built, so the text directly contradicted the new instruction-only exception. It now points at the exception plus the existing removal preflight it still requires unchanged. Update the one instruction-owners test assertion that hard-coded the sentence removed above, so the suite tracks current, not obsolete, text. * docs: add captain-approved project operation exception to hard rule 1 Firstmate stays read-only over projects by default, but when the captain clearly approves a concrete project operation and scope in the moment, firstmate may perform exactly that approved operation with its own tools. The approval is never inferred, broadened, or standing, and it does not relax the existing force, discard, unlanded-work, or merge-authority boundaries. * no-mistakes(review): Clarify captain-approved project operation boundaries * no-mistakes(document): Clarify captain-approved project operation scope * docs: cover directories and preserve the operation-or-scope alternative Widen the captain-approved project operation exception in AGENTS.md to files or directories, and restore the explicit operation-or-scope alternative that a prior pipeline auto-fix had collapsed into "and". Rework project-management SKILL.md's Remove section, which previously told firstmate to refuse project removal until a guarded helper existed; that helper was never built, so the text directly contradicted the new instruction-only exception. It now points at the exception plus the existing removal preflight it still requires unchanged. Update the one instruction-owners test assertion that hard-coded the sentence removed above, so the suite tracks current, not obsolete, text. * no-mistakes(review): Align project removal preflight with approved exception * no-mistakes(document): Align project removal documentation with approved exception * fix: restore removal test byte-for-byte and preserve the default sentence tests/fm-instruction-owners.test.sh had been changed to assert different text; restore it byte-for-byte to origin/main. project-management SKILL.md's Remove section now keeps the exact default "Never issue a raw removal command from Firstmate." sentence that test still asserts, immediately followed by the already-approved captain-operation-or-scope exception, so the default and the exception both stay explicit and consistent. * no-mistakes(document): Align project-write boundary documentation * fix(skills): route new project intake through secondmate scopes (kunchenguid#1275) * Route project intake through secondmate scopes * no-mistakes(test): Guard all main-home project registry mutations * no-mistakes(document): Consolidate secondmate routing documentation * no-mistakes: apply CI fixes * Restore new-project routing scope * no-mistakes(document): Clarify secondmate routing for new-project intake * no-mistakes: apply CI fixes * fix: scope validation corrections by accepted behavior (kunchenguid#1281) * fix: scope validation corrections by accepted behavior * no-mistakes(review): Classify stale delivery evidence as an autonomous correction * test: replace source assertions with behavioral coverage (kunchenguid#1282) * test: remove source-content assertions * no-mistakes(review): Replace source assertions with runtime behavior coverage * no-mistakes(review): Isolate Kimi task temp runtime coverage * no-mistakes(document): Refresh test cleanup documentation * no-mistakes: apply CI fixes * fix(watch): escalate busy workers with no completed turn (kunchenguid#1286) * fix(watch): bound how long a busy pane may run with no completed turn A busy pane (backend busy state or the harness's rendered footer) was unconditional, unbounded proof of liveness in every escalation path, so a hung foreground tool call behind a busy signature could run for hours undetected (2026-07 hibit-agent-focus-nonsteal-r1 incident: a catastrophic- backtracking regex hung one bash call for 25h behind an unchanging "Working..." footer). FM_BUSY_TURN_MAX_SECS (default 3600s) now bounds how long a busy pane may run with no completed turn (state/<id>.turn-ended, or its spawn record before any turn has completed). Past the bound, busy_turn_over_age routes the pane through the existing wedge_timer_check, reusing the identical stale reason, escalation counter, and demand-deep-inspection marker for human inspection only - never an automatic interrupt, signal, or restart of the worker or its tool process. A completed turn resets the age. Reproduced end-to-end against the real installed Pi TUI: a foreground `sleep 999999` bash call with no timeout renders the actual busy footer, and two captures ~15s apart show the elapsed counter changing the pane hash while the same turn stays unfinished. Running the pre-fix watcher against the real captures showed it never starts a wedge timer no matter how long the pane stays busy; the fixed watcher starts and escalates the timer through the same mechanism, while the real hung process remained untouched and alive throughout. * no-mistakes(review): fix: parse enriched AFK stale reasons * no-mistakes(review): fix: preserve enriched wedges during AFK supervision * no-mistakes(review): fix: route all enriched AFK wedges * no-mistakes(document): Clarify busy-turn age supervision documentation * fix(gitignore): ignore config/ as a directory, not by exact filename (kunchenguid#1261) A name-by-name list of config/ entries silently stops ignoring any new or home-local file placed there, which makes the working tree read as dirty and blocks guarded sync paths that refuse to touch a dirty home. AGENTS.md already documents config/ as captain-private and gitignored as a category; this makes .gitignore match that contract. * fix(tests): replace source-content .gitignore assertion with behavioral coverage (kunchenguid#1304) The second assertion in fm-gitignore-config.test.sh (added by kunchenguid#1261) greps .gitignore for a specific spelling of the config/ ignore pattern. It fails on a semantically equivalent pattern like config/** and does not prove Git actually ignores anything, per the completed source-content-test audit. Replace it with a real git check-ignore control test on a generated unrelated path, and strengthen the existing directory-coverage test with generated unpredictable direct and nested config/ paths. * feat: bound and consolidate startup memory during stow (kunchenguid#1303) * Add bounded startup memory curation * no-mistakes(review): Record reproducible stow verification evidence * no-mistakes(review): Validate inherited secondmate stow evidence * no-mistakes(document): Document editable startup-memory budget propagation * feat(bin): mark crewmate and scout steers as from-firstmate A steer lands in the receiving agent's own chat, where nothing else told firstmate's instructions apart from a human typing into that pane. The gap was proven in both directions on 2026-07-26: the captain opened a crewmate pane believing it was firstmate and issued cross-lane instructions there, and a Pi crewmate at an ask-user gate addressed "Captain, ..." into its own pane and sat parked - nobody reads a crewmate pane, and a parked pipeline emits no wake, so that direction fails silently. AGENTS.md section 1 rule 4 already required workers to honor a distinction the system gave them no means to make. fm-send now applies the existing from-firstmate carrier to every text steer whose target resolves through this home's meta, not just kind=secondmate. A crewmate or scout carries the marker alone; the corr= correlation token and the parent pending-reply record stay secondmate-only, because a crewmate already answers on its own status file. Explicit backend targets and the --key path are unchanged. Command-shaped text is the one exclusion. A harness recognizes a slash command, or a codex $<skill> invocation, only at the very start of the composer line, so any prefix silently demotes it to prose. Verified on claude 2.1.220 and pi 0.82.0: with either marker shape prepended, /no-mistakes stops opening the completion popup entirely and would submit as ordinary text. Crewmate sends of that shape therefore stay unmarked and byte-identical, which also keeps every documented popup hazard out of this change's blast radius: the only bytes that move are plain text no harness parses specially. The exclusion deliberately does not reach a secondmate, whose marker is what creates its reply guarantee. The ship and scout scaffolds gain a "Who is speaking to you" section teaching the reader side: marked is firstmate, unmarked is a human who may believe the pane is firstmate, self-identify as a worker on this task before acting, and escalation is always the status file. AGENTS.md states the provenance principle once in rule 4; the away-mode stub and the secondmate charter keep their own distinct consequences. * no-mistakes(review): align brief's unmarked-message exception wording to fm-send predicate * no-mistakes(test): fix stale corr-less assertion in Pi/Herdr marker e2e * no-mistakes(document): generalize task-selector marker context to from-firstmate --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: Daniel Kuykendall IV <danielkuykendall23@gmail.com> Co-authored-by: Trillium Smith <Spiteless@gmail.com> Co-authored-by: Unknownzed <45267749+Unknownzed@users.noreply.github.com> Co-authored-by: lhalbert <lucashalbert@users.noreply.github.com> Co-authored-by: AG <ag@agw3.org> Co-authored-by: deeto15 <92119640+deeto15@users.noreply.github.com>
…spawn gate, and probe verification (#10) * feat(bin): enforce the zero-budget model rule at spawn and config-edit time The fleet's stated safety rule - "the budget for every API-key provider is ZERO ... this is a safety rule, not a preference" - was implemented as prose inside a JSON comment blob that no code read. It relied on the coordinator recalling it correctly at every intake and every failover, forever. That is load-bearing because one API key commonly reaches both free and metered models on the same provider, rendered identically in every catalogue listing (six columns, no cost column, no entitlement column). A single mistyped or well-meant model name is a charge. A separate incident had already shown the fleet will route from a plausible name without checking: a model was configured from a catalogue listing, never probed, and every dispatch to that tier failed at launch until an investigation found it. Add config/models.json (local, gitignored) as the enforced copy, plus the checks that read it: - fm-spawn refuses a model whose API-key provider is not on the verified-free allowlist, whose provider cost posture is unclassified, whose registry status is rejected or blocked, or whose concurrency cap is already met. The check sits at the first point where harness and model are both final and the last point before any mutation, so a refusal creates nothing. It is also the only gate that sees an explicit --model that bypassed the dispatch config, which bootstrap validation structurally cannot see. - bootstrap binds config/crew-dispatch.json to the registry, so a rule naming an unregistered, non-approved, or unprobed model fails at config-edit time. - fm-model-verify runs the entitlement probe and the price-drift comparison, interval-gated by observation level so the steady-state cost is one file read. Probes close stdin and run under a timeout; pi -p can otherwise hang unbounded, and a wedged probe on the session-start path would present to supervision as a stale session. Three axes are kept deliberately separate, because conflating any two of them is itself a failure mode: cost (can this call be billed), routability (is the account entitled to it), and availability (is it answering right now). A rate-limited model is unavailable, not demoted, so a transient outage cannot permanently degrade the routing table; availability lives in state/ and routing status in config/, written by different code. Enforcement is asymmetric about the registry's absence, by design. With no config/models.json the spawn check is inert and behavior is byte-identical to before, so nothing is forced on a home that never opted in; bootstrap then reports the unenforced state rather than leaving it silent. With the file present every unclear answer refuses - malformed JSON, an unsupported schema, an unclassified provider, a missing jq - because a broken safety file must never read as an absent one. The allowlist stores each price numerically rather than only a cost class, which is what makes a repricing detectable at all: a name-based allowlist is structurally blind to one, since the thing that makes a name safe is a number living in a catalogue the provider rewrites. Allowlist evidence must include a genuinely price-bearing source; a probe is deliberately not enough, because it proves the account gets an answer and says nothing about what that answer costs. The promotion system ships dormant behind a config flag and a named evidence instrument, so activation is a configuration and data change rather than a code change. Its authority is validated as a ceiling in each direction: Tier 4 to Tier 3 may be automatic, Tier 3 to Tier 2 needs captain confirmation, and Tier 1 and Tier 0 are never entered by accumulated evidence - Tier 1 is triggered by risk, not capability rank, and a spotless Tier 2 record demonstrates nothing about credential or destructive-operation judgment. config/models.json is inherited by secondmate homes alongside config/crew-dispatch.json and must not be separated from it: inheriting the rules without the registry would leave a secondmate's own crewmates outside enforcement and make every inherited model read as unregistered there. * no-mistakes(review): cost-gate probe paths, surface sweep stderr, fix test epoch * no-mistakes(document): docs: add models.json to inheritance allowlist and jq toolchain
Adds the third layer above routing and scheduling: whether the fleet should accept another task at all right now. It ships inert - a home with no `_scheduling.admission_control` policy sees no behavior change and pays one cheap config read. The defining constraint is task independence. Admission reads only the fleet snapshot, never the incoming task, so the same snapshot returns the same band for every task; `bin/fm-admission.sh` enforces that structurally by refusing a task argument. Anything that varies per task stays in routing or scheduling. - `bin/fm-admission-lib.sh` is the single owner of the executable schema check, shared by bootstrap's startup diagnostic and the evaluator so the two cannot drift on the same config bytes. Unknown fields are refused rather than ignored, so a typo cannot silently disable a safety condition, and every rule from the accepted design refuses with an actionable reason. - `bin/fm-admission.sh` composes the existing read-only fleet snapshot into named signals, each with its own validity, and combines them into a preferred/soft/hard/unknown band. Every rule names the observed value, its source and freshness, the exact JSON config path, the configured value, and the resulting band. Exit status is the band, so a caller that ignores the output still stops safely. - Backlog consistency is a signal separate from worker-census integrity. A backlog row that contradicts task metadata is a bookkeeping fault to repair, not evidence of physical saturation; one aggregate health bit would close the fleet for the wrong reason. - The existing per-home session lock is the single-primary admission authority. No new process, daemon, reservation store, or second queue: deferred and refused requests stay in the owning backlog under a `load` hold, and capacity is re-examined at the two existing seams, successful cleanup and session start. - Nothing numeric enforces. Only the deterministic safety conditions - authority, census integrity, snapshot freshness - can set a band, and the schema refuses a configuration that tries to enable a threshold whose predictive value is unmeasured. Active workers, load-hold depth, and worker breakdown are recorded as observations with no cap. - Signals with no collector are recorded as unmeasured rather than assumed to be zero, and admission wait age stays explicitly uncollected because backlog age is task age. - The decision record is the named extension seam for the wake-outcome ledger, which does not expose one yet; nothing is persisted and no competing evidence store is opened. Dormant distributed machinery (reservations, remote nodes, a second intake authority) is settled as a validated schema contract rather than running code, so activating it later cannot change admission's semantics. `tests/fm-gotmp.test.sh` gains the new teardown dependency in its fake root, matching how its other sourced libs are already linked.
…n band and trigger validation
…verbose-fact claim
test_teardown_conformance_old_vs_new pins BASE_REF=HEAD when building the old-bin fixture, so its "old" fm-teardown.sh is HEAD's teardown, which now sources fm-admission-lib.sh. The lib was missing from OLD_BIN_UNCHANGED_SIBLINGS, so the old teardown aborted at source time. Mirror teardown's real dependency set, exactly like its other sourced libs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sbracewell64
force-pushed
the
fm/admission-control-stage01
branch
from
July 31, 2026 16:51
9c61437 to
70c9a52
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Implement fleet admission control Stages 0 and 1 exactly as designed by an accepted, captain-authorized scout study. The design was reviewed and approved before this work started; the task was explicitly to implement it as written, not to redesign it, so departures from the study are out of scope rather than improvements.
Admission control is a third layer above routing (who is capable) and scheduling (when accepted work runs): it decides whether the fleet should accept another task at all right now. Its defining constraint, and the reason the layer exists separately at all, is task independence - it reads ONLY fleet-snapshot properties and never the incoming task, so the same snapshot returns the same band for every task. bin/fm-admission.sh therefore refuses a task argument on purpose; that refusal is the invariant made structural, not missing argument handling.
DELIBERATE NON-ENFORCEMENT, the single most likely thing to look like an omission. The study found no measured evidence that any numeric signal predicts fleet coordination failure: eleven active workers ran on a lightly loaded host, and coordinator attention cost is not collectable until a wake-outcome ledger lands. Shipping a cap would manufacture policy from one point sample, and the repo already has a standing contract that isolated work dispatches immediately with NO concurrency cap. So every numeric threshold ships null with enforce:false, and enforcement_mode accepts exactly one value ('safety-only') which forbids enforce:true on any signal. Numeric enforcement is structurally unreachable until Stage 2 adds a mode value and the comparison path together as one reviewed change. Only deterministic safety conditions - admission authority, census integrity, snapshot freshness - can set a band. Threshold-comparison code being absent is the requirement, not an unfinished feature.
INERT BY DEFAULT. A home with no _scheduling.admission_control policy, or one carrying only underscore-prefixed operator notes, sees zero behavior change and pays one cheap config read. All three consuming seams (bootstrap, teardown, session start) are gated on an active policy. The live captain config today holds only a note, so this lands as a no-op for the fleet.
SIGNAL-SPECIFIC VALIDITY. The study observed a real production case where the broad snapshot validity bit was false (a historical backlog row had no child metadata) while task metadata gave a perfectly coherent eleven-worker census. Collapsing those into one aggregate health bit would close the fleet for an unrelated bookkeeping error. So backlog_consistency is its own signal, separate from census_integrity, reported and repairable but never contributing a capacity band. It is a seventh signal not present in the study's section 4 schema block, added because the study's own section 1.3 and 9.3 findings mandate the separation; the brief listed it as in scope.
NO NEW MACHINERY. The existing per-home session lock is the single-primary admission authority - no new process, daemon, reservation store, or second queue. Deferred and refused requests stay in the owning backlog under a 'load' hold, and capacity is re-examined at exactly two seams that already exist: successful cleanup and session start. The known already-empty-fleet gap (a fleet that empties while a request is held waits until session start) is deliberately left named and instrumented rather than cured with a timer; that restraint is the study's explicit recommendation.
DORMANT, NOT MISSING. Reservations, a distributed registry, remote nodes, host-resource metrics, and an admission daemon are all deliberately unimplemented. The schema validates their config placeholders and REFUSES to let them be enabled, so the distributed contract is settled in advance as validated policy rather than running code, and activating it later cannot change admission semantics. A task-weighted admission score, provider quota inside the admission band, and an admission daemon are permanently rejected designs.
LEDGER SEAM ONLY. The wake-outcome ledger is being built in parallel and exposes no extension seam yet (verified: nothing in tracked material). The decision record from --json is the named unit of admission telemetry, but nothing is persisted and no competing evidence store is opened - integration is explicitly the ledger's Phase 2, not this task's.
OTHER DELIBERATE CHOICES a reviewer may question: exit status encodes the band (0 preferred, 3 soft, 4 hard, 2 malformed policy) so a caller that ignores stdout still stops safely. Unknown config fields are REFUSED rather than ignored, because the study's central complaint was that bootstrap silently ignored unknown scheduling keys; a typo must not silently disable a safety condition. unknown_band accepts only soft or hard because the study's invariant says missing evidence must never resolve to 'probably fine'. Signals with no collector are recorded as unmeasured rather than assumed zero, and admission wait age stays explicitly uncollected because backlog age is task age, not load-wait age.
This repo is firstmate's own shared tracked material, so knowledge was routed by its documented placement rules: AGENTS.md got only a three-line intake stub plus a skill trigger (its token cost is paid by every session of every fleet member), the per-band procedure went to a new agent-only skill, the schema to docs/configuration.md as its single owner, and rationale to docs/architecture.md. Captain-private config values were deliberately kept out of tracked material - only the schema, its validation, and an all-null example ship.
tests/fm-gotmp.test.sh gains two lines because teardown now sources a new lib and that suite's fake root must mirror teardown's real dependency set, exactly as its other sourced libs are already linked. That is a required fixture completeness fix, not unrelated scope.
Verified before submitting: shellcheck lint clean, doc-audience check clean, 14 new behavior cases green, and the pr-forge, session-bootstrap, pure-contract-unit and backend-dispatch families run. Three failures remain that this change did not cause - fm-session-start, fm-calm-pi-extension (node ESM), and fm-backend-tmux-smoke - each confirmed to fail identically on a stashed baseline and environmental to this machine.
What Changed
bin/fm-admission.shandbin/fm-admission-lib.sh: a task-independent admission layer that reads only fleet-snapshot properties and resolves apreferred/soft/hardband (exit codes 0/3/4, with 2 for malformed policy). All numeric thresholds ship null under asafety-onlyenforcement mode that structurally refusesenforce:true; only deterministic safety conditions (admission authority, census integrity, snapshot freshness) can set a band, unknown config keys are refused rather than ignored, and a--jsondecision record serves as the telemetry unit. Per review feedback, an unmeasurable snapshot age now fails closed to the configuredunknown_bandinstead of resolvingpreferred, and band/trigger validation was tightened (bands.preferredrefuseshold_kind/auto_reconsider;queue.release_triggersis pinned to exactly its two seams).bin/fm-bootstrap.sh,bin/fm-teardown.sh, andbin/fm-session-start.sh— all gated on an active_scheduling.admission_controlpolicy, so a home with no policy (or only underscore-prefixed operator notes) sees zero behavior change.docs/configuration.md, rationale todocs/architecture.md, a per-bandfleet-admissionagent skill plus a three-line AGENTS.md intake stub, an all-null example indocs/examples/crew-dispatch.json, andtests/fm-admission.test.shwith 15 behavior cases (all green in the pipeline, alongside the bootstrap and gotmp regression suites).Risk Assessment
✅ Low: The feature remains additive and inert-by-default, all four review fixes were applied exactly as instructed and verified empirically against the shipped example and live script, and the fix diff introduces no new behavior beyond tightening validation and failing closed on unmeasurable freshness evidence.
Testing
Ran the new fm-admission behavior suite plus the gotmp and bootstrap suites that cover the consuming seams (all pass), then manually drove bin/fm-admission.sh through 11 end-user scenarios in a fixture home covering every intent constraint — inertness, task independence, band exit codes, authority, staleness fail-closed, signal separation, and safety-only schema refusals — captured as a CLI transcript; no visual evidence applies since the surface is a CLI.
Evidence: fm-admission.sh end-to-end CLI transcript (11 scenarios)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-admission.sh:262- Unmeasurable snapshot age bypasses a configured freshness limit.$stale(bin/fm-admission.sh:198) requires$snapshot_age != null, so a readable snapshot whosegeneratedfield is missing or unparseable (reachable via the public--snapshotflag, or on a host where bothdate -j -fanddate -dparsing fail) yields age null and thecensus_integrity.snapshot_agerule resolvespreferredeven whenmax_snapshot_age_secondsis configured (verified empirically: rule records valid:false, configured_value:10, result:"preferred"). Snapshot freshness is one of the three deterministic safety conditions the intent says may set a band, and the intent states "missing evidence must never resolve to 'probably fine'"; a null age with a configured non-null limit should arguably map to the census unknown_band instead of preferred. No shipped default is affected (the limit ships null), but once an operator configures a limit, this silently disables the freshness safety condition on exactly the inputs where it matters.bin/fm-admission-lib.sh:108- Validation accepts an arbitraryhold_kind(andauto_reconsider) on thepreferredband: thehold_kind must be "load"check is skipped for preferred whilehold_kindremains an allowed key, sobands.preferred.hold_kind: "captain"validates (verified against the shipped example). Runtime impact is nil since preferred admits, but the change's own rationale is that misleading configuration must be refused rather than accepted; tightening band_error to refuse hold_kind/auto_reconsider on preferred (or require hold_kind absent there) closes the gap.bin/fm-admission-lib.sh:217-queue.release_triggersis validated as a configurable non-empty subset of ["teardown","session-start"] but no consuming seam reads it: bin/fm-teardown.sh's admission_release_reminder and bin/fm-session-start.sh's Fleet admission subsection both fire unconditionally for any active policy, and the value is only echoed into the decision record. An operator who configures ["session-start"] only still gets the teardown reminder. Either pin validation to exactly the two triggers (matching docs/configuration.md's "pins ... the two release triggers" wording) or honor the configured subset at the seams.docs/architecture.md:160- docs/architecture.md describes combining signals "into apreferred,soft,hard, orunknownband", but nounknownband exists anywhere in the implementation: severity_order is exactly ["preferred","soft","hard"], unknown evidence maps to soft/hard viaunknown_band, and the exit-code contract has no fourth band. The doc sentence contradicts the schema it points to.bin/fm-admission-lib.sh:310- Missing-jq semantics are asymmetric: fm_admission_state reportsabsentwhen jq is missing (bin/fm-admission-lib.sh:310-313), so an ACTIVE policy's session-start and teardown seams silently skip, while fm-admission.sh fails closed with exit 2 and the message "admission policy invalid ... jq is required" even for a dispatch-only crew-dispatch.json that carries no admission block at all. Both directions are defensible - a policy's presence cannot be determined without a JSON parser, and bootstrap's existingMISSING: jqdiagnostic surfaces the root cause - so this is a noted tradeoff, not a requested change.🔧 Fix: fail closed on unmeasurable snapshot age; tighten band and trigger validation
1 info still open:
bin/fm-admission.sh:263- All four accepted round-1 findings are correctly fixed in fad40be and verified empirically: the freshness rule fails closed to the configured unknown_band when max_snapshot_age_seconds is set and the snapshot age is unmeasurable (valid:false preserved, controlling rule named, both directions plus the configured-band variant pinned by the new test); queue.release_triggers is validated as exactly ["teardown","session-start"] with the seams left unconditional and the doc updated; bands.preferred refuses hold_kind and auto_reconsider while still allowing underscore operator notes, with the shipped example still validating; and docs/architecture.md now describes the three-band set with unknown evidence mapping through unknown_band. No new defects introduced by the fix diff.✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-admission.test.sh tests/fm-gotmp.test.sh— 15 admission behavior cases + 4 gotmp cases, all greenbin/fm-test-run.sh tests/fm-bootstrap.test.sh— bootstrap seam regression, greenManual end-to-end CLI walkthrough ofbin/fm-admission.shin a fixture home: absent policy (exit 0, unchanged), note-only policy (inert), active policy with held lock and fresh census (human explanation, preferred/exit 0),--jsondecision record (task_id null, safety-only mode, backlog contradiction observed but band preferred, ledger seam integrated:false),--briefone-liner, lock not held (hard/exit 4), stale snapshot with unknown_band=soft (soft/exit 3), task argument refused (exit 2),validaterefusing enforce:true, a typo'd config key, and unknown_band=preferred (each exit 2 with actionable reason)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.