Skip to content

Harness thinking on the Spark engines, the roadmap's commit→verify→publish phase, and the round-strip dashboard - #10443

Merged
briansrls merged 82 commits into
mainfrom
srv2-deploy
Sep 5, 2026
Merged

briansrls merged 82 commits into
mainfrom
srv2-deploy

Conversation

@briansrls

@briansrls briansrls commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

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)

  • Thinking as a request switch (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 commit 60d8d707 with a derived link.
  • The harness talks to the engines directly (.225/.232:30000, replica chosen per worker); the ctrl dashboard router is no longer a dependency (operator ruling: gunbc must not touch ctrl).
  • Decode floor read from vLLM's own /metrics (8 tok/s), so the derived deadline covers max_tokens; step budget 40; per-round usage and throughput in the provider events.

Roadmap workflow: commit → verify → publish

  • Belt commit pass: a completed attempt's worktree is committed before verification observes its head (pure decision in roadmap_belt_commit, tick receipt schema v5).
  • Publication admitted only on a ValidationPassed receipt for the exact head (roadmap_publish_admission), deferred with a typed reason otherwise.
  • github.Pulls.Create and a non-forced git.Core.PushRefInRepo; roadmap_publish_actuate pushes the attempt branch and opens the PR from the token-holding helper, which then re-observes GitHub and answers with what it saw.
  • Roadmap: three sized nodes for these lanes, two parked rows unsized, and an unsized node no longer mints a closing-contract task.

Dashboard

  • Round strip: time-proportional segments with a fixed 2.4 s playback (round_playback with a 120 ms floor), the live segment pulsing; AmbientRule keyframe motion in the design material, silenced under reduced motion; sandbox lab with replay and 8× slow controls.
  • Throughput cell (avg tok/s with a band glyph) beside the agent line.

Fleet

  • Spark serving: engine de-fused from the release, co-tenancy rule, and a per-(release, engine) configuration observation table with no default arm (operator ruling: land it, line stops).

Merge notes

  • Merged origin/main at e5a51d6dd3; two trivial conflicts resolved main's way (an EOF blank line; the expected-red roster chain, taking main's retirement of chunk_23 and the shell-to-dag stubs chunk and deleting the stale chunk fn with it).
  • every_quarantine_probe_derives_exactly_one_disposition is red on origin/main by itself and on this branch by itself; not introduced here.
  • Witnesses were compiled with the deploy tree's binary built 2026-09-03; CI builds fresh.

Added 2026-09-05 (second day)

Terminal states and the strip. A spent round budget is turn.budget_exhausted → ProviderBudgetSpent → agent segment WorkflowSegmentStopped ("Agent stopped at its round budget"), never "Agent failed". round.usage carries tool_calls; a round with none is a ReasonRound (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=yes and the observer reads SubState.

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 on gunbc.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 are HeadVerdict, 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_progress importing HarnessRoundLedger); shell_to_dag_stub_witness_test dropped as main had.

🤖 Generated with Claude Code

https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY

Brian and others added 30 commits September 2, 2026 22:23
… 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
gunbc-ci-auto-heal and others added 19 commits September 5, 2026 01:44
…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
… 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
@briansrls
briansrls merged commit fdc7292 into main Sep 5, 2026
4 checks passed
@briansrls
briansrls deleted the srv2-deploy branch September 5, 2026 17:17
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant