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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 17 additions & 26 deletions dag/gunbc/dispatch_preflight.dag
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ import gunbc.roadmap_dispatch_environment {
capability_ensure_receipt_json,
}
import gunbc.fleet_desired_observe {
FleetRevisionStanding,
RevisionConverged,
RevisionDrifted,
RevisionUnobserved,
fleet_revision_standing,
LocalTreeRevision,
LocalTreeRevisionObserved,
LocalTreeRevisionUnobserved,
local_tree_revision,
}
import extdeps.git.object_store { git_object_id_wire_hex }
import extdeps.languages.json.emit {
Expand Down Expand Up @@ -54,7 +53,7 @@ import extdeps.shell
// This module is the read-only route that answers it independently of whether a spawn is
// admissible. It observes and it does not actuate: capability_ensure_observe runs
// shell.Test.IsExecutable and the realization's own declared identity_args (the --version probes),
// and fleet_revision_standing runs git ls-remote and rev-parse in the deployed tree. No worktree is
// and local_tree_revision runs git rev-parse in the deployed tree. No worktree is
// created, no tmux session is started, no file is written, and there is no flag that opens a write
// path — a preflight that can also change what it measures is a preflight whose green proves
// nothing.
Expand Down Expand Up @@ -148,30 +147,22 @@ fn preflight_capability_receipts(
capability_ensure_observe(realization: realization))
}

// The revision axis is the one that decides whether ANY spawn can happen, so it is
// reported beside the capabilities rather than ahead of them: a host may be perfectly
// provisioned and still spawn nothing, and a reader needs to see both facts at once.
fn revision_axis_verdict(standing: FleetRevisionStanding) -> PreflightAxisVerdict {
// The revision axis names the code the host would dispatch from, so it is reported beside the
// capabilities rather than ahead of them: a reader needs both facts at once. It refuses only when
// HEAD cannot be read. Drift from the admitted fleet ref is no longer an axis (operator ruling,
// 2026-09-05; `gunbc.roadmap_launch_admission` `launch_revision_outcome` carries the argument):
// convergence is the deploy path's fact, and the deployed-tree report renders it.
fn revision_axis_verdict(standing: LocalTreeRevision) -> PreflightAxisVerdict {
match standing {
RevisionConverged { revision } =>
LocalTreeRevisionObserved { revision } =>
AxisGrounded {
axis_label: dispatch_preflight_revision_axis_label,
evidence: join([
"deployed tree is the admitted fleet revision ",
"deployed tree is at revision ",
git_object_id_wire_hex(oid: revision) as String,
], ""),
}
RevisionDrifted { desired, local } =>
AxisRefused {
axis_label: dispatch_preflight_revision_axis_label,
reason: join([
"deployed tree is not the admitted fleet revision; the belt will withhold spawn and reap on every tick until it converges. desired=",
git_object_id_wire_hex(oid: desired) as String,
" local=",
git_object_id_wire_hex(oid: local) as String,
], ""),
}
RevisionUnobserved { cause } =>
LocalTreeRevisionUnobserved { cause } =>
AxisRefused {
axis_label: dispatch_preflight_revision_axis_label,
reason: join([
Expand All @@ -191,7 +182,7 @@ fn revision_axis_verdict(standing: FleetRevisionStanding) -> PreflightAxisVerdic
// establish the carrier's honesty.
//
// The repair is construction rather than a consistency check (DESIGN 5): the report carries the
// capability receipts and the fleet revision standing — two independent observations, neither
// capability receipts and the deployed tree revision — two independent observations, neither
// derived from the other — and preflight_axes projects them on demand for the verdict and for the
// wire. A report whose axes disagree with its receipts now has no spelling, so there is nothing
// left to validate.
Expand All @@ -200,7 +191,7 @@ type DispatchPreflightReport {
instance_id: String
repo_root: String
capability_receipts: List<CapabilityEnsureReceipt>
revision: FleetRevisionStanding
revision: LocalTreeRevision
}

fn preflight_axes(report: DispatchPreflightReport) -> List<PreflightAxisVerdict> {
Expand All @@ -225,7 +216,7 @@ fn preflight_report_for_instance(
instance_id: instance.instance_id as String,
repo_root: instance.repo_root as String,
capability_receipts: receipts,
revision: fleet_revision_standing(repo: instance.repo_root),
revision: local_tree_revision(repo: instance.repo_root),
}
}

Expand Down
8 changes: 4 additions & 4 deletions dag/gunbc/fleet/fleet_desired_admission.dag
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import gunbc.fleet_revision_acceptance {
}
import extdeps.git.plumbing { GitWorktreeAt }
import gunbc.required_ci_epoch_observation { RequiredCiEpochObservation, observe_required_ci_epoch_at_commit }
import gunbc.fleet_desired_observe { observe_fleet_revision_relation, observe_fleet_desired_revision, observe_local_tree_revision }
import gunbc.fleet_desired_observe { observe_fleet_revision_relation, observe_fleet_desired_revision, local_tree_revision, LocalTreeRevisionObserved, LocalTreeRevisionUnobserved }
import gunbc.fleet_main_revision {
FleetDesiredAdvanceInput,
FleetDesiredExpectationRefusedInput,
Expand Down Expand Up @@ -404,10 +404,10 @@ func admit_fleet_desired_from_floor_event_wet() -> ProcessExit {
) {
AdmissionRefused { cause: why } => exit_failure(reason: why)
AdmissionActionAuthorized { action: action } =>
match observe_local_tree_revision(repo: fleet_desired_admission_repo_root) {
Absent =>
match local_tree_revision(repo: fleet_desired_admission_repo_root) {
LocalTreeRevisionUnobserved { cause: _ } =>
exit_failure(reason: fleet_desired_unobservable_judge_source_refusal_reason())
Present { value: judge_source_observed } => {
LocalTreeRevisionObserved { revision: judge_source_observed } => {
let wrote = Filesystem.Write(
path: fleet_desired_admission_authorized_action_path,
content: fleet_desired_authorized_action_script(
Expand Down
39 changes: 27 additions & 12 deletions dag/gunbc/fleet/fleet_desired_observe.dag
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,33 @@ fn observe_fleet_revision_relation(
)
}

// The local half of the comparison: what revision THIS host's deployed tree is at.
// Absent (typed, located in the cause the caller renders) when the repo path is not a
// git repository or HEAD does not decode — never a fabricated revision.
fn observe_local_tree_revision(repo: FilePath) -> GitObjectId? {
// THE DEPLOYED TREE'S OWN REVISION, read locally and compared to nothing: which software is acting.
// This is the fact the roadmap consumes -- a launch identity names the code that dispatched it, a
// tick receipt names the code that ticked -- and it is NOT convergence. Whether this revision equals
// the admitted fleet ref is the deploy path's question, answered by FleetRevisionStanding below for
// the converge CLI and the deployed-tree report. Dispatch and the belt do not consult it (operator
// ruling, 2026-09-05): desired advances on every green merge and a deploy takes minutes, so a gate
// demanding equality was satisfiable only in the gap between two merges, and the tick refusing
// under drift turned every merge into a pipeline outage on the host. Unreadable still refuses: a
// dispatch that cannot name the code it runs has no identity to bind.
type LocalTreeRevision
= LocalTreeRevisionObserved { revision: GitObjectId }
| LocalTreeRevisionUnobserved { cause: String }

fn local_tree_revision(repo: FilePath) -> LocalTreeRevision {
let read = git.Core.RevParseInRepo(repo: repo, target: "HEAD" as GitRef)
if read.exit_code == 0 {
git_object_id_from_untagged_hex(hex: trim(s: read.revision))
match git_object_id_from_untagged_hex(hex: trim(s: read.revision)) {
Present { value: oid } => LocalTreeRevisionObserved { revision: oid }
Absent =>
LocalTreeRevisionUnobserved {
cause: concat("fleet_desired_observe: HEAD is not a decodable object id: ", trim(s: read.revision)),
}
}
} else {
none
LocalTreeRevisionUnobserved {
cause: "fleet_desired_observe: local tree revision unreadable (not a git repository, or HEAD undecodable)",
}
}
}

Expand All @@ -200,17 +218,14 @@ fn admitted_revision_standing(repo: FilePath, remote: String, ref_name: GitRef)
let observed = observe_admitted_revision(repo: repo, remote: remote, ref_name: ref_name)
match converge_target_revision(observed: observed) {
Present { value: desired } =>
match observe_local_tree_revision(repo: repo) {
Present { value: local } =>
match local_tree_revision(repo: repo) {
LocalTreeRevisionObserved { revision: local } =>
if git_object_id_eq(left: desired, right: local) {
RevisionConverged { revision: desired }
} else {
RevisionDrifted { desired: desired, local: local }
}
Absent =>
RevisionUnobserved {
cause: "fleet_desired_observe: local tree revision unreadable (not a git repository, or HEAD undecodable)",
}
LocalTreeRevisionUnobserved { cause } => RevisionUnobserved { cause: cause }
}
Absent =>
match observed {
Expand Down
81 changes: 31 additions & 50 deletions dag/gunbc/roadmap/roadmap_belt_actuate.dag
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ import gunbc.roadmap_belt {
import gunbc.roadmap_launch_admission {
LaunchCause, Operator, Timer,
LaunchRefusal,
LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchRevisionDrift,
LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved,
LaunchAuthorityUnobserved, LaunchNodeUnknown, LaunchNodeSuperseded, LaunchAlreadyAccepted, LaunchReviewRequired,
LaunchSizeMissing, LaunchExecutionContractMissing, LaunchDependenciesBlocked, LaunchSessionsUnobservable,
LaunchAlreadyLive, LaunchStaleSession, LaunchLivenessUnobserved, LaunchSessionMultiplicityConflict,
LaunchNoCapacity, LaunchCauseNotPermitted, LaunchGateNotEvaluated,
LaunchAdmission, LaunchAdmitted, LaunchRefused,
LaunchHostStanding, LaunchHostObserved, LaunchHostObserveOnly, LaunchHostHalted, LaunchHostUnobserved,
launch_host_halt_transition, launch_host_stage_revision, LaunchHostRevisionStage, HostRevisionConverged, HostRevisionHalted,
launch_host_halt_transition, launch_host_stage_revision, LaunchHostRevisionStage, HostRevisionObserved, HostRevisionHalted,
launch_host_refusal,
LaunchSessionsStanding, LaunchSessionsObserved, LaunchSessionsUnobserved, LaunchSessionsPending,
launch_host_with_sessions, LaunchStagedAdmission, LaunchStageDecided, LaunchStageNeedsSessions, launch_admission_staged,
Expand Down Expand Up @@ -363,9 +363,9 @@ import gunbc.git_diff_change_window {
}
import extdeps.git.object_store { git_object_id_from_untagged_hex, git_object_id_wire_hex }
import gunbc.fleet_desired_observe {
admitted_revision_standing,
local_tree_revision,
FleetRevisionStanding,
RevisionConverged, RevisionDrifted, RevisionUnobserved,
LocalTreeRevisionObserved, LocalTreeRevisionUnobserved,
}
import extdeps.git { shape_git_rev_parse_head_args, shape_git_status_porcelain_nul_args, shape_git_add_all_args, shape_git_commit_message_args }
import gunbc.roadmap.roadmap_belt_commit { BeltCommitDecision, CommitNeeded, CommitNotNeeded, belt_commit_decision }
Expand Down Expand Up @@ -5868,30 +5868,27 @@ fn belt_session_liveness_from_process(
// matched BeltSessionLiveness directly. Consumers match the coproduct.
data belt_dispatch_wire_contract_exemplar_provider: String = "codex"

// DISPATCH READS BACK THE DEPLOYED REVISION BEFORE IT ACTS, because every fact this path consumes —
// node roster, signoff, contract, the command it runs — comes from the deployed tree.
// `gunbc.fleet_desired_observe` `fleet_revision_standing` already decided that cell three ways, and
// its ONLY consumer was `gunbc.fleet_converge_cli`: convergence knew the host was drifted while the
// served page dispatched against the drifted tree, saying nothing. Not a stale display — a worker
// spawned from code the fleet never admitted, reported as an ordinary spawn.
//
// So the standing is consulted FIRST — before node lookup, session observation, worktree — because
// a roster from an unadmitted tree cannot be trusted to name the right node, and refusing later
// means refusing after acting on it.
//
// BOTH non-converged arms refuse, neither an absorbing widen: `RevisionDrifted` carries BOTH
// revisions (the membership-diff `from` ruling — a catch-up needs the prior, a receipt must name
// what it observed); `RevisionUnobserved` carries its cause and is unobserved-GRADE, never
// satisfied. Dispatching on `RevisionUnobserved` would be the empty-observation narrow:
// could-not-compare rendered as nothing-is-wrong.
// DISPATCH READS BACK THE DEPLOYED REVISION BEFORE IT ACTS, and binds it into the launch identity:
// every fact this path consumes -- node roster, signoff, contract, the command it runs -- comes from
// the deployed tree, so an attempt must name the code that produced it. What it does NOT do, as of
// the 2026-09-05 operator ruling, is refuse when that revision differs from the admitted fleet ref.
// It did: `fleet_revision_standing` was consulted first and both non-converged arms refused, on the
// argument that a roster from an unadmitted tree cannot be trusted to name the right node. The
// argument priced the wrong side. The worker checks out origin/main itself, the belt verifies
// against main, publication opens a PR against main -- convergence decides only which version of
// the CONTROL software runs, and desired advances on every green merge while a deploy takes
// minutes, so equality held only in the gap between two merges. Because the tick shared the gate,
// a host one merge behind stopped verifying, reviewing and publishing as well: convergence lag
// became a pipeline outage. Drift is a host fact for the deployed-tree report and the deploy path;
// here only an UNREADABLE revision refuses, because an attempt with no code to name has no identity.
//
// The repo path is `instance.repo_root`, not the `srv1_gunbc_repo_root` constant
// `fleet_converge_cli` uses: the gate is a fact about the instance dispatched, and hardcoding srv1
// `fleet_converge_cli` uses: the read is a fact about the instance dispatched, and hardcoding srv1
// would mint a second path authority answering for the wrong host on any other instance.
// THE TRANSITION GATE DOMINATES THE REVISION GATE; the order is the safety property. A repository
// convergence advances the base ref BEFORE it transitions the worktree, so a converge that died in
// between leaves the ref at the admitted revision over a tree that never moved — and
// `fleet_revision_standing` reads that ref. The revision gate would answer CONVERGED and admit a
// a revision read alone would see an ordinary HEAD and admit a
// dispatch into exactly the half-transitioned tree the belt must stay out of. Asking the inhibition
// first stops a gate being satisfied by the artifact of the failure it should notice.
//
Expand All @@ -5912,7 +5909,7 @@ fn belt_transition_admission_for_instance(instance: HostDashboardInstance) -> Ac
// THE ONE STAGED OBSERVER behind the page, POST /dispatch and the timer. It reads in gate order and
// stops at the first refusal: instance posture is a model fact (no effect); the transition file is
// read only for an actuating instance; the fleet revision only under an admitted transition; tmux
// only under a converged revision. Each halt is a LaunchHostHalted whose later fields do not exist,
// only under an observed revision. Each halt is a LaunchHostHalted whose later fields do not exist,
// so `tmux listed under an inhibited transition` is not a state it can produce (DESIGN 4b: no
// constructor). The refusal in a halt is derived by the admission's own gate functions
// (launch_host_halt_transition, launch_host_stage_revision), never spelled here.
Expand All @@ -5930,14 +5927,10 @@ fn belt_launch_host_standing_for_instance_with_capacity(
ActuationAdmitted =>
match launch_host_stage_revision(
instance_id: instance_id,
revision: admitted_revision_standing(
repo: instance.repo_root,
remote: instance.admitted_revision.remote,
ref_name: instance.admitted_revision.ref_name,
),
revision: local_tree_revision(repo: instance.repo_root),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve per-instance admission refs

For actuating instances with operator-controlled admission refs, this replacement bypasses the explicit launch grant rather than merely tolerating fleet deployment lag. macbook_local_dashboard_instance uses refs/gunbc/local-admitted, and srv2_deploy_dashboard_instance uses refs/gunbc/srv2-admitted; both are documented as refs the operator must advance deliberately. With either ref absent or stale, a readable HEAD now passes this stage, allowing an eligible POST dispatch to spawn work anyway. Limit the drift relaxation to the fleet-managed source, or otherwise retain comparison against these local admission refs.

Useful? React with 👍 / 👎.

) {
HostRevisionHalted { standing } => standing
HostRevisionConverged { revision } =>
HostRevisionObserved { revision } =>
LaunchHostObserved {
instance_id: instance.instance_id,
actuation: DashboardActuating,
Expand Down Expand Up @@ -6178,7 +6171,6 @@ fn launch_refusal_band(r: LaunchRefusal) -> DispatchBand {
LaunchCauseNotPermitted { cause: _, mode: _ } => BandFail
LaunchTransitionInhibited { reason: _ } => BandLoud
LaunchRevisionUnobserved { cause: _ } => BandLoud
LaunchRevisionDrift { desired: _, local: _ } => BandLoud
LaunchAuthorityUnobserved { detail: _ } => BandLoud
LaunchSessionsUnobservable { reason: _ } => BandLoud
LaunchStaleSession { session_name: _, detail: _ } => BandLoud
Expand Down Expand Up @@ -7090,35 +7082,24 @@ fn belt_verification_presentation_detail(
}

// THE EXECUTION STANDING IS OBSERVED AT THE MINT SITE from the instance the tick ran for and the
// revision standing of that instance's tree -- the same admitted_revision_standing read the
// dispatch preflight makes, against the same per-instance admitting ref, because a tick receipt
// that answered from a different authority than the gate would report a revision the gate never
// admitted -- and the mode is the production row the tick itself consumed. A drifted or
// unobservable revision withholds the binding rather than recording a revision the tick did not
// serve.
// deployed tree's own revision -- the same local_tree_revision read the launch gate and the
// dispatch preflight make, because a tick receipt that answered from a different authority than
// the gate would report a revision the gate never bound -- and the mode is the production row the
// tick itself consumed. An unreadable revision withholds the binding rather than recording a
// revision the tick did not serve. Drift from the fleet ref is not consulted (operator ruling,
// 2026-09-05; see launch_revision_outcome).
fn belt_tick_execution_standing_for_instance(
instance: HostDashboardInstance,
mode: SpawnMode,
) -> BeltTickExecutionStanding {
match admitted_revision_standing(
repo: instance.repo_root,
remote: instance.admitted_revision.remote,
ref_name: instance.admitted_revision.ref_name,
) {
RevisionConverged { revision: r } =>
match local_tree_revision(repo: instance.repo_root) {
LocalTreeRevisionObserved { revision: r } =>
TickExecuted {
instance_id: instance.instance_id as String,
deployed_revision: git_object_id_wire_hex(oid: r) as String,
spawn_mode: mode,
}
RevisionDrifted { desired: d, local: l } =>
TickExecutionWithheld {
refusal: join([
"revision drifted: desired ", git_object_id_wire_hex(oid: d) as String,
" local ", git_object_id_wire_hex(oid: l) as String,
], ""),
}
RevisionUnobserved { cause: c } =>
LocalTreeRevisionUnobserved { cause: c } =>
TickExecutionWithheld { refusal: join(["revision unobserved: ", c], "") }
}
}
Expand Down
Loading
Loading