Repository navigation
A fresh host can be deployed: an empty repository root is bootstrapped to the candidate and read back as that revision - #10722
Conversation
…d to the candidate and read back as that revision
gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall declared this OutsideTheLadder
with a bounded population of one -- deployment_spec_srv1 at a fresh repository root -- and it is the
reason a new host could not be deployed at all. The tree member refuses with
RepositoryBootstrapUnavailable because a Create would leave a non-repository directory that
Git-native convergence then refuses forever, turning a recoverable absence into a permanent one.
THE PRE-STATE REFUSAL IS NOT RELAXED; ONE OF ITS CAUSES IS ROUTED. ConvergencePreStateUnobservable
fired for two states with opposite remedies: a repository whose refs cannot be read is BROKEN and the
remedy is a person, while a root with nothing in it is UNBORN and the remedy is to create the
repository. A non-empty root and an unobservable one still refuse with the identical typed cause. The
stall rules out relaxing that refusal, and this does not relax it -- it separates two states the
refusal had collapsed.
THE DISTINCTION IS OBSERVED, NOT SUPPLIED. The deploy that ordered the transition established the
tree's absence by listing its parent, but this process reads the root itself rather than taking a
caller's word about host state it can observe -- the operand would be a second account of a fact the
filesystem already holds. Emptiness is the test rather than "is there a .git": a directory holding
files but no repository is the rsync-era partial state, whose bytes are already wrong and whose
repair is not `git init`.
BOTH NEW WRITES ARE FAIL-CLOSED READINGS OF THE CLAIM THE ARM MAKES. The base ref is created with an
empty expected-old, which is git's "this ref must not exist", and the worktree moves by `checkout -b`,
which refuses a branch that already exists. If "nothing was here before" is false, the bootstrap
refuses rather than initializing over somebody's repository.
THE READBACK IS THE ONLY VERDICT. Every step can exit zero and leave a repository that is not at the
candidate -- exactly how the rsync-era tree was assembled -- so the outcome comes from a fresh
observation through repository_convergence_verdict, the same authority the ordinary path uses rather
than a bootstrap-specific notion of success. `before` and `after` are deliberately the same
observation: an empty root had no refs or worktrees to preserve, so the preservation comparison is
vacuous rather than skipped.
The branch is gunbc_repository.default_branch, not a literal "main" -- that authority's own header
refuses a sixth nickname for which repository this project is.
EVIDENCE IS WET, because nothing else can close this stall: its trigger asks for HEAD, index and
tracked worktree read back as one revision, and every step can exit zero without that holding. Two
rows in test.claim.repository_bootstrap_wet_witness_test, both PASS by execution:
an empty root is bootstrapped and reads back as that revision
-- HEAD at the candidate, `git diff --quiet HEAD` clean, tracked file holding the candidate bytes,
all checked from OUTSIDE the transition rather than from its exit status
a root holding content without a repository still refuses and is not initialized
-- the discriminator: without it the first row would pass equally well had the refusal simply been
deleted. The stray file is untouched and no .git appeared.
The fixture authors no answer: it builds a source repository with ordinary git and leaves the target
as a directory holding nothing; every fact the assertions read comes from the transition's own
observation chain.
WHAT THIS DOES NOT DO: the tree member still refuses absence with RepositoryBootstrapUnavailable.
Flipping that arm to Create belongs in its own commit now that the capability is proven; landing it
here would make a fresh host depend on a capability in the same change that introduced it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
CI note: the The failure is Those citations are Every branch cut from current main inherits this, so #10724 and #10732 carry it too. |
…nly the repository-convergence oneshot remains (#10724) * A fresh host can be deployed: an empty repository root is bootstrapped to the candidate and read back as that revision gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall declared this OutsideTheLadder with a bounded population of one -- deployment_spec_srv1 at a fresh repository root -- and it is the reason a new host could not be deployed at all. The tree member refuses with RepositoryBootstrapUnavailable because a Create would leave a non-repository directory that Git-native convergence then refuses forever, turning a recoverable absence into a permanent one. THE PRE-STATE REFUSAL IS NOT RELAXED; ONE OF ITS CAUSES IS ROUTED. ConvergencePreStateUnobservable fired for two states with opposite remedies: a repository whose refs cannot be read is BROKEN and the remedy is a person, while a root with nothing in it is UNBORN and the remedy is to create the repository. A non-empty root and an unobservable one still refuse with the identical typed cause. The stall rules out relaxing that refusal, and this does not relax it -- it separates two states the refusal had collapsed. THE DISTINCTION IS OBSERVED, NOT SUPPLIED. The deploy that ordered the transition established the tree's absence by listing its parent, but this process reads the root itself rather than taking a caller's word about host state it can observe -- the operand would be a second account of a fact the filesystem already holds. Emptiness is the test rather than "is there a .git": a directory holding files but no repository is the rsync-era partial state, whose bytes are already wrong and whose repair is not `git init`. BOTH NEW WRITES ARE FAIL-CLOSED READINGS OF THE CLAIM THE ARM MAKES. The base ref is created with an empty expected-old, which is git's "this ref must not exist", and the worktree moves by `checkout -b`, which refuses a branch that already exists. If "nothing was here before" is false, the bootstrap refuses rather than initializing over somebody's repository. THE READBACK IS THE ONLY VERDICT. Every step can exit zero and leave a repository that is not at the candidate -- exactly how the rsync-era tree was assembled -- so the outcome comes from a fresh observation through repository_convergence_verdict, the same authority the ordinary path uses rather than a bootstrap-specific notion of success. `before` and `after` are deliberately the same observation: an empty root had no refs or worktrees to preserve, so the preservation comparison is vacuous rather than skipped. The branch is gunbc_repository.default_branch, not a literal "main" -- that authority's own header refuses a sixth nickname for which repository this project is. EVIDENCE IS WET, because nothing else can close this stall: its trigger asks for HEAD, index and tracked worktree read back as one revision, and every step can exit zero without that holding. Two rows in test.claim.repository_bootstrap_wet_witness_test, both PASS by execution: an empty root is bootstrapped and reads back as that revision -- HEAD at the candidate, `git diff --quiet HEAD` clean, tracked file holding the candidate bytes, all checked from OUTSIDE the transition rather than from its exit status a root holding content without a repository still refuses and is not initialized -- the discriminator: without it the first row would pass equally well had the refusal simply been deleted. The stray file is untouched and no .git appeared. The fixture authors no answer: it builds a source repository with ordinary git and leaves the target as a directory holding nothing; every fact the assertions read comes from the transition's own observation chain. WHAT THIS DOES NOT DO: the tree member still refuses absence with RepositoryBootstrapUnavailable. Flipping that arm to Create belongs in its own commit now that the capability is proven; landing it here would make a fresh host depend on a capability in the same change that introduced it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY * The publication member stops writing two programs to one unit name: only the repository-convergence oneshot remains THE DEPLOY WROTE THE SAME UNIT FILE TWICE, WITH DIFFERENT PROGRAMS, ON EVERY RUN. The publication member wrote the rsync tree-sync unit, installed it, reloaded systemd and restarted it; then wrote the repository-convergence unit TO THE SAME PATH UNDER THE SAME UNIT NAME (apply_repository_convergence_unit_write_step stages under spec.names.tree_sync_unit_name), reloaded, and restarted it again. One name, two programs, sequentially, in a single deployment. IT WAS ALREADY DEAD BY DECLARATION. gunbc.live_deploy.deployed_tree_scope binds deployed_tree_repository_transition = GitNativeConvergence and repository_transition_admission admits it, while the emitter kept emitting and running rsync anyway. That module's own header says a PR comment is not a wall; the wall was declared and the route it named was never cut. This is the cut. WHAT REMAINS IS THE THIN ACTUATOR the re-scope asked for, carrying only what lies outside the transition's authority: the directories, the environment file the convergence ExecStart reads, the slice it joins, the receipts directory, one unit written, one daemon-reload, one restart as the service user, and the readback. Sixteen steps to twelve, one program per unit. Repository existence, empty-root discrimination, candidate installation, HEAD/index/tracked-byte agreement and refusal of non-empty non-repositories stay owned by the transition proven wet in #10722; nothing here re-observes or reinterprets them. DELETED, all orphaned by the cut: apply_tree_sync_unit_write_step, emit_tree_sync_unit_doc, live_deploy_tree_sync_unit_file, live_deploy_tree_sync_unit_for, and the GunbcSourceTree arm of emit_artifact_upsert -- which was already unreachable, since that kind renders its own member commands and is therefore excluded from the membership reconcile. WITNESSES REPOINTED WHERE THE CLAIM SURVIVED, RETIRED ONLY WHERE THE SUBJECT DIED: - apply_script_and_tree_sync_unit_stay_within_ci_runner_grants -> apply_script_stays_within_ci_runner_grants. The grant boundary was the claim; the unit-byte pin was not. It is now STRICTLY STRONGER: `rsync` is forbidden anywhere in the emission rather than only in the spellings the old unit avoided. - the git-metadata-as-files wall keeps its positive control and its discriminating red; only the negative arm moves from the deleted unit to the whole apply script. - retired: the byte oracle over the rsync ExecStart, seven tree-sync decisions (principal, slice, environment file, ExecStart, oneshot, installable, description) and their two red controls. Each lost its subject, and a decision over a unit no producer emits is permanently green while reading as coverage of a route that does not run. The principal fact those decisions protected is asserted by repository_convergence_placement's own witnesses, over the unit that now runs. EVIDENCE: apply closure resolves with zero diagnostics; emit_test 59/59; the emission oracle 31/33. The two failures -- the_serve_unit_matches_the_deployed_bytes and serve_unit_execstart_carries_admitted_entrypoint -- were MEASURED FAILING ON THE PRE-CUT TREE as well, so they are pre-existing and not this change. They are deployed-bytes oracles, and the fleet desired ref is currently pinned five commits behind main while main is red, which is enough on its own to drift them. RESIDUE, DECLARED RATHER THAN FIXED: the unit is still NAMED tree-sync. tree_sync_unit_name, tree_sync_unit_path, tree_sync_env_path and tree_sync_restart_step_with_diagnosis now all name a unit that runs repository convergence -- a section 3 nickname. Renaming a live systemd unit is a member-lifecycle migration: the old unit must be stopped and removed on the host, which is its own change with its own failure modes, and folding it in here would put two claims in one review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…d to the candidate and read back as that revision (#10722) gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall declared this OutsideTheLadder with a bounded population of one -- deployment_spec_srv1 at a fresh repository root -- and it is the reason a new host could not be deployed at all. The tree member refuses with RepositoryBootstrapUnavailable because a Create would leave a non-repository directory that Git-native convergence then refuses forever, turning a recoverable absence into a permanent one. THE PRE-STATE REFUSAL IS NOT RELAXED; ONE OF ITS CAUSES IS ROUTED. ConvergencePreStateUnobservable fired for two states with opposite remedies: a repository whose refs cannot be read is BROKEN and the remedy is a person, while a root with nothing in it is UNBORN and the remedy is to create the repository. A non-empty root and an unobservable one still refuse with the identical typed cause. The stall rules out relaxing that refusal, and this does not relax it -- it separates two states the refusal had collapsed. THE DISTINCTION IS OBSERVED, NOT SUPPLIED. The deploy that ordered the transition established the tree's absence by listing its parent, but this process reads the root itself rather than taking a caller's word about host state it can observe -- the operand would be a second account of a fact the filesystem already holds. Emptiness is the test rather than "is there a .git": a directory holding files but no repository is the rsync-era partial state, whose bytes are already wrong and whose repair is not `git init`. BOTH NEW WRITES ARE FAIL-CLOSED READINGS OF THE CLAIM THE ARM MAKES. The base ref is created with an empty expected-old, which is git's "this ref must not exist", and the worktree moves by `checkout -b`, which refuses a branch that already exists. If "nothing was here before" is false, the bootstrap refuses rather than initializing over somebody's repository. THE READBACK IS THE ONLY VERDICT. Every step can exit zero and leave a repository that is not at the candidate -- exactly how the rsync-era tree was assembled -- so the outcome comes from a fresh observation through repository_convergence_verdict, the same authority the ordinary path uses rather than a bootstrap-specific notion of success. `before` and `after` are deliberately the same observation: an empty root had no refs or worktrees to preserve, so the preservation comparison is vacuous rather than skipped. The branch is gunbc_repository.default_branch, not a literal "main" -- that authority's own header refuses a sixth nickname for which repository this project is. EVIDENCE IS WET, because nothing else can close this stall: its trigger asks for HEAD, index and tracked worktree read back as one revision, and every step can exit zero without that holding. Two rows in test.claim.repository_bootstrap_wet_witness_test, both PASS by execution: an empty root is bootstrapped and reads back as that revision -- HEAD at the candidate, `git diff --quiet HEAD` clean, tracked file holding the candidate bytes, all checked from OUTSIDE the transition rather than from its exit status a root holding content without a repository still refuses and is not initialized -- the discriminator: without it the first row would pass equally well had the refusal simply been deleted. The stray file is untouched and no .git appeared. The fixture authors no answer: it builds a source repository with ordinary git and leaves the target as a directory holding nothing; every fact the assertions read comes from the transition's own observation chain. WHAT THIS DOES NOT DO: the tree member still refuses absence with RepositoryBootstrapUnavailable. Flipping that arm to Create belongs in its own commit now that the capability is proven; landing it here would make a fresh host depend on a capability in the same change that introduced it. Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…nly the repository-convergence oneshot remains (#10724) * A fresh host can be deployed: an empty repository root is bootstrapped to the candidate and read back as that revision gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall declared this OutsideTheLadder with a bounded population of one -- deployment_spec_srv1 at a fresh repository root -- and it is the reason a new host could not be deployed at all. The tree member refuses with RepositoryBootstrapUnavailable because a Create would leave a non-repository directory that Git-native convergence then refuses forever, turning a recoverable absence into a permanent one. THE PRE-STATE REFUSAL IS NOT RELAXED; ONE OF ITS CAUSES IS ROUTED. ConvergencePreStateUnobservable fired for two states with opposite remedies: a repository whose refs cannot be read is BROKEN and the remedy is a person, while a root with nothing in it is UNBORN and the remedy is to create the repository. A non-empty root and an unobservable one still refuse with the identical typed cause. The stall rules out relaxing that refusal, and this does not relax it -- it separates two states the refusal had collapsed. THE DISTINCTION IS OBSERVED, NOT SUPPLIED. The deploy that ordered the transition established the tree's absence by listing its parent, but this process reads the root itself rather than taking a caller's word about host state it can observe -- the operand would be a second account of a fact the filesystem already holds. Emptiness is the test rather than "is there a .git": a directory holding files but no repository is the rsync-era partial state, whose bytes are already wrong and whose repair is not `git init`. BOTH NEW WRITES ARE FAIL-CLOSED READINGS OF THE CLAIM THE ARM MAKES. The base ref is created with an empty expected-old, which is git's "this ref must not exist", and the worktree moves by `checkout -b`, which refuses a branch that already exists. If "nothing was here before" is false, the bootstrap refuses rather than initializing over somebody's repository. THE READBACK IS THE ONLY VERDICT. Every step can exit zero and leave a repository that is not at the candidate -- exactly how the rsync-era tree was assembled -- so the outcome comes from a fresh observation through repository_convergence_verdict, the same authority the ordinary path uses rather than a bootstrap-specific notion of success. `before` and `after` are deliberately the same observation: an empty root had no refs or worktrees to preserve, so the preservation comparison is vacuous rather than skipped. The branch is gunbc_repository.default_branch, not a literal "main" -- that authority's own header refuses a sixth nickname for which repository this project is. EVIDENCE IS WET, because nothing else can close this stall: its trigger asks for HEAD, index and tracked worktree read back as one revision, and every step can exit zero without that holding. Two rows in test.claim.repository_bootstrap_wet_witness_test, both PASS by execution: an empty root is bootstrapped and reads back as that revision -- HEAD at the candidate, `git diff --quiet HEAD` clean, tracked file holding the candidate bytes, all checked from OUTSIDE the transition rather than from its exit status a root holding content without a repository still refuses and is not initialized -- the discriminator: without it the first row would pass equally well had the refusal simply been deleted. The stray file is untouched and no .git appeared. The fixture authors no answer: it builds a source repository with ordinary git and leaves the target as a directory holding nothing; every fact the assertions read comes from the transition's own observation chain. WHAT THIS DOES NOT DO: the tree member still refuses absence with RepositoryBootstrapUnavailable. Flipping that arm to Create belongs in its own commit now that the capability is proven; landing it here would make a fresh host depend on a capability in the same change that introduced it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY * The publication member stops writing two programs to one unit name: only the repository-convergence oneshot remains THE DEPLOY WROTE THE SAME UNIT FILE TWICE, WITH DIFFERENT PROGRAMS, ON EVERY RUN. The publication member wrote the rsync tree-sync unit, installed it, reloaded systemd and restarted it; then wrote the repository-convergence unit TO THE SAME PATH UNDER THE SAME UNIT NAME (apply_repository_convergence_unit_write_step stages under spec.names.tree_sync_unit_name), reloaded, and restarted it again. One name, two programs, sequentially, in a single deployment. IT WAS ALREADY DEAD BY DECLARATION. gunbc.live_deploy.deployed_tree_scope binds deployed_tree_repository_transition = GitNativeConvergence and repository_transition_admission admits it, while the emitter kept emitting and running rsync anyway. That module's own header says a PR comment is not a wall; the wall was declared and the route it named was never cut. This is the cut. WHAT REMAINS IS THE THIN ACTUATOR the re-scope asked for, carrying only what lies outside the transition's authority: the directories, the environment file the convergence ExecStart reads, the slice it joins, the receipts directory, one unit written, one daemon-reload, one restart as the service user, and the readback. Sixteen steps to twelve, one program per unit. Repository existence, empty-root discrimination, candidate installation, HEAD/index/tracked-byte agreement and refusal of non-empty non-repositories stay owned by the transition proven wet in #10722; nothing here re-observes or reinterprets them. DELETED, all orphaned by the cut: apply_tree_sync_unit_write_step, emit_tree_sync_unit_doc, live_deploy_tree_sync_unit_file, live_deploy_tree_sync_unit_for, and the GunbcSourceTree arm of emit_artifact_upsert -- which was already unreachable, since that kind renders its own member commands and is therefore excluded from the membership reconcile. WITNESSES REPOINTED WHERE THE CLAIM SURVIVED, RETIRED ONLY WHERE THE SUBJECT DIED: - apply_script_and_tree_sync_unit_stay_within_ci_runner_grants -> apply_script_stays_within_ci_runner_grants. The grant boundary was the claim; the unit-byte pin was not. It is now STRICTLY STRONGER: `rsync` is forbidden anywhere in the emission rather than only in the spellings the old unit avoided. - the git-metadata-as-files wall keeps its positive control and its discriminating red; only the negative arm moves from the deleted unit to the whole apply script. - retired: the byte oracle over the rsync ExecStart, seven tree-sync decisions (principal, slice, environment file, ExecStart, oneshot, installable, description) and their two red controls. Each lost its subject, and a decision over a unit no producer emits is permanently green while reading as coverage of a route that does not run. The principal fact those decisions protected is asserted by repository_convergence_placement's own witnesses, over the unit that now runs. EVIDENCE: apply closure resolves with zero diagnostics; emit_test 59/59; the emission oracle 31/33. The two failures -- the_serve_unit_matches_the_deployed_bytes and serve_unit_execstart_carries_admitted_entrypoint -- were MEASURED FAILING ON THE PRE-CUT TREE as well, so they are pre-existing and not this change. They are deployed-bytes oracles, and the fleet desired ref is currently pinned five commits behind main while main is red, which is enough on its own to drift them. RESIDUE, DECLARED RATHER THAN FIXED: the unit is still NAMED tree-sync. tree_sync_unit_name, tree_sync_unit_path, tree_sync_env_path and tree_sync_restart_step_with_diagnosis now all name a unit that runs repository convergence -- a section 3 nickname. Renaming a live systemd unit is a member-lifecycle migration: the old unit must be stopped and removed on the host, which is its own change with its own failure modes, and folding it in here would put two claims in one review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…nary Create #10722 built the Git-native bootstrap and proved it wet; #10724 made the publication member invoke that oneshot and nothing else. But the tree member still refused absence with RepositoryBootstrapUnavailable, so the decision was made BEFORE the transition could run and a fresh host stayed undeployable. The capability had no invoker. That sequencing was deliberate -- a fresh host must not depend on a capability introduced in the same change -- and this is the commit it was deferred to. THE ABSENT ARM NEEDS NO MEMBER SPECIAL CASE AT ALL now. decide_member collapses to `Create { member, to: desired }` for every member; the tree is no longer singled out. REPOSITORYBOOTSTRAPUNAVAILABLE IS DELETED, NOT LEFT UNREACHABLE, and the reason is what its text said. It read: "the repository root is absent and no bootstrap transition exists". That sentence was true when written and is now false. A refusal cause whose message denies a capability shipping in the same tree is worse than dead code -- the next author reads it as a standing limitation. THE WALL DID NOT DISAPPEAR, IT MOVED TO WHERE IT CAN BE CHECKED. A fresh host is no longer refused at PLANNING time on a stale claim; it is refused at EXECUTION time by the transition, on observation: a non-empty root refuses with the original pre-state cause, `checkout -b` refuses an existing branch, an empty expected-old refuses an existing ref, and the readback refuses a repository that is not at the candidate. Construction over declaration. THE STALL CLIMBS ONE RUNG, NOT TWO. gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall goes OutsideTheLadder -> Mitigatable. It is no longer outside the ladder: the operation has a route and it executes. It is NOT MechanicallyPreventable, and the distinction is the enrolment of the evidence rather than the quality of the wall -- the wet witnesses perform host effects, so the hermetic floor refuses them at its boundary and counts them executed while they assert nothing. Reporting MechanicallyPreventable would cite a wall that does not run (DESIGN 4b(1)). The next-rung trigger is now exactly that: enrol those two rows in a lane that executes them. EVIDENCE, and it is split into two discriminating halves rather than one end-to-end assertion: member_identity_witness an_absent_repository_plans_the_tree_publication -- absence is a Create whose plan publishes; a plan that still refused fails the first arm, and one that derived nothing for the tree fails the second, because a bootstrap planning no publication is the fresh host that installs nothing and reports success. emit_test the_publication_member_emits_convergence_and_no_rsync -- the publishing plan's tree fragment carries the convergence invocation and its readback, and no rsync. This is #10724's outstanding acceptance item: that the ACTUATOR reaches the transition, which is a different claim from the transition working. A single end-to-end witness over a full-install plan would have passed for reasons neither half pins down, because a wholesale install emits the same line whatever the observation was. THE UNIT-NAME RESIDUE IS RECORDED, NOT ASSERTED AWAY. The first draft of that witness also asserted `gunbc-tree-sync.service` is absent from the fragment. It is not: the convergence unit is still WRITTEN TO the tree-sync path, the section 3 nickname #10724 declared as residue rather than repaired. Asserting its absence would assert a member-lifecycle migration that has not happened. Verified on srv1: apply closure resolves with zero diagnostics, member_identity 11/11, emit_test 59/59 plus the corrected actuator claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY
…nary Create (#10760) #10722 built the Git-native bootstrap and proved it wet; #10724 made the publication member invoke that oneshot and nothing else. But the tree member still refused absence with RepositoryBootstrapUnavailable, so the decision was made BEFORE the transition could run and a fresh host stayed undeployable. The capability had no invoker. That sequencing was deliberate -- a fresh host must not depend on a capability introduced in the same change -- and this is the commit it was deferred to. THE ABSENT ARM NEEDS NO MEMBER SPECIAL CASE AT ALL now. decide_member collapses to `Create { member, to: desired }` for every member; the tree is no longer singled out. REPOSITORYBOOTSTRAPUNAVAILABLE IS DELETED, NOT LEFT UNREACHABLE, and the reason is what its text said. It read: "the repository root is absent and no bootstrap transition exists". That sentence was true when written and is now false. A refusal cause whose message denies a capability shipping in the same tree is worse than dead code -- the next author reads it as a standing limitation. THE WALL DID NOT DISAPPEAR, IT MOVED TO WHERE IT CAN BE CHECKED. A fresh host is no longer refused at PLANNING time on a stale claim; it is refused at EXECUTION time by the transition, on observation: a non-empty root refuses with the original pre-state cause, `checkout -b` refuses an existing branch, an empty expected-old refuses an existing ref, and the readback refuses a repository that is not at the candidate. Construction over declaration. THE STALL CLIMBS ONE RUNG, NOT TWO. gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall goes OutsideTheLadder -> Mitigatable. It is no longer outside the ladder: the operation has a route and it executes. It is NOT MechanicallyPreventable, and the distinction is the enrolment of the evidence rather than the quality of the wall -- the wet witnesses perform host effects, so the hermetic floor refuses them at its boundary and counts them executed while they assert nothing. Reporting MechanicallyPreventable would cite a wall that does not run (DESIGN 4b(1)). The next-rung trigger is now exactly that: enrol those two rows in a lane that executes them. EVIDENCE, and it is split into two discriminating halves rather than one end-to-end assertion: member_identity_witness an_absent_repository_plans_the_tree_publication -- absence is a Create whose plan publishes; a plan that still refused fails the first arm, and one that derived nothing for the tree fails the second, because a bootstrap planning no publication is the fresh host that installs nothing and reports success. emit_test the_publication_member_emits_convergence_and_no_rsync -- the publishing plan's tree fragment carries the convergence invocation and its readback, and no rsync. This is #10724's outstanding acceptance item: that the ACTUATOR reaches the transition, which is a different claim from the transition working. A single end-to-end witness over a full-install plan would have passed for reasons neither half pins down, because a wholesale install emits the same line whatever the observation was. THE UNIT-NAME RESIDUE IS RECORDED, NOT ASSERTED AWAY. The first draft of that witness also asserted `gunbc-tree-sync.service` is absent from the fragment. It is not: the convergence unit is still WRITTEN TO the tree-sync path, the section 3 nickname #10724 declared as residue rather than repaired. Asserting its absence would assert a member-lifecycle migration that has not happened. Verified on srv1: apply closure resolves with zero diagnostics, member_identity 11/11, emit_test 59/59 plus the corrected actuator claim. Claude-Session: https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The stall this closes
gunbc.guarantee_stall.deployed_repository_empty_root_bootstrap_stall—current=OutsideTheLadder,ceiling=StructurallyGuaranteed,blocker=ClimbableButUnbuilt, population of one:deployment_spec_srv1at a fresh repository root.It is the reason a new host cannot be deployed at all. The tree member refuses with
RepositoryBootstrapUnavailablebecause aCreatewould leave a non-repository directory that Git-native convergence then refuses forever — turning a recoverable absence into a permanent one.The pre-state refusal is not relaxed; one of its causes is routed
ConvergencePreStateUnobservablefired for two states with opposite remedies:A non-empty root and an unobservable one still refuse with the identical typed cause. The stall explicitly rules out relaxing that refusal, and this does not: it separates two states the refusal had collapsed.
The distinction is observed, not supplied. The deploy established the tree's absence by listing its parent, but this process reads the root itself rather than taking a caller's word about host state it can observe.
Emptiness, not "is there a
.git". A directory holding files but no repository is the rsync-era partial state — its bytes are already wrong and its repair is notgit init.Both new writes are fail-closed readings of the arm's own claim
checkout -b, which refuses an existing branchIf "nothing was here before" is false, the bootstrap refuses rather than initializing over somebody's repository.
The readback is the only verdict
Every step can exit zero and leave a repository that is not at the candidate — exactly how the rsync-era tree was assembled. The outcome comes from a fresh observation through
repository_convergence_verdict, the same authority the ordinary path uses, not a bootstrap-specific notion of success.beforeandafterare deliberately the same observation: an empty root had no refs or worktrees to preserve, so the preservation comparison is vacuous rather than skipped.Branch name is
gunbc_repository.default_branch, not a literal"main".Evidence is wet
Nothing else can close this stall — its trigger asks for HEAD, index and tracked worktree read back as one revision, and every step can exit zero without that holding. Two rows, both PASS by execution (
claim_batch --wet):an_empty_root_is_bootstrapped_to_the_candidate_and_reads_back_as_that_revision— HEAD at the candidate,git diff --quiet HEADclean, tracked file holding the candidate's bytes; all checked from outside the transition rather than from its exit status.a_root_holding_content_without_a_repository_still_refuses_and_is_not_initialized— the discriminator. Without it the first row would pass equally well had the refusal simply been deleted. The stray file is untouched and no.gitappeared.The fixture authors no answer: it builds a source repository with ordinary git and leaves the target as a directory holding nothing. Every fact the assertions read is produced by the transition's own observation chain.
What this does not do
The tree member still refuses absence with
RepositoryBootstrapUnavailable. Flipping that arm toCreatebelongs in its own commit now that the capability is proven — landing it here would make a fresh host depend on a capability introduced in the same change.🤖 Generated with Claude Code
https://claude.ai/code/session_019iXPtAqzPaNBZbaaM5rxrY