Repository navigation
Harness thinking on the Spark engines, the roadmap's commit→verify→publish phase, and the round-strip dashboard - #10443
Merged
Merged
Conversation
… dispatch toolchain off the host Two changes, both prerequisites for dispatching from a machine that is not srv1. The fleet hardware-standing and capacity panels were composed into the daily workspace's main element, so the surface an operator opens to decide WHAT TO LAUNCH led with facts that change no launch decision. They move to their own GET /fleet page. Both panels are pure Fragment producers over their own authorities, so the new page composes the same two calls the workspace used to make and neither panel's authority moves. The header gains one link per served page so the split does not make them unreachable. test.claim.fleet_capacity_panel_witness_test follows the panel rather than being deleted: its claim was always "the panel reaches the rendered page the operator sees", never "the panel is on the daily workspace". Re-pointing its emission at roadmap_fleet_page preserves the discriminating property -- remove the panel call from that page and every positive row goes red. Second, dashboard_execution_environment fused the capability SET with one machine's paths, so every executable was a Linux path by construction and a second host could only be served by copying twelve rows. The capability list, identity probes and ensure policies are now invariant and the paths are a DashboardToolchain parameter. srv1_linux_toolchain reproduces the previous values exactly; macbook_local_toolchain was read off the machine with `which` and each identity probe executed rather than assumed. The GNU identity strings turn out to be load-bearing on both hosts: the attempt-pointer argv passes `ln -sfnT` and -T does not exist in BSD ln, so on a Mac the system /bin/ln fails the coreutils identity probe at preflight instead of failing the flag mid-attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…ision ref a per-instance fact
Three blockers stood between a locally served workspace and a dispatch that
could even be evaluated. Two are cleared here and the third is reshaped rather
than removed.
The instance. dashboard_instance_for_repo_root resolved exactly one root, so a
locally served page reported instance srv1-live, host srv1, repo
/opt/gunbc/gunbc while running on a laptop. macbook_local_dashboard_instance
declares the machine. Four fields differ from srv1-live for stated reasons:
instance-owned dispatch state (so worktrees do not scatter through the
operator's working checkout), a named tmux server (so the operator's own shells
are not indistinguishable from dispatch sessions to the reaper), a loopback
listen host (an actuating dispatch surface on a laptop is a different exposure
from a LAN dashboard), and local provisioning. Selection is by serve entry
function, the shape the module already declares and srv1_lab already follows.
The toolchain. /sessions.json refused with "tmux not found" although tmux is
installed -- the only declared instance carried /usr/bin/tmux, and this machine
has it under Homebrew. With macbook_local_toolchain bound, that route now
observes: {"observe_refused": false, "sessions": []}.
The revision gate. It compared HEAD against refs/fleet/desired on origin, which
is the right authority for a fleet host and the wrong question for a
development checkout whose HEAD is ahead by construction. Exempting local
instances would be the escape hatch DESIGN 5 names -- a toggle whose only
effect is to proceed as if the refusal had not fired -- so the gate keeps its
exact form and only the NAMED REF varies. admitted_revision is a required field
on HostDashboardInstance, so no instance can exist without answering which
authority admits its revisions; the compiler refused every construction site
until each did, which is how the census was taken.
belt_tick_execution_standing_for_instance is moved with it. Its own annotation
said it makes the same read the dispatch preflight makes, and leaving it on the
fleet ref would have had a tick receipt report a revision the gate never
admitted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…route mkdir through the toolchain The launch gates are ordered and the transition gate sits second: it reads transition-inhibition.json under the instance root, and a root that does not exist makes the ABSENCE of that file unestablished rather than established. So it refused, correctly, and no later gate was ever reached. Provisioning the roots is a prerequisite of evaluating any launch at all. dashboard_instance_ensure_dispatch_roots is a FILTER over the instance's own dashboard_instance_provision_plan, executed through the same dashboard_apply_provision_op the full apply runs -- not a second list of which roots an instance owns, and not a hand-authored mkdir, which would have been out-of-band actuation that drifts from the plan the moment a root is added. The steps the full apply also performs -- serve binary, source staging, systemd unit -- are excluded because the last has no macOS realization at all; running the whole apply here would refuse at that step having already done work. The first wet run then refused with `env: /usr/bin/mkdir: No such file or directory`, which is the same class the toolchain split was already about: one more Linux literal, this time in the provisioning path rather than the dispatch path. It cannot come from the capability set, because that set answers "what may a DISPATCH run" and is checked by a preflight that has not happened yet at root-creation time. So HostDashboardInstance carries its toolchain, and the two mkdir sites take the program from it. Recorded rather than hidden: execution_environment is derived from that same toolchain at each constructor, so the two can be made to disagree. None do, and each names one toolchain twice in adjacent lines. The structural fix is for execution_environment to become a function of the toolchain rather than a field, which is a 37-site repoint and is named as this residue's dissolution trigger. Wet result: three roots created, exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
Parameterizing the admitting source left two cause strings behind that still
spelled the fleet's answer. The first wet refusal on the local instance read
"origin advertises no refs/fleet/desired" when what had actually been asked was
remote "." for refs/gunbc/local-admitted -- a true refusal carrying a false
reason, which is worse than a bare one because it sends the reader to a remote
and a ref that were never consulted.
Both causes now name the remote and ref the observation used. Found by running
the refusal rather than by reading the diff.
Wet discriminating pair on macbook-local, both executed:
no ref -> 503 deployed_revision_unobserved,
"remote . advertises no refs/gunbc/local-admitted"
update-ref, HEAD -> revision gate passes; admission reaches provider
selection carrying instance_id macbook-local and the
exact admitted revision
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…un found The dispatch that had passed every launch gate refused at selection with "no provider offer is selection-eligible". The cause was not a missing harness: codex_inventory_offers_for_instance answers [] when the instance's provider executable is Absent, and macbook-local carried `executable: none`. A diagnostic that points at SELECTION and a cause that lives in the INSTANCE is a long walk, so the row says so. The binding is expected to be short-lived -- DCH-3 makes the dedicated harness the default selection and DCH-4 retires the vendor runtimes -- and it earns its place meanwhile by exercising the belt path on this machine: worktree, attempt record, named-server tmux session, verification, publication, none of which has ever run on macOS. Proving that against a provider known to work is what makes a later harness failure attributable to the harness. Two more host facts surfaced by running it rather than reading it: /usr/bin/test does not exist on macOS, so readiness refused at the provider-state-root stage with `env: /usr/bin/test: No such file or directory`. Same class as the mkdir literal, same fix: the toolchain carries it. The provider state root was derived from codex_roadmap_account_selection, which is srv1's LABELLED account -- ~/.codex@gunbc-roadmap. This machine's Codex is authenticated under the plain default root, so readiness refused against a path that does not exist, for a reason that reads as a missing login rather than as the wrong account. The account selection is now a per-instance fact and both roots still come from codex_account_state_root. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…a Spark Post a request; while the response stops on tool use, execute the tools and post the results back. Four modules, no vendor runtime, no ctrl. harness_wire is the codec DCH-1's shape did not carry. extdeps.llm. anthropic_messages_api owns what a request and response ARE and has no encoder or decoder, so nothing in the corpus could turn one of its values into bytes. The codec is authored in gunbc rather than extdeps because a codec is a realization of the shape, the same split provider_interface_binding already draws between an interface and its transport. A wire stop reason decodes totally or refuses. The tempting `_ => EndTurn` is the absorbing fallback in its most expensive position: an unrecognized terminal state would become the one value the harness reads as "the model finished". Streaming is deliberately not decoded -- DCH's streaming hypothesis still has no observation behind it, and encoding a decoder for it would build on an unmeasured claim. harness_tool carries the four-tool surface and the six dispositions DCH-2 owes, under one rule: a tool's contract is carried in its declared shape, and any limit it enforces is reported in its result rather than inferred from a truncated or missing one. Duration is real rather than nominal -- GNU timeout's documented 124 exit is what makes "the harness killed this" decidable rather than guessed from a nonzero exit, executed against gtimeout 9.11 and confirmed. Only run_command is realized; the other three refuse by name and are NOT advertised to the model, because a tool advertised but unimplemented refuses at the moment of use, having already spent the turn. harness_turn is the loop. Its budget is an argument that strictly decreases and exhaustion is a named terminal, because an unbounded "while tool_use" is a program whose termination depends on the remote model's behaviour. There is no TurnSucceeded constructor: a terminal is a provider observation or a harness refusal, never a claim the task was done. That is structural rather than cautious -- on this transport at least two upstream causes produce the same end_turn, and DCH-0p reproduced the second one. WET RESULT, srv5, deepseek-v4-gguf:no-h3-stop: round 1 POST 997 bytes -> 28s cold -> stop_reason tool_use tool executed via gunbc.WitnessBin.Run in 28ms round 2 POST 1507 bytes -> 3s -> end_turn steps=2 input_tokens=521 output_tokens=38 answer: "There are 4 files ending in .dag inside dag/gunbc/harness" -- correct, and unanswerable without the tool round, which is why that prompt was chosen. Two defects the wet runs found, both fixed here rather than reasoned about: the model reference was cast straight to String and sent as the record's rendering, which the backend refused with `invalid model name`; the fix cites extdeps.ollama.api's wire projection rather than rebuilding name + ":" + tag. And the first probe went to srv6, which had been answering earlier the same session and had since stopped -- nothing observes which backend is serving before choosing one, and the row now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…emon class that hid it Operator direction: use the second Spark pair and deepseek-v4-flash. Four probes failed before the reason was found, and the reason is worth more than the fix. .232 and .233 answer Ollama on its default port with the same version string and the same 200s as .225/.226 -- and an EMPTY store, because on those two hosts Ollama is not what serves. A llama-server pair sits behind a router on .188:3738 holding deepseek-v4-flash split across both machines. The Ollama surface there is live, truthful, and entirely beside the point, which is the worst shape a wrong probe can have: it answers, so nothing refuses, and the emptiness reads as "this host has no models" rather than as "you are asking the wrong daemon". A not_found_error naming the model was the only signal, and it pointed at the model rather than at the surface. The port becomes a parameter for the same reason the address already was: 11434 is Ollama's and 3738 is the router's, and a helper that could spell only one made the second stack unreachable without an edit rather than a row. The router's model identity is NOT gunbc.spark.serving_desired's desired model. That authority governs the Ollama serving unit's single converged member; this is a different serving realization with its own identity, residency and lifecycle, so citing the spark row here would be one name over two serving stacks. The router's model is declared beside the router, and the ollama_model_ref_wire citation stays correct for the surface it names. DCH-1's shared-standard modelling paid here and it is worth naming: the router answers /v1/messages with Anthropic-shaped content blocks, a stop_reason and a usage object, so a second implementation of the wire reused the decoder with no second decoder written -- including the thinking block, which this model emits and the codec already carried. WET RESULT, router .188:3738, deepseek-v4-flash:iq3s-split: round 1 POST 998 bytes -> 28s -> stop_reason tool_use tool executed via gunbc.WitnessBin.Run in 24ms round 2 POST 1647 bytes -> 17s -> end_turn steps=2 input_tokens=184 output_tokens=52 the answer names all four .dag files by name, which the model had no way to know without the tool round. Also fixed: an annotation had been written inside a declaration body, which DESIGN section 4c refuses structurally rather than by review -- the compiler named all five lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…selection The arity objection was right and it decided the shape. "Which vendor" is forked across six parallel coproducts -- ProviderOffer, ResolvedProviderSelection, DispatchCredentialBindingIdentity, DispatchModelIdentity, DispatchProviderControl and DispatchCliProvider -- so a fourth vendor is not one arm but six plus every match site. That arity is the tell that those axes are realization facts fused into a selection interface. ProviderKind is also the wrong home: it lives in extdeps.llm.cli_lifecycle and enumerates three EXTERNAL vendor CLIs, and DESIGN's external-upstream decomposition is explicit that a generic hub may not enumerate concrete products. The harness has no vendor, no account, no entitlement and no rate limit, so widening that enum would put it into machinery whose every input it would have to fabricate. So the arm goes on DispatchCliProvider, which is already gunbc's own "who executes this attempt" axis at the right layer. The vendor machinery is untouched and still serves the three vendor arms; DCH-4's deletion collapses this type toward one arm rather than unpicking a fourth copy of a fork. Three places had to stop assuming executor implies vendor, and each refuses rather than fabricating. dispatch_cli_to_provider_kind returns a coproduct, because asking which vendor kind the harness is has no answer. dispatch_provider_instance_for narrows its domain to a ProviderKind, so asking it about the harness is unwritable rather than wrong -- every call site in the tree already passed a literal vendor. dispatch_provider_instance answers Absent, which this Optional already meant. The spawn needs no runtime placement: the worker is our own binary running a .dag entry out of the attempt's checkout, with the worktree and brief passed as --arg. The belt's annotation claiming `gunbc run` cannot pass arguments is stale -- measured against `gunbc run --help` -- and is left for its own repair rather than propagated into new code. Selection is the harness by default and REFUSES rather than falling back. Reaching for a vendor on a not-serving backend would be the absorbing fallback DESIGN section 5 names, and it would be fiction anyway: vendor offers are built with an unobserved standing while eligibility requires an observed one, so there is no fallback to lose. The harness has no such gap because its question is answerable by us -- is the backend serving the model we will name -- and harness_observe_backend answers it in four states, parsing the model list rather than substring-matching a body that could be an error document quoting the name back. Wet: harness_observe_backend returns serving, advertising deepseek-v4-flash:iq3s-split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…re-deriving it The dispatch receipt reported provider gunbc-harness from dispatch_actuator_selection while the spawn refused with the vendor selector's 'no provider offer is selection-eligible' -- one route reporting another route's refusal. Two answers to 'who executes this attempt' had been reconciled only by both happening to say Codex, and became visible the moment they could disagree. dispatch_spawn_commands_for_resolved_instance already existed for exactly this: it takes the resolved executor and skips the inventory. The selection is now made once and the spawn consumes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…e it edits
First wet dispatch through the belt reached the harness and died:
`entry file does not exist or is not a file: dag/gunbc/harness/harness_cli.dag`.
The argv passed RELATIVE source roots, so the worker resolved its own modules
out of the attempt's checkout -- and a worktree is branched from the base ref,
which does not carry the harness.
Repointing the base ref would have made that run work and left the defect in
place, because the defect is not about which ref. A harness that must exist
inside the tree it is editing cannot be used to work ON the harness (an attempt
could break its own worker mid-turn), cannot start from any base ref predating
it, and would silently change VERSION with the subject under edit.
So the entry and source roots are now absolute under the instance's repo_root
-- the controller's own checkout, fixed for the process's life -- while the
worktree reaches the worker as the argument that sets its tools' working
directory. Tool and subject, named separately.
The instance-less spawn path refuses rather than guessing a checkout: only a
dashboard instance names a repo_root, and inventing one would reintroduce the
same conflation from the other side.
Progress this lands, all wet against macbook-local:
POST /dispatch -> 200 {"status":"spawned","provider":"gunbc-harness"}
tmux session created on the instance's named server
attempt worktree created from the base ref
worker process started, then refused as above -- which is what this fixes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xk77WXo8jGi8Ura8fEbZA7
…ne, stop answering idle sockets with 400 Three findings, each measured on srv2 rather than reasoned: 1. The harness worker was macOS-pinned through its TOOL, not just its probe. harness_worker_config took the worktree as a parameter and then filled timeout_program from harness_macbook_timeout_program, so a worker on any Linux host refused at its first tool call against /opt/homebrew/bin/gtimeout. The toolchain is a parameter, not a host fact: DashboardToolchain gains timeout, both host rows answer it, and the actuator supplies instance.toolchain.timeout. Proven wet on srv2 -- WitnessBin.Run executed in 7ms through /usr/bin/timeout, which returns exactly the documented 124 on kill. 2. Dispatch against srv2_lab refused with deployed_revision_unobserved because that lab is fed from its own origin.git bundle, which advertises no refs/fleet/desired. The refusal is correct and the lab is isolated by construction, so the fix is a deployment instance rooted at a dedicated clone with its own admitting ref, not a hand-pushed ref into provisioned state. 3. serve answered idle connections with 400. set_read_timeout(10s) then mapped every read error, WouldBlock included, to "bad request", so a browser's speculative preconnect sockets collected "Resource temporarily unavailable (os error 11)" after 10s. "The client sent something malformed" and "the client has not spoken yet" are different facts; only the first is a 400. A timeout with zero bytes read, and a clean EOF at a request boundary, now close silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HKiBEVmrsXx2shGkrKsjdg
…pt fact The first wet dispatch on srv2-deploy spawned and the pane exited 101. provider-events.jsonl carried the cause: repo_relative_path_normalized: path /home/briansrls/gunbc-srv2-deploy/dag is not under process workspace root .../attempts/shell-typed-invocation-<key> Two correct designs collided. gunbc_harness_spawn_argv resolves the worker's own modules from ABSOLUTE source roots under the controller's repo_root -- the tool/subject separation its own annotation draws, and the reason the previous commit exists. But the pane was created with working_dir: worktree_path for every provider, and `gunbc run` takes its process workspace root from the cwd, so repo_relative_path_normalized rejected the controller dag/ the worker had been pointed at on purpose. A vendor CLI is TOLD the attempt by being stood in it, so for claude and codex the pane cwd is the subject and stays the worktree. The harness is told the same thing as an argument (--arg worktree=), so its pane belongs in the controller checkout. dispatch_pane_working_directory makes that a per-provider answer with no default arm, so a new provider must state which it is. Everything else about the failed attempt was already correct -- worktree, branch, state directory, current-pointer -- which is why every receipt looked right while nothing ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HKiBEVmrsXx2shGkrKsjdg
…unit instead of a pane
tmux holds a dispatched attempt because claude and codex are interactive
terminal programs and need a pty. That is a fact about those two providers, not
about dispatch, and the gunbc harness -- whose worker is `gunbc run`, a
non-interactive batch process -- was paying for it twice:
- tmux puts every pane in its own transient scope (tmux-spawn-<uuid>.scope,
memory.max=max), so the worker escaped the serving unit's cgroup and
`gunbc run` refused to plan at all: HostBudgetUnreadable, reproduced on
srv2-deploy on two consecutive dispatches.
- pane capture needs a shell-command payload, which extdeps.tmux's own
annotation names as the one foreign-executor seam in attempt capture.
DispatchSessionContainer makes the container a per-provider choice with no
default arm. A transient systemd unit supplies the same four properties a
container owes an attempt -- outlives the request, captures output, observable,
killable -- with no pty, and supplies as ordinary unit properties the two things
the pane could not express: MemoryMax, which makes the budget enforceable BY
CONSTRUCTION rather than by a check that fails, and Result plus ExecMainStatus,
which is the terminal state that separates a finished worker from a crashed one.
Three deliberate choices:
Units are named per ATTEMPT, not per node. Attempt keys are content hashes, so
the `duplicate session` collision -- where a dead pane blocks the next dispatch
and nothing reaps it -- has no spelling here.
No --collect. It unloads the unit the moment it exits, discarding exactly the
terminal state a transient unit is started in order to read afterwards. A unit
lingering in `failed` is evidence; a collected one is an absence
indistinguishable from never having run.
The harness arm REFUSES where a toolchain declares no systemd-run rather than
falling back to tmux. tmux is precisely where this worker cannot get an
enforceable bound, so the fallback would be a guaranteed HostBudgetUnreadable
wearing the shape of a spawn that worked -- the absorbing fallback DESIGN
section 5 forbids. macbook_local_toolchain answers `none` because macOS has no
systemd, which is why the field is optional rather than a path every toolchain
must invent.
Measured on srv2-deploy: the worker's cgroup is its own attempt unit with
memory.max=17179869184, WorkingDirectory is the controller checkout, output
appends to the attempt's events path, and no tmux server is running.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
… scratch per attempt Two changes, both aimed at making concurrency expressible rather than assumed. THE CEILING WAS A SECOND SPELLING OF AN AUTHORITY. dispatch_worker_memory_ceiling was authored here as byte_size(count: 17179869184) with a hand justification about observed peaks. That number is EXACTLY gunbc_runner_slot_desired().memory_max, which is not a coincidence to note but an authority to consume: that row carries an operator ruling derived from 186 floor-job cgroup peaks, a superseded prior derivation retained as method, and its own dissolve-on -- none of which a literal inherits and all of which it would silently contradict the moment the ruling moves. The reuse is on PROVEN COINCIDENCE, not a matching number. The slot ruling measured floor-job cgroup peaks; a floor job is a gunbc process resolving this corpus, which is the harness worker's workload shape. Two concepts that merely shared a number would have had to stay separate; these share the measurement. THE REQUEST SCRATCH PATH WAS ONE FIXED FILE FOR EVERY WORKER ON A HOST. harness_request_scratch_path is /tmp/gunbc-harness-request.json, written once per turn by every worker. That is invisible while exactly one worker runs and is silent corruption the moment two do: one worker's request body overwritten by another between the Filesystem.Write and the POST that reads it back. Nothing refuses -- the model answers the wrong prompt. It is now derived from the attempt's own state directory and passed as an argument, which removes the collision and makes the scratch LOCATION a parameter rather than an assumption. Measured on srv2-deploy with two workers running concurrently in distinct attempt units: each carries MemoryMax=17179869184 and its own attempt-state/<node>-a<key>/harness-request.json. WHERE THIS IS POINTED, AND WHAT IS DELIBERATELY NOT BUILT. A per-attempt ceiling is half of a concurrency question whose other half already exists: gunbc.runner_slot_allocation derives memory, cpu and disk admitted widths and commits the MINIMUM over those axes -- the most concurrent fraction a host admits -- and gunbc.fleet_host_budget expresses shares as fractions over a measure and charges control-plane residency against what a host may commit. Dispatch consumes none of it: nothing asks how many attempts this host admits before starting one, so concurrency is bounded by whoever is clicking rather than by the budget. That is recorded as this row's dissolution trigger rather than implied to be solved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…ad the unit, name the right attempt Four defects, each hiding the next, all surfaced by dispatching and looking. 1. THE EVENT STREAM WAS FED THE WRONG CONTENT. provider-events.jsonl carries a declared schema that gunbc.roadmap_provider_events reads through jq with fromjson. The systemd container pointed the worker's PROGRESS output at it, so jq exited 5 on the first line and the whole attempt's agent evidence refused as unreadable. The refusal was correct and named file, line and column; the producer was wrong. Worker stdout now has its own worker-stdout.log. 2. NOTHING EMITTED A TERMINAL EVENT. The harness now emits the existing schema itself rather than having its liveness reconstructed from outside: turn.started before the first POST, item.started/item.completed per tool round, and turn.completed or turn.failed at the end. Only the turn knows whether it stopped having COMPLETED or having run out of steps; an observer of a vanished process cannot make that distinction whatever container holds it. The file is rewritten from the fold's own accumulator each step, so progress is visible DURING the turn -- which a block-buffered stdout capture cannot offer. 3. NOTHING OBSERVED THE PROCESS. The Agent obligation deliberately needs BOTH a terminal event and a compatible exit -- it refuses a turn.completed whose process exited nonzero as conflicting evidence -- so emission alone was never sufficient. A --user scoped systemctl show supplies the second source. LoadState is read FIRST because it is the only honest absence: a unit the manager never heard of answers ActiveState `inactive` with a successful exit, indistinguishable from one that ran and stopped. A refused read maps to Unobserved, never Absent. The read is a direct service call rather than the routed gunbc.systemctl_show_read, because that routes through host_operation_exec -> host_effect, which reaches this module via live_deploy.candidate -> roadmap_belt -> roadmap_belt_actuate. Importing it is a cycle. The transport indirection buys local-vs-ssh dispatch, which this observation does not need: the belt runs on the host it is reading. 4. THE PROCESS WAS LOOKED UP FOR THE WRONG ATTEMPT, and per-attempt units are what made it visible. dispatch_tmux_session_name takes only a node, so the pane observation was attempt-agnostic and could not tell two attempts apart. belt_workflow_progress_for_attempt_ref renders its row from current_key while iterating a branch ref, and those diverge once a node has more than one attempt. Measured: row 800de95d7a0b40d0, unit asked for 3a133af54ebf3999. The process is now looked up by the same key the row identifies itself with. Measured on srv2-deploy: the Agent row reports `failed` carrying the harness's own turn.failed cause, reconciled against unit Result=success ExecMainStatus=0 -- a clean refusal, not a crash, which is what the cross-check is there to tell apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…d one
extdeps.gunbc WitnessBin.Run declares stderr_capture: BoundedTail{16384} and
reports stderr_truncated with its byte counts. It declares NOTHING for stdout,
which is the channel that actually returns bulk output. So harness_run_command
announced truncation for the small channel while pasting the large one into the
turn verbatim, and HarnessOutputStanding's own annotation -- "a cap that
ANNOUNCES itself, so a large result is distinguishable from a complete one" --
was true of stderr only.
MEASURED, NOT ANTICIPATED. A worker ran `cat` on the plan document its own brief
names. That single tool_result was 162KB of a 170KB request: five messages,
roughly 42k tokens. Under three concurrent workers the router closed the
connection without a reply after four minutes (curl 52).
WHAT THE REPLAY REFUTED, recorded because the first diagnosis was wrong. The
backend does NOT reject the payload: replaying that exact 170KB request returned
200 in 4m47s. Size is not refused, it is SLOW, and the empty reply was
contention between concurrent workers rather than a size verdict. The bound is
still the repair, for the other reason -- an unbounded context is what makes a
turn take five minutes, and a five-minute turn is what makes concurrency fatal.
A 24-step budget is unusable at that latency whatever the backend does.
THE BUDGET IS THE HARNESS'S OWN, NOT THE TRANSPORT'S. Bounding here keeps the
cap where the contract that announces it lives, and it is the tool RESULT --
what enters the context -- that must be bounded, which is a different question
from how much the transport chose to read. A stdout capture policy, when one
lands, is the lower bound; this stays the narrower of the two.
Measured on srv2-deploy after the change: largest tool_result 16443 bytes
carrying one truncation marker, total request 29766 bytes across 7 messages --
against 167910 bytes across 5 before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…roster
harness_context_budget read spark_serving_desired_context_length -- the OLLAMA
roster ceiling. The harness does not post to the Ollama units; it posts to the
llama-server pair behind the router. That is the meaning fork
gunbc.harness.harness_wire already refused for the MODEL ("one name over two
serving stacks"), written again one field over, and it was invisible because
both rows read 1048576.
harness_router_context_window is declared beside harness_router_model, for the
reason that row already gives. Dissolves on fabric serving, when the window
becomes a property of the served work contract.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
The migration tasks had prose acceptance and nothing executed it, which is specification without execution. These are the executable half. WHAT A STUB CAN ASSERT IS FORCED, NOT CHOSEN. A witness naming a symbol the migration has not written yet is an unresolved import: it fails the MODULE, not the test, and a module that cannot compile cannot be enrolled, cannot execute and cannot answer. The mirror fails too -- "assert ExecArgv is gone" cannot be written by a witness that references ExecArgv. So each stub asserts the ABSENCE of the shell-rendering its task removes, over the API that exists today: false now, true exactly when the work lands. ENROLMENT IS SOUND BECAUSE THESE REACH THEIR SUBJECT, which is the rule the roster states at its own head after 101 rows were held while never executing -- they reached a hermetic refusal, and ExpectedRedArm read prose-carrying TypeError as ordinary failure. "A genuine red is loud once someone looks, while a never-executed row looks identical to progress." These are pure projections over .dag values under SubstrateInputsOnly: no host effect, no tool resolution, no budget. Three were executed and returned false, which is a verdict. THEY DO NOT RETIRE ON GREEN. Per DESIGN 4b(4) a discriminating RED that flips stays enrolled as the executing evidence that the property still holds, becoming a permanent regression control. The row that gates acceptance keeps guarding. WHAT IS NOT COVERED, named rather than papered over: the ExecArgv replacement migration has no stub. It is an operation on a service block, not a function returning a value, so there is nothing to assert absence over -- its red control is a corpus-shape property, which is a census question rather than a witness one. Enrolling a row that cannot discriminate would be worse than the gap. Four substrate refusals shaped this and each caught a guess: contains is a method not an import, MassStorageLun has backing_device rather than file, a bare process has no cgroup for the memory budget, and two module names do not follow their paths (gunbc.runner_host_deploy, not gunbc.runner.runner_host_deploy). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
Two defects the transcripts named, and one researched authority behind them. MAX_TOKENS WAS 2048, A CHAT-SHAPED LITERAL. On a thinking model a turn spent its whole allowance reasoning and emitted the same sentence twice rather than acting, and a heredoc carrying real file content does not fit in it at all -- a worker that cannot emit a file cannot write one. Now 65536. IT IS A POLICY ROW, NOT A CAPABILITY ROW, because the model declares no output ceiling. extdeps.deepseek.deepseek_v4 records the research: no primary source documents a maximum output for V4-Flash and the GGUF carries no output key. What bounds output is the ENGINE -- measured against the router, max_tokens 1048576 is refused naming the shared context while 262144 answers 200 -- so input plus output must fit the window with no separate ceiling beneath it. A deepseek_v4_max_output_tokens row would have attributed the engine's admission rule to the model. THE SYSTEM PROMPT DESCRIBED THE TOOL AS OBSERVATION. It said run_command 'returns its combined output' and 'use it when you need to observe', and the worker did exactly that: 34 tool calls in one attempt, every one a read -- cat, grep, sed -n, find, git log -- and no write of any kind. It now states the tool is how the tree is CHANGED, spells the heredoc form, and says reading alone completes nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
The first cut retained a fixed six tool rounds, which triggers at about thirteen messages REGARDLESS OF SIZE. Measured on t01: 25 rounds executed, 14 messages retained, compaction notice present -- on a run using roughly 40k tokens of a 1048576-token budget, about 4% of the window. The worker had its own working history discarded roughly once per round while nothing was near the limit, and a worker that repeatedly forgets what it just learned is a plausible cause of the non-convergence this mechanism was added to help measure. This is the mistake this module's own annotation warned about one paragraph earlier: a length trigger cannot fire at 78KB against 1M, so a round count was substituted -- and a round count is blind to the quantity it exists to bound. harness_compaction_due gates on the backend's counted input crossing three quarters of the budget. Below that the history is kept whole. Retention raised 6 -> 24 rounds for when it does fire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
The worker plans correctly and then will not commit. On t08 it reached a complete and right plan at round six -- 'I need to: 1. Delete roadmap_site_readback_script 2. Delete roadmap_site_digest_readback_script ...' -- and then spent rounds seven through twenty-one re-checking imports it had already read. Twenty-one rounds, zero writes, on the one task that requires inventing nothing. Deferring was free because nothing told it otherwise: steps_remaining lives in the loop and the model never saw it. That is the same asymmetry harness_tool's annotation already objects to one layer down -- 'the harness knew the limit and the caller could not see it' -- reproduced at the turn level. Each tool-result message now carries the remaining round count and the condition for acting: when the rounds left will not cover more investigation AND the change, make the change. It rides on the tool-result message rather than its own turn because the assistant/user alternation after the opening brief is what the compaction pairing depends on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
Every word the harness said to a model was prose -- a paragraph-long String
literal for the system prompt, and two notices built inline with join. Each fused
several independent facts into one sentence, so a caller wanting four of five
had to restate all five, and nothing could read, assert or vary any of them.
DESIGN 4c already rules on this: a String whose purpose is commentary is
misplaced data.
TWO PROPERTIES, BOTH ENFORCED BY THE CARRIER RATHER THAN BY CARE.
A premise cannot be authored without naming what it comes from.
HarnessGroundedPremise requires a DeclarationRef, so a clause with no downstream
referent is unwritable -- thirteen worker premises now each cite the declaration
they are read from. That is the citation discipline the rest of this corpus
already has, applied to the one medium where nothing would ever grep it: before
this, a prompt could claim run_command runs in the worktree long after that
stopped being true and nothing anywhere would notice.
The imperatives are not authored at all. harness_conclusions DERIVES them:
ToolCanWriteBy and CompletionRequires{FilesOnDiskDiffer} entail
ChangeTheTreeWithTheTool and ReadingAloneCompletesNothing;
CompletionRequires{AcceptanceCheckPasses} entails VerifyWithTheTool. Delete the
write-form premise and the imperative disappears on its own, which is the
property a paragraph can never have -- prose keeps instructing a model to do
things the system no longer supports.
The tool's name is never spelled in prose: ToolIs carries a HarnessTool and the
projection reads harness_tool_wire_name, so the prompt cannot disagree with the
wire about what the tool is called.
The grounding rule also deleted what was still typed prose. BeBrief and
ChangeIsExactlyTheBrief had no declaration to point at and are gone;
ReadingAloneCompletesNothing survived by becoming a conclusion, which is what it
always was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
Two changes, both about the same thing: a fact with two representations where only one executes. FIRST, ten witnesses over gunbc.harness.harness_guidance. They test the two layers that have a deterministic oracle -- PROJECTION (a premise renders to its clause) and ENTAILMENT (a premise set derives an imperative set) -- and not the third. Whether a worker given this prompt edits N lines is a sampled property of a nondeterministic system; an assertion over it is a flaky oracle, not a test, so it stays in the measurement harness and out of the gate. The load-bearing half is the NEGATIVE one. Asserting that the worker roster yields the change imperative is nearly free and would stay green if harness_conclusions were replaced by a hardcoded list. The three witnesses that carry information DROP a premise and require the imperative to disappear -- the discriminating RED of DESIGN section 5. They fail exactly when the syllogism decays back into authored prose. NOT WRITTEN, deliberately, and the omission is recorded in the module: "every premise cites a declaration". DeclarationRef carries NonEmptyStr fields and HarnessGroundedPremise requires one, so an ungrounded premise has no constructor. Per section 4b that check's RED is unauthorable, making it a permanently-green decoration that would be cited as coverage. The carrier holds it already; a test would only launder it. SECOND, harness_encode_message / _assistant_block / _user_block are deleted. They were a second encoder for one fact -- a message projected to wire JSON -- beside the live path in harness_turn, with zero callers anywhere in the corpus. The fork was also the lossy half: its tool_use arm wrote `input` as an empty object, so a history replayed through it would show the model its own prior commands with the commands removed, and a worker that cannot see what it already ran re-runs it. Nothing reached that arm, which is why it survived -- a dead fork is invisible until someone repoints at the tidier-looking surface. harness_tool_result_block sat inside the deleted range and is live (two callers); it is restored in place. It takes no Anthropic types, so the nine now-unused imports go with the fork. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
THREE CHANGES, ONE THREAD: the harness could not say how fast it was running, and the reason it could not was that it decoded the numbers and threw them away. FIRST, round.usage is written. input_tokens and output_tokens were always parsed -- the compaction gate reads the first, the terminal line prints both -- but nothing persisted them, so context size per round and tokens produced for it were observable only while the turn was still in memory. Reconstructing a rate afterwards meant pairing assistant-message CHARACTER counts against durations the host log rounds to the nearest minute. That answers "roughly" and cannot answer the question that matters here, which is whether a slow round was decoding slowly or waiting in a queue. The clock is read through extdeps.clock's typed surface and its refusal is CARRIED, not defaulted: a failed read emits clock_refused with the detail rather than an epoch_ms, so "the clock did not answer" and "this round happened at time T" stay distinct. A zero would fabricate an instant, and a consumer differencing two of them would compute a plausible interval from an event that never had a time -- fabricated plausible output applied to a measurement, which is the hardest place to notice it. SECOND, gunbc.harness.harness_throughput decodes that stream. It sits beside the WRITER rather than beside the dashboard, because the events format is the harness's fact. The rate is derived from tokens, interval and round count, never stored beside them. The first round's tokens are excluded from the numerator on purpose: a usage row is written when a response has ALREADY arrived, so the earliest reading marks the end of round 1's generation, not its start, and counting it would credit that output to a window it did not run in -- worst at the small round counts a dashboard is most often read at. Seven witnesses over a controlled fixture whose answer is computed by hand, including the discriminating reds: one clocked round must refuse rather than divide, and an unclocked row must not be absorbed as if it had a time. THIRD, PostJsonFromFile takes a deadline. It had none, so curl waited forever -- not a slow path but a missing refusal, and DESIGN section 5 rules that a bounded "forever" is not an "unknown" error. Measured here: a wedged backend held one harness POST open past thirty minutes while the router answered an unrelated probe in 0.15ms, so the turn neither progressed nor failed and no arm could say so. The value is an INPUT, not a literal in the argv -- how long a caller waits for ITS backend is the caller's policy, curl's ability to bound a request is the operation's interface. The harness declares 900s against measured rounds: real completions here run 13s to 480s, so a shorter deadline would kill rounds this backend genuinely finishes. The band thresholds behind the speed symbol are a declared policy row carrying the measurements that produced them, so the number behind each symbol is auditable and moves in one place when the engine does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
The Activity region could say an attempt was alive and could not say whether it was doing anything. "worker process: running" is true of an agent producing tokens and equally true of one wedged on a backend that will never answer -- the state this deployment actually hit, where a POST stayed open past thirty minutes while the process stayed perfectly "running". A rate separates those two without the reader opening a transcript, which is the whole reason the row sits beside the process row rather than in disclosure. NO NEW HOST READ WAS NEEDED, which is why this is small: WorkflowAttemptEvidence already carries provider_events as a String, so the throughput is DERIVED at the same seam that already derives provider execution state. WorkflowAttemptProgress gains one field computed from bytes it was already holding. THE BAND IS A COPRODUCT, NOT A GLYPH STRING. A single fn returning "turtle" would make the SYMBOL the carrier of the distinction, so a renderer wanting different glyphs -- or a text log wanting words -- would match on presentation strings and break silently when one is retitled. ThroughputBand is the concept; throughput_band_glyph and throughput_band_word are two projections of it. An unmeasured rate renders as "unmeasured", never as a zero and never as a fast glyph: "no interval yet" and "measured slow" are different claims and the cell keeps them apart. The row-count witness is updated to 3 AND gains a label/value assertion for the new row, so it stays an identity join rather than degrading into a count that any third row would satisfy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
Corrections from the router owner, measured from the router's code rather than inferred here, plus one of their four points DECLINED with a receipt. THE WINDOW WAS THE CAP. harness_router_context_window carried 1,048,576, which is the engine's ADMISSION CEILING, not an allowance. The KV pool holds roughly 4.1M tokens, so four sessions at the ceiling exhaust it exactly and everything further queues on capacity rather than compute -- which is the pathology behind this deployment's slow-round measurements, where duration tracked router occupancy instead of request size (a 17 KB request at 480s, a 50 KB one at 60s). The working window is now 200,000, the operator's number from a stated 100k-250k band. It is NOT derived from pool-divided-by-slots: that arithmetic yields ~1M and would have reproduced the bug, because the divisor is not a constant this module can see. harness_router_admission_cap is a NEW row carrying the ceiling separately. With one row the server's limit and the client's entitlement were indistinguishable, which is exactly how the ceiling ended up in the window field; two rows make the confusion hard to repeat and give the ordering something to check. MAX_TOKENS WAS SIZED BY A MISREADING. 65,536 came from an instruction to use the maximum the model allows -- but the engine ADMITS that many, it does not produce them quickly. The preallocation theory this repo briefly held is wrong: vLLM's KV cache is paged and commits blocks per token actually generated. The real cost is the tail, and 65,536 tokens at the declared floor is roughly forty-five minutes, so one legitimate long answer exceeded any deadline the harness carried. Now 8,192. THE DEADLINE IS DERIVED FROM THE BUDGET IT MUST COVER. A flat 900s beside a 65,536-token ceiling were two unrelated numbers, so raising the budget silently created a generation the deadline could not admit. It is now max_tokens / decode_floor + prefill_allowance, so the two cannot drift; a timeout means the backend failed to sustain the declared floor, a real fault, rather than meaning the model answered at an ordinary rate for longer than a hand-written constant allowed. Read back by execution as 641s -- pinned in a witness because integer division, not the author's mental model, decides it. DECLINED: repointing /v1/messages to /v1/chat/completions. The report predicted a fast 404 there on the grounds that the router forwards paths verbatim. Measured against the captured runs, that path served 8 POSTs with 0 failures under curl --fail-with-body, which fails hard on 404, and returned decoded Anthropic content blocks carrying chatcmpl-prefixed tool ids -- vLLM's Anthropic-compatible endpoint. Changing it would have broken a path with a receipt. Recorded rather than silently skipped, because the reasoning was sound and only the premise was wrong. Three witnesses, all with authorable reds: the window must stay under the cap, one response must fit inside the window, and the deadline must derive to its computed value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
THE FLEET'S ACTUAL DEPLOYMENT COULD NOT BE WRITTEN DOWN. OllamaServingLaunchProfile
carried the engine in its TYPE NAME while its three fields -- a bind address, a
working window, a slot count -- were already engine-agnostic facts about the
service. So "vLLM, tensor parallel, world size 2" had no representation, which is
why it lived in hand-written units on four boxes and why convergence kept
restoring the engine this fleet had abandoned. A model that cannot describe what
is running cannot converge toward it.
Split into ServingContract (what) and ServingEngine (how), joined by
ServingLaunchProfile -- the section 3 interface/realization split with the engine
as one of N handlers. spark_serving_desired_engine now declares
VllmTensorParallel { world_size: 2 }. Two rather than four because the pairs sit
on point-to-point direct-attach rails and the only path between pairs is the WiFi
LAN; that topology is already modeled in gunbc.spark.fabric_link_desired and is
cited rather than restated. The cut surfaced two accessors reaching through the
fused shape, which is the fail-closed census working.
CO-TENANCY, AND WHAT THE TYPE SYSTEM WAS MISSING. A Spark was wedged on
2026-09-03; the accident was not the second engine's identity but that TWO
ENGINES SHARED ONE MEMORY POOL AND NEITHER COULD SEE THE OTHER. The incumbent
held ~100 GB, the arriving engine's free-memory probe FAILED, it fell back to a
stale 116.5 GiB reading, and offloaded a 108 GiB model on top of a live engine.
extdeps.memory.types already offered UnifiedShared, so the KIND was expressible.
The gap was one level up: GpuFacts carries its own MemoryFacts with its own
capacity, structurally parallel to host memory and related to it by nothing. On a
GB10 the accelerator reservation IS system memory, so the model could assert
128 GB device and 128 GB host and be internally consistent while describing a
machine that does not exist. Double-counting was representable; sharing was not.
Adding a UnifiedMemory type would have been a nickname for MemoryKind and left
that intact. ServingCellMemory is a POOL instead: on a unified cell there is one
number and every occupant draws from it.
OCCUPANCY IS CHECKED BEFORE SIZE, and the order is the wall. An arriving engine's
own view of free memory is not evidence when an incumbent holds part of the pool
-- a FRESHER probe would have been just as wrong, one instant before the
incumbent allocated. Asking "is anyone here" needs no probe at all. The refusal
causes are separate variants because their remedies differ: an occupied cell is a
placement error, an oversized request is a sizing error.
Five witnesses replay the wedge with the observed numbers. The load-bearing one
is the CONTROL: the same 108 GiB request on an EMPTY cell is ADMITTED. Without
it a fence that merely refused large requests would pass every other witness and
prove nothing -- and it records the true fact that the model would have loaded
fine on a genuinely empty box.
RUNG, STATED RATHER THAN IMPLIED. The de-fusion is a real climb: a deployment
that had no representation now has one. The fence is MECHANICALLY PREVENTABLE and
NOT YET LOAD-BEARING -- nothing observes residency from a live cell and nothing
calls it on a launch path, so it is a wall beside the door rather than in it. It
would not have stopped the wedge, which came from outside this model entirely.
Next-rung trigger: deriving the serving units as a HostEffect from
serving_converge_realize, after which the fence sits on the path that starts
engines. "Refusable" here means refusable in the model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
FOUND WHILE ANSWERING "WHY IS MAX_TOKENS 8192", WHICH IS THE PART WORTH KEEPING: the number turned out to be the uninteresting half. The round arm branched on whether tool calls were PRESENT and never on whether they were COMPLETE, so a response that stopped at the max_tokens ceiling MID TOOL CALL had its truncated arguments executed anyway. WHY THAT IS CORRUPTION AND NOT A SLOW PATH. This harness offers one tool, it runs a shell command, and its declared write forms are heredoc and in-place edit. A heredoc cut off at the token boundary writes a PARTIAL FILE and exits 0, so the loop continues against a tree it silently damaged and every downstream signal -- the exit status, the event stream, the round counter -- reports an ordinary successful round. Nothing in the system could have reported it. That is the fabricated-plausible-output failure DESIGN section 5 forbids, reached through a hole nobody opened on purpose. THE EXHAUSTIVENESS IS WHAT HID IT. harness_stop_reason_wire matches all seven stop reasons and is total; the decode was never incomplete and no warning was available. The defect sat one level up, in a caller that had the reason in scope and did not consult it -- the "total at the level examined, blind one level down" pattern this repo already records, with the tell being a judgment that looks finished because nothing about it is missing. RAISING THE CEILING IS NOT THE FIX AND THE COMMENT SAYS SO. A larger max_tokens moves the boundary; it does not remove it, and no ceiling is large enough for every file a worker might write. The wall is that a truncated call must not execute at all. TurnTruncatedMidToolCall carries the ceiling and the output count so the refusal can be told apart from a budget or transport failure, and its text says plainly that the refusal is not evidence the work needed a longer call. Adding the variant made two total folds non-exhaustive, which is the closed substrate doing exactly what it is for: a new state forces every fold to say what it means rather than inheriting whatever arm happened to catch it. Two witnesses. The control is the load-bearing one -- an ordinary tool_use or end_turn stop must NOT read as truncation, since a predicate answering true for every reason would satisfy the positive witness and refuse every round the harness runs. MEASURED CONTEXT, so the ceiling is not mistaken for a tight one: across every run today the largest single-round output was 233 tokens against the 8192 ceiling, roughly 35x headroom. The truncation path is reachable only by a large heredoc write, which is precisely what this task type does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…grep missed activity_view_from_parts gained a throughput parameter in fca16f5. The census of its callers was piped through head, which silently truncated the list at ten lines and dropped the two sites in roadmap_sandbox. The presentation witness closure compiled because it does not reach the sandbox; the serve entry does, and the dashboard refused at startup for four hours. Two lessons recorded at the site rather than left implicit. A census is not a census if any part of it is truncated -- the complete count is 11 sites in 4 files and now reconciles. And a restart was declared healthy on the unit being active rather than on a 200 coming back, which is specification without execution in its most literal form. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…oseying
WHAT IT SHOWS. One segment per round, in the Activity region beside the
throughput row. A segment's WIDTH is the wall interval that ended at its round,
so a long round is a wide segment rather than one averaged away; its COLOUR is
what the round produced -- dim for a read round, the figure material for a round
that changed files, an outline for a round nobody measured. The run that
motivated it, on 2026-09-04, would read as fifty minutes of dim ending in a
bright sliver at round twenty-two: the research/write imbalance that took hours
to find by hand, visible at a glance.
WHY TIME AND NOT COUNT, answering the question asked before it was built: what
happens when rounds get longer, or budgets get bigger? Cells-per-round would
average or truncate; a time axis does neither. Longer rounds get wider, more
rounds get thinner, and the thing being glanced at -- dim width against bright
width -- stays readable at any count, degrading into a research-time-versus-
write-time bar, which is the moseying metric itself. Widths are flex-grow
weights taken directly from the millisecond intervals: proportional by
construction, no percentage arithmetic, nothing to round.
THE FACT UNDER THE COLOUR IS AN OBSERVATION, NOT A CLASSIFICATION. A round is a
write round when the worktree's changed-path count ROSE over the previous
measured value -- a delta over git's own answer, read after the round's tools
ran. Parsing the shell command for a redirect would have been a heuristic where
a fact was available; a level test would paint every round after the first write
as a write. files_changed is optional on the wire and in the ledger so that
"not measured" stays distinct from "measured zero": older streams paint
unmeasured, never read.
THE GIT OP WAS PORTED TO BE REPOSITORY-ADDRESSED. WorkingTreeStatus was one of
five operations extdeps.git.inspect declares as still repository-ambient -- it
ran in the process cwd, which for the harness is its own checkout, not the
attempt worktree; the ambient form would have counted the wrong tree and
reported every round clean. It now takes repository_path and passes -C, in the
shape of the already-ported sibling. Its three consumers were censused and all
were genuinely ambient (none held a root), so each now passes Toplevel().path,
which is the read the ambient form had been performing implicitly. Verified wet:
observe_candidate_checkout evaluated against the live checkout through the
ported op.
ONE CARRIER, TWO PROJECTIONS. WorkflowAttemptProgress now carries a round
LEDGER; the throughput cell is derived from it rather than carried beside it,
because a rate stored next to the rounds it is computed from is two
representations of one fact. The ledger rides on WorkRowView beside the activity
view, not inside it: ActivityView is matched by name in sixty-three arms across
four files and the ledger is read in one place, so widening the view's contract
would have made every consumer name a field it never reads. Both are read off
the same progress at the same seam (rounds_of, daily_row_rounds), so they cannot
describe different attempts.
THE SEGMENT FOLD LIVES WITH THE LEDGER, NOT THE PAGE. Widths, kinds and the
N-1 rule are pure derivations, witnessed over a fixture whose answer is computed
by hand; the page only paints spans. Four witnesses: N rounds yield N-1
segments; a width is the interval that ended at its round; a rise is a write and
the prior flat round is a read (the discriminating pair -- a level test fails
it); an absent count is unmeasured, not read.
VERIFIED BY EXECUTION, BOTH LAYERS. The full serve closure compiles; the
dashboard was restarted and checked by a 200 on / and /sandbox rather than by
the unit being active -- the check that was skipped earlier tonight and cost
four hours of downtime. /sandbox renders nine segments against a moseying
fixture (two long reads, one short write, three rows); the first serializes as
flex-grow: 120000 with the tooltip "round 2 · read · 120s · 260 tok". The main
page renders no strip because none of its attempts has a clocked ledger yet:
absent by design, not empty. The seven pre-existing throughput witnesses were
re-run against the changed module and hold.
Two census defects found and recorded while threading, both mine: a caller list
piped through head silently dropped two sandbox sites, and a literal count
taken with grep "Type {" matched fn signatures too and came back exactly 2x.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…ll is its own strip kind
The harness ended a turn at its step budget by writing turn.failed, so the dashboard read "Agent
failed" beside "Verification passed" on one row. The budget is the harness's own bound; reaching it
stops the worker, it does not fail it. turn.budget_exhausted now carries steps_taken, the provider
fold lands it in ProviderBudgetSpent, the agent segment is WorkflowSegmentStopped ("Agent stopped at
its round budget · Verification next"), and the belt commits and verifies it as a terminal exactly as
before.
round.usage carries tool_calls, counted from the model's tool_use blocks. A round with zero is a
ReasonRound and paints in the figure halo, between the dim read and the figure write; older ledgers
without the count classify as before. Three discriminating witnesses, all executed green; the three
closures (harness_cli, roadmap_belt_actuate, roadmap_sandbox) compile with 0 blocking.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…he verdict at the top of the brief Every attempt of a node started fresh from the base ref with the same brief, so a worker that spent its budget mid-task was followed by one that began from nothing; five attempts of one node re-read the same tree. gunbc.roadmap.roadmap_attempt_continuation decides, from the prior attempt's terminal state and verification segment, whether the spawn continues: a failed verdict on a terminal prior branches the new worktree from the prior attempt's branch (its committed work is already in the tree) and prefixes the brief with the verdict; a green or unverified prior starts fresh as before. The turn is written at <attempt-state>/continuation-turn; a lineage at three turns, or an unreadable turn file, refuses with a typed SpawnFailed rather than restarting silently. Plumbed as AttemptOrigin through dispatch_spawn_commands_for_resolved_instance and the belt's spawn. Four discriminating witnesses executed green; the belt and sandbox closures compile with 0 blocking. The stopped agent segment also gets a neutral obligation style. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
The first node dispatched against the continuation machinery: one function in gunbc.roadmap.roadmap_attempt_continuation, one oracle authored before the worker runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
… pulse keeps its own timing Each strip segment carried inline animation-delay/animation-duration for the fill replay; an inline longhand outranks every stylesheet rule, so on the live segment it also overrode the pulse rule's 3000ms and the pulse ran at the 120ms fill floor -- the flashing the operator saw. Segments now carry --fill-delay and --fill-duration; longhand rules read them after the fill shorthand and before the pulse shorthand, so the pulse's timing is the last word by cascade order. The sandbox lab sets the same custom properties. A witness is red the moment a longhand is written inline again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
… has exited A transient unit that exits 0 is collected by the manager and reads LoadState=not-found; one that exits nonzero is retained as failed. The process observer read not-found as "never ran", so the one worker that finished with turn.completed (attempt ce21f99eb2e82ec1, 2026-09-05) was the one the belt could not commit or verify, while the eight failed attempts before it were all committed. RemainAfterExit=yes keeps the unit loaded with its ExecMainStatus readable; the observer now reads SubState (exited/failed/dead => finished, else live) because ActiveState stays active under RemainAfterExit. extdeps.systemd gains SubState. Witness executed green; belt closure 0 blocking. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
… stops auto-opening anomalous rows Every anomalous row opened its ledger, so a board of eight spent attempts was eight open ledgers of identical text. A row at rest now shows its summary sentence and the round strip with the ledger behind the disclosure; it opens while the worker process is live. The refused arm keeps its located reason open. Sandbox closure 0 blocking; render witness green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
(cherry picked from commit 86dd174)
… a fail-closed decoder, and a harness entry gunbc.harness.harness_review_guidance models the reviewer independently of the worker: its premises (one candidate, one goal, evidence offered, changing the tree withheld, a verdict file required) are grounded on declarations and its imperatives are derived from them, so dropping the withheld-change premise drops "your output is the verdict, not a change" on its own. gunbc.roadmap.roadmap_review_role carries the verdict (approve / request_changes with located reasons / cannot_judge), decodes it fail-closed from the reviewer's JSON file, maps it to the review segment state, and builds the per-candidate brief. harness_reviewer_cli runs the same harness under the reviewer's prompt with a 20-round budget. Seven witnesses executed green; the harness_cli closure compiles with 0 blocking. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…s verdict as the review segment, publication gated on approval The review pass runs after verify on the same tick. For an attempt whose verification passed and whose head has no verdict file and no reviewer, it spawns one reviewer unit (gunbc-review-<node>-<key>) against the attempt worktree, confined to the attempt's review directory as its only writable path, with the reviewer's own prompt and a brief carrying the goal, the diff range, the acceptance command and the belt's verification detail. A reviewer that exits without a verdict is a named failure, not respawned. The review segment reads the verdict file (approved / changes requested / cannot judge / undecodable) and before that says whether the head is unverified, a reviewer is running, or the pass has not run. Publication now needs a decoded approve beside the verification pass. Continuation reads the review: changes requested continue the lineage with the reviewer's reasons; a verified head awaiting review holds rather than spawning. Belt closure 0 blocking; witnesses green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…rktree is its argument gunbc run refuses a source root outside its process root; the first reviewer exited 101 with the worktree as cwd. Witness executed green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…w is separated into the questions the design argument asks gunbc.roadmap.roadmap_review_criteria models one checkable question per proposition of the modeled design argument (gunbc.design_argument) plus the node-goal criterion; a criterion naming a proposition the argument does not carry is a witness failure. The belt spawns one reviewer unit per criterion (gunbc-review-<node>-<key>-<criterion>) writing <head>.<criterion>.json, each briefed with its one question and tells. gunbc.roadmap.roadmap_review_role reconciles the roster: changes requested on any criterion fails the head carrying that criterion's reasons; a criterion nobody could judge blocks; running reviewers read active, unstarted pending; only all-approved completes, and only that admits publication. Reviews of different criteria disagreeing is the design, not a conflict; each answer keeps its criterion's name. Belt closure 0 blocking; seven witnesses executed green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…s six reviewers spread across the roster Seeded on the shared worktree, all six landed on .232 while .225 idled (measured 2026-09-05). Witness executed green; harness_cli closure 0 blocking. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…mpts were dispatched from gunbc.harness.harness_goal_audit_guidance is the auditor's own syllogism: it judges the OUTCOME against the node's approved goal and displaced cost, is never shown the worker's brief, and derives the one question the other roles do not ask -- whether a green acceptance check is evidence the goal is met. gunbc.roadmap.roadmap_goal_audit_role reuses the review's verdict carrier and reconciliation over a roster of one; the "goal" question leaves the review roster because it is now the audit's. The belt spawns one auditor unit per verified head beside the reviewers, the goal-audit segment reads its verdict, publication needs the audit's approve beside the review's, and continuation reads the audit after an approved review. Publication's PR base is now the instance's dispatch base branch (dispatch_base_branch), not the repository default: a srv2 attempt dispatched from origin/srv2-deploy was being offered against main. The srv2 clone's origin is repointed at GitHub so publication can run there. Belt and harness closures 0 blocking; eleven witnesses executed green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
Resolutions: both sides' systemd-run argument builders kept (main's --collect transient/wait builders beside this branch's user-manager builder, which deliberately omits --collect so a finished unit's ExecMainStatus stays readable); the spark serving model taken from main wholesale, including deleting this branch's serving_cotenancy and serving_realization_config, since the fleet work merged on main is the authority; the belt CLI entrypoints left where main moved them (roadmap_belt_tick_cli) with this branch's JSON tick kept; roadmap_serve keeps the daily-workspace and fleet page imports. Two pre-existing reds on this branch that the floor refused: roadmap_dispatch_actuator_witness_test now reaches the tmux commands through the session container, and roadmap_workflow_progress imports HarnessRoundLedger instead of resolving it by accident. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Repair-Judged: docs/design-rung-drops.md
…MAP.md gunbc.review_verdict already declares a ReviewVerdict that gunbc.idea_pr_spine reaches by bare name; a second type of that name in the review role made the bare reference unresolvable on the floor (CI run 33972543154). The roles' verdict is a judgment of one head, so it is named for that. ROADMAP.md is the registry projection the build lane found drifted; regenerated from its authority. Stage0 mirrors re-derived with a fresh claim_executor and found identical (first_generation_equal). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…deferral's reason vocabulary dag/test/claim/shell_to_dag_stub_witness_test.dag no longer exists on main and its eleven claims were red against main's modules; the merge had left it standing. The verify pass's deferral for a non-terminal worker names the agent obligation again, which the belt witness pins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
This was referenced Sep 5, 2026
briansrls
added a commit
that referenced
this pull request
Sep 11, 2026
…0598) Its deploy branch landed on main (#10443), so the row returns as its annotation said it would. Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gunbai-bot Bot
pushed a commit
that referenced
this pull request
Sep 13, 2026
extdeps.systemd SystemdUnitProperty carried two identical SubState arms and two identical wire arms. One name, two declarations in one coproduct, on main since #9761/#10443 -- a section 3 single-authority defect that stayed invisible because variant lookup took the first exact match and never asked whether a second existed. The refusal arm in this branch's find_variant_child_keyed is what surfaced it: both children key equal, so lookup refuses, and the required floor and the heal job both red with ChangedWitnessObservationFailed naming this line. The corpus population is measured, not assumed -- an arm-level scan of every coproduct declaration under dag/ and src/ finds exactly one declaration with a duplicated leaf key, this one. The duplicate declaration arm and its duplicate wire arm are deleted. Behaviour is unchanged: SubState still maps to "SubState", and the seven witnesses of test.claim.systemd_property_directive_overlap pass. This edit is outside the v1 seed scope of this PR and is admitted only as a forced consequence of a correct refusal, with a measured population of one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa46zVYGRz2PUMouekjznP
gunbai-bot Bot
pushed a commit
that referenced
this pull request
Sep 13, 2026
One name, two declarations in a single type. Lookup by exact authored name takes the first match and never asks whether a second exists, so the duplicate is not merely tolerated -- it is unobservable, and no consumer can report it because none can distinguish the two. The receipt is live rather than constructed: extdeps.systemd SystemdUnitProperty carried two identical SubState arms since #9761/#10443 and passed every required run in that window, surfacing only when this branch began keying variant lookup and refusing on more than one match. Rung found at is recorded as mechanically preventable BY ACCIDENT, which is the honest grain: the refusal that caught it is a lookup-time consequence of keying for an unrelated repair, not a wall built for this class, and on any route that does not key the class stays silent. The ceiling is structural impossibility -- a duplicate leaf has no constructor once the declaration fold refuses it -- and the trigger names that capability, not an artifact. A lookup-side refusal does not retire the row. Filed on swift-bat-902's ruling that the declaration-time wall is a ledger obligation rather than gunbc#11301's scope, so the repair that surfaced the class is not mistaken for the wall against it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa46zVYGRz2PUMouekjznP
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.
What this lands
One day of work on the roadmap harness, dashboard and the verify→publish phase, from the srv2 deploy branch. Every piece is witnessed; the witness names are in the commit messages.
Harness (DeepSeek V4 Flash on the Spark vLLM engines)
chat_template_kwargs: thinking + reasoning_effort=max), inline</think>decode client-side, reasoning replayed on the chat-completions shape; the Think Max preamble is pinned to the upstream encoder at commit60d8d707with a derived link..225/.232:30000, replica chosen per worker); the ctrl dashboard router is no longer a dependency (operator ruling: gunbc must not touch ctrl)./metrics(8 tok/s), so the derived deadline coversmax_tokens; step budget 40; per-round usage and throughput in the provider events.Roadmap workflow: commit → verify → publish
roadmap_belt_commit, tick receipt schema v5).ValidationPassedreceipt for the exact head (roadmap_publish_admission), deferred with a typed reason otherwise.github.Pulls.Createand a non-forcedgit.Core.PushRefInRepo;roadmap_publish_actuatepushes the attempt branch and opens the PR from the token-holding helper, which then re-observes GitHub and answers with what it saw.Dashboard
round_playbackwith a 120 ms floor), the live segment pulsing;AmbientRulekeyframe motion in the design material, silenced under reduced motion; sandbox lab with replay and 8× slow controls.Fleet
Merge notes
origin/mainate5a51d6dd3; two trivial conflicts resolved main's way (an EOF blank line; the expected-red roster chain, taking main's retirement ofchunk_23and the shell-to-dag stubs chunk and deleting the stale chunk fn with it).every_quarantine_probe_derives_exactly_one_dispositionis red onorigin/mainby itself and on this branch by itself; not introduced here.Added 2026-09-05 (second day)
Terminal states and the strip. A spent round budget is
turn.budget_exhausted→ProviderBudgetSpent→ agent segmentWorkflowSegmentStopped("Agent stopped at its round budget"), never "Agent failed".round.usagecarriestool_calls; a round with none is aReasonRound(figure halo), so the strip shows read / write / reasoning / unmeasured. The live pulse no longer inherits the fill's inline duration (it ran at 120 ms); the fill schedule travels as--fill-delay/--fill-duration. Ledgers open only while a worker is live.Successful workers were never committed. A transient unit that exits 0 is garbage-collected by systemd and read as never having run; only failed workers reached commit and verify. Worker units now
RemainAfterExit=yesand the observer readsSubState.Continuation turn. When a node's last attempt has ended and its verdict failed, the next spawn branches from that attempt's branch with the verdict at the top of the brief; three turns per lineage, then a typed refusal. A verified head awaiting review holds rather than respawning.
Three roles, each its own syllogism. The reviewer (
harness_review_guidance) judges one candidate on one criterion; criteria are one per DESIGN proposition §2–§6 (roadmap_review_criteria, grounded ongunbc.design_argument, witnessed), one reviewer unit per criterion writing<head>.<criterion>.json, reconciled by dominance (changes-requested > blocks > running > pending > all-approved). The goal auditor (harness_goal_audit_guidance) judges the outcome against the node's approved goal and displaced cost, is never shown the worker's brief, and asks whether the acceptance check proves the goal. Publication requires every criterion and the audit to approve. Verdicts areHeadVerdict, decoded fail-closed.Publication. The PR base is the instance's dispatch base branch (
dispatch_base_branch), not the repository default. Exercised end to end on srv2 against GitHub: worker → commit → verification → six reviews → audit → #10546.Merge with main. Main's spark serving model taken wholesale; belt CLI entrypoints stay where main moved them (
roadmap_belt_tick_cli); two pre-existing reds fixed (dispatch witness via the session container;roadmap_workflow_progressimportingHarnessRoundLedger);shell_to_dag_stub_witness_testdropped as main had.🤖 Generated with Claude Code
https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY