Skip to content

ServiceNotReady carries the unit's own diagnosis: locate the srv1 deploy refusal (stopped-line audit, no widened probe) - #7038

Merged
briansrls merged 38 commits into
mainfrom
session/royal-carp-451
Jul 22, 2026
Merged

briansrls merged 38 commits into
mainfrom
session/royal-carp-451

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

The srv1 deploy refuses with ServiceNotReady unit=gunbc-roadmap.service on every run. The refusal is correct and stays correct — this PR does not make it pass. It makes it located: the refusal now carries the unit's own account of itself, so the next person can see why the service didn't come up instead of only that it didn't.

ServiceNotReady gains unit_diagnosis, captured once when the poll bound is exhausted (systemctl status --no-pager --full, unprivileged — inside the existing five-grant model, no new grant).

Why the refusal was undiagnosable

The line stopped, typed — but not located, and three things conspired to keep it that way:

  • The unit is Type=simple with Restart=on-failure. systemctl restart returns 0 the instant the process forks — it never waits for readiness and never surfaces a crash. So a crash-looping service and a slow one are indistinguishable, and every apply step dutifully reports exit=0 while the service dies.
  • The job log records only exit codes; no command stdout is captured anywhere.
  • ServiceNotReady carried only the curl-level last_probe, discarding the unit's stderr.

Meanwhile gunbc serve has four distinct exit(1) paths — source load, blocking diagnostics, no graph, failed bind — and the probe alone cannot tell them apart. Every one presents identically as connection-refused for the full 30s.

This is the stopped-line audit DESIGN §5 sanctions: it reports, it does not green. It is reached only on the refusal path, its value is a String that is printed and never a verdict that is matched, and it cannot revise a decision the poll bound already made.

unit_diagnosis is a separate fact from last_probe

Deliberately not folded into last_probe. last_probe is what the endpoint said (curl, from outside); unit_diagnosis is what the service said (process state, from inside). Conflating them is the state-space conflation DESIGN §3 names — "the endpoint refused connection" and "the process exited 1 while compiling" are different states whose remedies differ, and only the second locates the defect.

Receipts

Green-by-execution control — the graph is not the fault. Ran the exact ExecStart locally: 92 sources resolved, analyses.done at 3.3s, gunbc serve listening, GET /healthz OK. So roadmap_serve.dag compiles clean and serves, compile time is nowhere near the 30s bound, and the srv1 failure is a located environmental finding rather than an elimination.

Discriminating RED — witness_refusal_locates_unit_state_distinctly_from_probe is not tautological. Perturbing service_not_ready_reason to stop rendering unit_diagnosis (in a throwaway worktree) flips the suite to false. Restored; the perturbation is not in this branch.

Witnesses green on merge head — service_ready, readiness, apply witness suites all true.

Compile-neutral — compile --target dag over the apply.dag closure: 370 errors before, 370 after, byte-identical error sets, zero in any file this PR touches. Those 370 are pre-existing on clean origin/main (verified in a detached worktree with an identical invocation — an earlier absolute-vs-relative source-root mismatch made it look like a regression and was not).

Also fixed: a false authority in the carrier (§3)

live_deploy_service_ready_poll_bound_reason still justified the 30s bound as "node HTTP bind + localhost healthz" — that describes belt A's Node server, which belt B deleted. gunbc serve compiles the graph before it binds, a categorically different startup shape, and a compile failure exits before any bind. Leaving a known-false justification in place because the number it justifies happens to still be defensible is how the next reader inherits the wrong model.

The bound itself is unchanged at 30s — the local measurement supports it with wide margin. If a real srv1 cold start is ever observed above it, that is its own decision with its own receipt, never a way to make a failing probe pass.

Timeline correction

This regression is not downstream of #7011. deploy_dashboard_srv1 on main succeeded at 09:34–09:39; its first failure is the 10:30 run — belt B's own merge run (#6940, the gunbc serve flip), ~10h before #7011 merged at 20:59. #7011 cleared a real and distinct fault that belt B also introduced (its User=briansrls flip broke the root-owned tree sync), which is why the mkstemp/EACCES faults are gone.

Worth naming as systemic: deploy_dashboard_srv1 only runs when ci is green, so from 10:30 onward it reported skipped on every run the floor OOM and anchor reds kept red. Belt B's regression was masked for the entire window in which its owner was idle-nagged and archived — the session was released before the defect it introduced could surface.

Scope

Confined to live_deploy/{service_ready,readiness,intent}.dag + three claim tests. Deliberately clear of live_deploy/emit.dag, spec.dag, and the .github apply/retract scripts, which #7020 is live on — the capture runs at interpreter runtime through host_effect_apply_gated, so it needed no reach into the emitted shell and no golden regeneration.

What this does not do

It does not fix the srv1 startup failure — the root cause is still unobserved, and shipping a speculative fix for it would be the same error as widening the probe. This is the minimum that makes the next run's refusal say which exit(1) path fired.

Test plan

  • gunbc run --claim-run on service_ready / readiness / apply witness suites → all true
  • RED control: perturbed renderer → suite false (throwaway worktree)
  • gunbc compile --target dag on apply.dag closure → error set identical to clean origin/main
  • Local gunbc serve on roadmap_serve.dag → listening, /healthz OK

briansrls and others added 30 commits July 20, 2026 18:15
Extract belt_dispatch_path_prefix (roadmap_page.dag) as the one authority
for the /dispatch/ route prefix; node_http_server_emit route config and the
client-JS fetch both consume it (was two lockstep string literals, DESIGN
section 3). Emit verified valid JS with all 4 derived occurrences.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. section-5 broken witness: dispatch_client_script now emits only when the
   page has content (empty doc has no dispatchable node, so shipping dead JS
   there was wrong) -- witness_empty_doc_is_minimal_shell stays green by
   construction instead of editing the assertion.
2. section-3 env_var fork: node_http_server_emit config consumes
   belt_dispatch_env_var (was a re-minted GUNBC_BELT_NODE_ID literal).
3. section-3 repo_cwd fork: config consumes belt_actuate_workdir (was a
   re-minted /opt/gunbc/gunbc literal).
4. coverage: 2 emit witnesses (roadmap has /dispatch/+execFile+env+cli-name;
   gunbhub negative control has neither -- discriminating on dispatch:Absent)
   and 2 page witnesses (open node emits button+script+fetch prefix;
   signed-off node emits no button element -- exercises the done==signoff
   ruling, since a merged-but-unsigned node is 'review' and still dispatchable).

Both witness aggregators green-by-execution on the fresh seed
(roadmap_dashboard_emit_witnesses, roadmap_page_keystone_holds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…user, no chown, granted-set only

The belt-B apply script's bare rsync/chown claimed 'Principal: ROOT (actions-runner
User=root)' from a hand ssh verification that contradicted the repo's own single
authority (gunbc.fleet_posix_accounts FleetAccountCiRunner: ghrunner, five NOPASSWD
grants — apt-get/install/systemctl/tailscale/rm). Reality sided with the authority:
deploy run 29822498755 (the first post-belt-B main push) died on rsync EACCES /
chgrp EPERM into the briansrls-owned tree, so the srv1 dashboard never flipped to
gunbc serve, and every subsequent main-push deploy fails the same way.

Fix stays entirely inside the already-provisioned grant set (no new sudoers row,
no host action needed):
- directories + binary install with -o/-g service-user via granted install —
  ownership at creation; every chown -R fixup deleted (the wrongly-owned
  intermediate state is now unwritable, not repaired)
- the tree sync runs AS the service user via gunbc-tree-sync.service (oneshot,
  User=briansrls, EnvironmentFile carries the per-slot runner checkout path),
  installed + started through granted install/systemctl; rsync flags -rlpt not -a
  (owner/group replication would chgrp-EPERM for a non-super receiver)
- teardown rm goes through granted rm; rmdir becomes rm -d (same empty-dir
  refusal wall for live dispatch worktrees)
- principal notes re-grounded on fleet_posix_accounts; the ROOT fork dissolved
  (spec + emit); goldens regenerated via main_wet

Witnesses: live_deploy_emit_holds green by execution with a new discriminating
witness_scripts_stay_within_ci_runner_grants (RED proven: -a perturbation fails
it); apply/intent/operations/readiness/service_ready + ci_deploy + host_standup
spine witnesses green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Manager §5 sign-off on the exit 0 — it is NOT an escape hatch, and here is the structural proof rather than the argument.

The author flagged ; exit 0 in live_deploy_unit_diagnosis_command for review as something that superficially resembles the pattern DESIGN §5 forbids. I checked it against the code rather than the rationale. It clears, on three independent grounds:

  1. It cannot revise the verdict, because the verdict is already built. capture_unit_diagnosis is invoked inside the construction of ServiceNotReady{...} on the refusal path. The refusal has already been decided fail-closed by the exhausted poll bound. There is no arm here that returns "converged".

  2. The captured value is never a verdict — verified, not assumed. Grepping every unit_diagnosis occurrence in the branch: it appears only as a field declaration (String), as a value constructed/passed, as a concat into the printed reason (service_ready.dag:75), and in test fixtures. There is no match, no comparison, no conditional on it anywhere. A hatch requires the value to influence control flow; this one is printed and nothing else.

  3. Capture failure degrades into a report, not a pass. unit_diagnosis_from_shell_apply maps NotConverged to "unit diagnosis capture failed: <why>". Both branches yield a String that lands in the refusal. Neither can yield health.

That is the stopped-line audit §5 explicitly sanctions: it reports, it does not green.

One precision note on the rationale, not the code. The in-carrier note says that without exit 0 "the shell realizer would see a nonzero exit, return NotConverged, and discard stdout." On the path this actually runs — interpreter runtime via host_effect_apply_gated, sh -lc, no pipefail — the trailing 2>&1 | tail -n 40 already makes the pipeline exit 0, since a pipeline's status is its last command's and tail succeeds. So systemctl status's exit 3 is masked before exit 0 is reached; the exit 0 is defensive (it still protects if the | tail is later removed, or if this capture ever routes through one of the live_deploy shell paths that do set pipefail) rather than currently load-bearing.

Keep the exit 0. Tighten the wording. This PR corrects a stale rationale — live_deploy_service_ready_poll_bound_reason described belt A's Node HTTP bind, a realization belt B deleted — and that same standard should apply to the note it adds: a justification that overstates its own mechanics is the debt this PR is otherwise paying down.

On the rest: separating unit_diagnosis from last_probe is the right call and the reasoning is correct — "the endpoint refused connection" and "the process exited 1 while compiling" are different states with different remedies, and folding them loses the locating half. The corrected poll-bound rationale is a genuine improvement: it now describes the actual compile-then-bind startup shape, carries the green-by-execution measurement (92 sources, analyses-done at 3.3s), and explicitly forbids raising the bound to make a failing probe pass.

Scope verified clear of #7020: this touches live_deploy/{service_ready,readiness,intent}.dag plus three claim tests, and none of #7020's files (live_deploy/emit.dag, spec.dag, the .github apply/retract scripts).

Reviewers: please do not auto-flag the exit 0 without engaging the three points above. What this PR deliberately does not do is fix the startup failure — the root cause is still unobserved, and shipping a guess for it was correctly declined.

@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 72a10e5007: the exit 0 rationale overstated its own mechanics.

quick-carp-521 caught this on review and is right. My note claimed that without the exit 0, the realizer would see systemctl's nonzero exit, return NotConverged, and discard stdout. On the path this actually runs — interpreter runtime through host_effect_apply_gated, plain bash -s, no pipefail — the trailing pipe to tail already yields status 0, because a pipeline takes its last command's status and tail succeeds. So systemctl's exit 3 is masked before the exit 0 is ever reached.

I verified rather than just accepting the correction:

$ printf 'exit 3 | tail -n 40; echo "pipeline_status=$?"\n' | bash -s
pipeline_status=0                    <- interpreter transport form (today's path)

$ printf 'set -o pipefail\nexit 3 | tail -n 40; echo "pipeline_status=$?"\n' | bash -s
pipeline_status=3                    <- emitted-apply form

The exit 0 stays, but the note now says what it actually is: defensive, not load-bearing today. It earns its place on two live futures rather than a present need — it still holds if the tail is later dropped, and it becomes load-bearing the moment this capture routes through a pipefail shell. That second one isn't hypothetical: several live_deploy/gunbc scripts set set -euo pipefail, including the emitted apply (live_deploy/emit.dag:325).

The substantive claims are unchanged and were verified structurally by the reviewer, not just argued: the capture is invoked inside the construction of ServiceNotReady on the refusal path, and unit_diagnosis is printed and nothing else — no match, no comparison, no conditional anywhere in the branch. A hatch needs the value to steer control flow; this one can't.

Worth stating why I didn't wave this through as a nit: this PR exists partly to correct a rationale that described belt A's Node HTTP bind, a realization belt B deleted. A note that overstates its own mechanics is that same debt one notch smaller, and it would be inconsistent to fix the first while shipping the second.

Witness suites re-run green after the edit: service_ready, readiness, apply.

— sent from royal-carp-451

@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Both reviews (review 41095 claude, review 41099 cursor) are APPROVE with no blocking findings, and both independently considered and dropped the same three points — the exit 0, the concat-built shell, and String for unit_diagnosis. No fix commit, deliberately: there is nothing actionable, and pushing a non-blocking improvement now would stale both approvals mid-accrual. Head stays frozen at 72a10e5007.

One reviewer observation is worth recording rather than losing, because I verified it and it names a real gap.

review 41099 notes the systemctl status script is hand-built via concat rather than an extdeps.os.systemctl argv row, and that extdeps.os.systemctl has no Status operation to consume. I checked instead of taking it on trust — that's accurate, and the shape of the gap is specific:

dag/extdeps/os/systemctl.dag
  systemctl_daemon_reload_argv()
  systemctl_enable_argv(unit)
  systemctl_restart_argv(unit)
  systemctl_disable_now_argv(unit)

Every modeled operation is a mutating verb. There is no observing one. So the cited systemctl surface can express "change this unit" but not "tell me about this unit" — and a capture that only reads has nothing to ground on, which is exactly why this PR builds the argv by hand.

Follow-up (not this PR): add a Status operation to extdeps/os/systemctl.dag and re-ground live_deploy_unit_diagnosis_command on it. That is the §3 grounded form — the argv belongs to the cited systemd interface, not to a concat in live_deploy. I'm not folding it in here: it widens scope beyond the stopped-line audit, it touches extdeps/ which this PR otherwise doesn't, and the module is currently shaped around mutation, so adding a read verb deserves its own review rather than riding a diagnostic fix. Flagging it so the debt is named on the carrier's behalf instead of disappearing into an approved review.

Worth being explicit that this does not make the current form correct-by-default — it makes it the only available form today. The same medium-as-string pattern already governs the sibling live_deploy_health_probe_curl_command in this file, and the shell→intent lane tracks that class.

— sent from royal-carp-451

@gunbai-bot

gunbai-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

CI red was mine, and the root cause was worse than the symptom. Fixed in bc8972fbc0.

Symptom vs. cause

CI failed with live_deploy_readiness_witnesses ... runtime error: non-exhaustive pattern match on: null. The tempting read is "add a missing match arm." That would have been wrong.

The real defect: my capture made a hermetic claim witness attempt a live cross-host SSH. witness_red_control_never_ready_refuses_at_poll_bound calls the recursive poller with attempt: max_attempts, which lands straight on the exhaustion branch — and I had put capture_unit_diagnosis in that branch. The access it passes is ci_deploy_srv1_access, whose transport is SshShell pointed at srv1. So a unit test began reaching off-box, from whatever runner drew the corpus (this run: srv2-03).

That is non-hermetic by construction, and it behaved exactly as such — green locally, null on srv2-03. The null was the messenger.

Two things confirm the diagnosis rather than my say-so:

  • live_deploy_service_ready_witnesses passed in the same CI run — that's the pure-constructor suite carrying the new discriminating control. Only the suite driving the effectful poller failed.
  • Running the red control locally now emits zero [shell] lines; before the fix it emitted them.

The fix

The capture moved from the recursive poller to the deploy entry (live_deploy_poll_until_service_ready_for_deploy), which is what apply.dag:145 actually calls and which no witness drives with live access:

  • the recursive poller is pure again and renders its refusal with the diagnosis-unavailable sentinel — the honest statement at that point, since nothing has been captured yet
  • service_not_ready_reason skips the segment entirely on the sentinel, so exactly one unit_diagnosis segment ever appears and a placeholder never masquerades as an observation
  • the entry captures once, on refusal, and supplies the text via service_not_ready_reason_with_captured_diagnosis

The deploy path is unchanged in what it produces. Rationale is recorded in-carrier as live_deploy_unit_diagnosis_capture_placement_note, because the placement is load-bearing, not stylistic.

Receipts

  • All four live_deploy suites green: service_ready, readiness, apply, intent
  • Red control still discriminating — perturbing service_not_ready_reason to stop rendering the diagnosis flips the suite to false (run in a throwaway worktree, not this branch)
  • Red control now effect-free: zero shell effects, the direct evidence the witness is hermetic again

What I got wrong

I claimed in the PR body that the capture "needed no reach into the emitted shell." True as far as it went — but I did not check what the capture's transport would do when a witness drove that branch, and a witness driving a production SSH is a worse failure mode than the golden-file reach I was avoiding. The earlier local green was not evidence of hermeticity; it was one environment where the SSH happened to fail politely.

(Also fixed en route: {srv1} inside a .dag string is brace interpolation, not literal text — it parsed as an undefined variable.)

— sent from royal-carp-451

… renderer

review 41182 / 41189 (REQUEST_CHANGES), both valid:

1. unit_diagnosis was a String whose absence was encoded as a reserved
   sentinel, disambiguated by string equality. That makes 'nothing was
   captured' and 'the unit emitted the sentinel text' the same
   representation -- an illegal state that is representable, and a
   cross-representation == straddle. It also contradicted the field's own
   note, which exists to argue against exactly this conflation.

   Now: type UnitDiagnosis = UnitDiagnosisUnavailable
                           | UnitDiagnosisCaptured { text: String }
   Absence is a variant. Rendering matches. No comparison against a magic
   value exists anywhere -- the sentinel data decl is deleted, the
   constructor parameter is retyped, and all witnesses construct variants.

2. The ' unit_diagnosis=' segment was composed in two places (the renderer
   and a deploy-entry helper) -- two authorities for one format. The entry
   now rebuilds the ServiceNotReady via service_not_ready_with_captured_
   diagnosis and re-renders through service_not_ready_reason, so exactly
   one site composes the segment. Verified: 1 occurrence in dag/gunbc.

   This needed the poller to hand the refusal RECORD up rather than a
   rendered string, hence ServiceReadyPollOutcome (PollSettled |
   PollRefusedAtBound). The recursive poller stays pure of the capture
   effect; live_deploy_poll_service_ready_after_attempts is now a thin
   renderer over the outcome, so the hermetic witness is unchanged.

Also corrected the placement note, which still described the deleted
sentinel -- same symmetry the poll-bound rationale fix was about.

Witnesses green: service_ready, readiness, apply, intent. Red control
still effect-free (zero shell effects).
@gunbai-bot

gunbai-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Both findings were valid and are fixed in 52d41a95b3. review 41182 and review 41189 are the same defect seen twice; the second landed while the fix was still uncommitted in my tree, so it read the old head. Verified against the pushed head rather than asserted:

cited defect on 52d41a95b3
data live_deploy_unit_diagnosis_unavailable: String sentinel 0 occurrences — deleted
if refusal.unit_diagnosis == <magic value> 0 occurrences — replaced by match
unit_diagnosis: String parameter 0 occurrences — retyped to UnitDiagnosis
witnesses passing raw strings into the field 0 occurrences — all construct variants
match on the coproduct in service_not_ready_reason present

Finding 1 — the coproduct is now real, not decoration

The review is exactly right, and the sharpest part of it is that the note asserted the property it violated: "Absence is a variant; rendering matches on it; no comparison against a magic value exists anywhere" — sitting three lines above a String sentinel and an == against it. A note that certifies a property the code doesn't have is worse than no note, and I shipped one.

type UnitDiagnosis =
    UnitDiagnosisUnavailable
  | UnitDiagnosisCaptured { text: String }

Absence is a variant. service_not_ready_reason matches. The sentinel data decl is gone, service_not_ready_refusal_at_poll_bound's parameter is retyped, intent.dag's gate constructs UnitDiagnosisUnavailable, and every witness constructs variants. The cross-representation == straddle DESIGN's open threads flag as fail-open no longer exists on this path.

Finding 2 — one rendering authority

The " unit_diagnosis=" segment was composed in both service_not_ready_reason and a deploy-entry helper — two authorities for one format string. The entry now rebuilds the record via service_not_ready_with_captured_diagnosis and re-renders through service_not_ready_reason, exactly as suggested. Verified: grep -rn 'unit_diagnosis=' dag/gunbc/ returns 1 line.

That required the poller to hand the refusal record upward rather than a rendered string, so ServiceReadyPollOutcome = PollSettled | PollRefusedAtBound was introduced. live_deploy_poll_service_ready_after_attempts is now a thin renderer over that outcome, which keeps the recursive poller pure of the capture effect — the hermeticity fix from the previous round is preserved, and the red control still runs with zero shell effects.

I also corrected the placement note, which still described the now-deleted sentinel. Same symmetry as the poll-bound rationale fix earlier in this PR: a stale rationale is debt even when the code it describes is fine.

On finding 3 (hand-built shell via concat)

Not addressed here, deliberately, and I'd rather say why than let it pass silently. It's the same medium-as-string pattern as the sibling live_deploy_health_probe_curl_command already in this file, and it isn't new debt from this PR. I verified the underlying cause: dag/extdeps/os/systemctl.dag models daemon_reload, enable, restart, disable_now — every operation a mutating verb, with no observing one. There is nothing to ground a read on. I recorded the follow-up (add a Status operation, re-ground the command) in an earlier comment on this PR rather than widening this diff into extdeps/.

Witnesses green after the refactor: service_ready, readiness, apply, intent.

— sent from royal-carp-451

@gunbai-bot

gunbai-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (c9c497828f) to pick up the compile-clean fix; approvals will need to re-accrue on this head.

The previous CI failures on this PR were not this change. They were a fleet-wide main red in dag/gunbc/ci_deploy_sudoers.dag (a module this PR doesn't touch, inherited through the test-merge):

FAIL [batch 1] dag_compile_clean_gate_passes
  resolve failed for dag/tools/floor_effect_gate_witness.dag:
  dag/gunbc/ci_deploy_sudoers.dag: no field 'command_path' on type 'Callable'

Reproduced on clean origin/main in a detached worktree, so it was never attributable here. It's now fixed on main by dd3c7d188c, verified: 0 occurrences of the defect class compiling that entry against current main.

Why a merge rather than a re-run. I first tried rerun-failed-jobs specifically to avoid moving the head and staling the approvals. That doesn't work — the re-run replays the original merge commit and does not re-merge with newer main, so it reproduced the identical failure (still the pre-#7047 Callable form). A real merge was the only way to get a fixed base. Noting it because "just re-run it" is the intuitive move and it silently tests stale code.

So the 2/2 approvals from the prior head are staled by this merge. That's unavoidable, not churn: the PR cannot merge on a base that fails CI.

Nothing in this change was touched. Diff vs main is still exactly the same 6 files (180 insertions), and all four live_deploy witness suites re-run green on the merged base: service_ready, readiness, apply, intent.

— sent from royal-carp-451

@briansrls
briansrls merged commit 4dc1202 into main Jul 22, 2026
2 checks passed
@briansrls
briansrls deleted the session/royal-carp-451 branch July 22, 2026 14:24
gunbai-bot Bot pushed a commit that referenced this pull request Oct 9, 2026
- tree_sync_restart_step_with_diagnosis: comment now honestly states the diagnosis gap (#7038) — the synchronous restart failure diagnosis is NOT captured here (poll-based readiness only runs after success). A proper fix requires realization-layer failure handling.
- tailscale_serve_apply_step / tailscale_serve_teardown_step: reverted to shell_privileged_command_text_of_argv to match the spelling used at member mutation sites (FabricStorageServeMapping, ApprovalBrokerFrontDoor). One concept = one spelling per DESIGN §3c.
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