Skip to content

approval loop D: converge materialization, loopback bind, mtcollins1_boot mode - #11484

Merged
briansrls merged 78 commits into
mainfrom
session/tidy-wren-372-approval-loop-d
Sep 20, 2026
Merged

briansrls merged 78 commits into
mainfrom
session/tidy-wren-372-approval-loop-d

Conversation

@briansrls

@briansrls briansrls commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • fleet-converge modes approval_keyring_converge and mtcollins1_boot: WIF-pinned SM materialization of the approval MAC keys onto srv1 (0640 root:briansrls) and of the submission key + bmc-mtcollins1-gunbc/2 in-run; signed POST /approvals using stored_request_json; bounded GET poll; grant_from_store gate; attach + CD handoff only on admit.
  • SOL collector is attached before the BMC stimulus; capture + receipt glob upload with always(). Denied/expired/unfiled refuse with no controller write. Materialized files are wiped on EXIT.
  • Roadmap unit model listen_host is 127.0.0.1. approval_identity_trust stays NotEstablished until dashboard_deploy installs that unit (hermetic POST /approve would otherwise require Clock.Now).

Test plan

  • Hermetic claim_batch over the three new witness modules.
  • generated_artifact_gate main_wet to emit .github/workflows/fleet-converge.yml on a runner with a cgroup memory bind.
  • Operator: approval_keyring_converge on srv1, then dashboard_deploy, then flip trust.
  • Operator tap: mtcollins1_boot — no controller write without the tap.

@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 16, 2026 23:52
@gunbai-bot
gunbai-bot Bot force-pushed the session/tidy-wren-372-approval-loop-d branch from d08f46c to a1236f1 Compare September 17, 2026 00:02
@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Both findings in review 67096 are real and are fixed in 1abe4d431f.

  1. expires_at was issued_at, so approval_poll_standing expired on the first tick. The stored request and the poll now use issued_at plus the poll budget (90×15s) via Clock.TimestampAdd. A non-canonical issued_at refuses before POST. Witnesses: approval_window_is_the_poll_budget_not_zero, a_zero_window_expires_on_the_next_observation.

  2. A failed status GET is StatusUnreadable, not StatusPending. That is PollUnreadable / BootGateStoreUnreadable (controller untouched). Witness: a_failed_status_get_is_unreadable_not_pending; the authorization witness now includes PollUnreadable.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Both findings in review 67111 were real; fixed in 37fd79fe4a.

  1. Deleted the forked mtcollins1_bmc_gunbc_secret_ref on the boot-run module. The WIF fetch in ci_spec now uses gunbc.secret_provision mtcollins1_bmc_gunbc_secret_ref (the mint receipt).

  2. approval_keyring_converge_wet now digests the installed capability MAC key (sha256sum_file_digest_via_shell) and admits it through sha256_prefix_compare / approval_capability_digest_standing. The prelude still materializes and does a first-cut prefix check before install; the modeled comparator is the wall on the files that remain on srv1.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Both findings in review 67114 were real; fixed in 3426f1126e.

  1. roadmap_serve now has POST /approvals and GET /approvals/{escalation_id}. The POST parses the envelope before any keyring/token read (hermetic garbage body is 400 approval submission: … with no link). Outcomes map to 201/409/400/403/502/503 as part C specified. GET is text/plain standing= lines from escalation_status_text. An unread GET after expires_at stays PollUnreadable, not a fake PollExpired.

  2. mtcollins1_boot_after_gate passes the BootGateAdmitted authorization into mtcollins1_attach_diskless_image. Attach refuses unless it is AuthorizationGranted for the diskless image name; a Bool-only gate no longer reaches the controller write.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Addressed review 67124 in 5f92578.

  1. approval_loopback_origin() now composes http:// + srv1_live_dashboard_instance().listen_host + : + listen_port. Status URLs reuse approval_request_submit_path rather than a second "/approvals/" literal.

  2. keyring_host_must_be_srv1 compares against operator_host_srv1. Wet converge reads the host through actions_variable_read(name: fleet_converge_expected_host_env_name) and refuses ActionsVariableAbsent the same way the other fleet-converge modes do.

— sent from tidy-wren-372

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Addressed review 67148 in b323b29.

  1. SOL hold is no longer “capture file nonempty.” Prelude sol activate still redirects stdout+stderr into the capture for the operator, but standing is a live ipmitool pid: /proc/<pid>/cmdline must be readable and contain ipmitool. The prelude refuses MtCollins1SolActivateNotHeld after 2s if that is false; mtcollins1_boot_wet re-checks GUNBC_MTCOLLINS1_SOL_PID_FILE before any controller write.

  2. clock-unreadable is no longer cast to Timestamp. mtcollins1_boot_instant_from_probe admits only a canonical UTC instant; Absent or the loud receipt word mint no window and do not enter approval_poll_standing(observed_at: …). Poll ticks return BootGateStoreUnreadable instead of comparing a fabricated instant to expires_at.

— sent from tidy-wren-372

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67185 — both findings stand and are landed on this head.

  1. Keyring install no longer mints root / briansrls. keyring_mkdir_argv / keyring_chown_argv take fleet_posix_root_user.name and fleet_posix_operator_user.name from gunbc.fleet_posix_accounts (the same accounts the corpus already refused to nickname). Group is the operator login, which is the POSIX principal that must be able to read /etc/gunbc-roadmap/* at 0640; gunbc_service_user is the same spelling in live_deploy and is not a second POSIX identity.

  2. The prelude grep ipmitool /proc/$SOL_PID/cmdline arm is gone. Hold is mtcollins1_boot_sol_is_held at the start of mtcollins1_boot_wet. fleet-converge.yml was regenerated from that prelude so the YAML is not a third copy.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67192 — both findings stand and are landed on this head.

  1. mtcollins1_boot_gate_from_poll no longer calls grant_from_store. PollApproved / PollDenied carry the GET fields; the grant is grant_from_approved_decision (the same revision join grant_from_store uses after it observes the slot). The authorization witness now exercises matching PollApproved → BootGateAdmitted and a foreign revision → BootGateIntentMismatch.

  2. kv_nonempty(..., fallback: \"unspecified\") is gone. Approved/denied/pending status missing required keys or a non-canonical instant is StatusUnreadable. The client witness covers standing=approved with no fields.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67200 — both findings stand and are landed on this head.

  1. The two identical env_path copies are gone. Both the keyring converge and the boot wet path read through gunbc.actions_run_binding actions_variable_trimmed, which is the existing env helper (actions_variable_read with empty collapsed).

  2. The boot step no longer borrows the 30-minute live-deploy ceiling. mtcollins1_boot_step_timeout_minutes is ceil((prelude 120s + poll window + actuation 480s)/60) = 33, and the prelude sleep is mtcollins1_boot_sol_settle. The run witness asserts that timeout covers those three allowances. Dashboard deploy still uses gunbc_ci_live_deploy_step_timeout_minutes.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67200 wait — review 67208.

Clock.TimestampAdd stays on offset: Nat because the shell transport still cannot interpolate Second (same constraint as TimestampOffset). The feature:offset-nat-carrier dissolve-on now names both operations: when Measure interpolation lands, convert TimestampOffset and TimestampAdd to offset: Second and unwrap to Nat only in the transport binder.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67210 — both findings stand and are landed on this head.

  1. approval_submission_client_frontier now names POST/GET plus grant_from_approved_decision over standing fields as the capability that retires it. grant_from_store is explicitly not that trigger (runners cannot open the 0600 store).

  2. mtcollins1_boot_wet reads FLEET_CONVERGE_EXPECTED_HOST and refuses off srv1 with a typed reason before any POST, same pin as the keyring mode. Witness: boot_loopback_filing_is_srv1_only.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67219: both findings were in the tree and are now on 9513f2a495.

The capability digest ran after keyring_install_file for both keys, so a payload that failed approval_mac_key_payload_sha256_prefix was already at /etc/gunbc-roadmap/approval-mac-key when the run exited non-zero. The prelude sha256sum + ApprovalMacKeySha256Mismatch line was a second representation of that same pin and was the only copy that ran before a write. Digests now run on the staged files after Filesystem.Read and before SSH/install (approval_keyring_staged_digest_ok). The submission key now has the same gate, with approval_submission_mac_key_payload_sha256_prefix (01f19b2e2c7a) on gunbc.auth.approval_keyring_pins — the prefix that approval_request_submission.dag previously only named in prose. The prelude sha256 Do is gone (YAML regenerated).

max_seconds: "30" is now to_string(second_count(s: mtcollins1_boot_submit_timeout)) with mtcollins1_boot_submit_timeout: Second = second(count: 30), same shape as harness_backend / harness_turn.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67229: the Unclaimed HTTP grant plus a stable GITHUB_RUN_ID was a real re-actuation hole; it is closed on e3b9fc6c7b.

grant_from_approved_decision still mints Unclaimed from the GET fields (same shape as srv3's source-declared grant). Identity mismatch arms of authorize stay unreachable when that grant is built from the same request — those checks are not the exclusion wall. The wall is the durable claim slot, as srv3_boot_once_cd already models: ExpectSlotAbsent CAS on authorization_claim_store_root (/var/lib/gunbc/authorization-claims) before attach/handoff, then complete or abort. A re-run of the same GitHub run id hits ClaimLost and does not write the controller. The claim key is the escalation id (a scoped-authorization/ prefix is not slot-addressable in durable_cas_file_store).

The dashboard artifact truncated after the first finding (- d...). If there was a second item, it did not arrive in this ping.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67233: the two standing folds were dangling; fixed on a3ab26781c.

approval_keyring_staged_digest_ok no longer re-matches sha256_prefix_compare. After a successful file digest it calls approval_capability_digest_standing or approval_submission_digest_standing (or refuses if the expected prefix is not one of those pins). The witness over those folds now sits on the same verdict the wet converge uses.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67237: the expires_at kv injection was real; closed on 7faf3fad80.

parse_stored_request now refuses unless issued_at and expires_at are canonical UTC instants (utc_instant_is_canonical), so a payload with embedded newlines never becomes a filed pending record that escalation_status_text would interpolate. Decision decided_at / obligation_execute_by get the same gate; a denial reason with CR/LF is unreadable. The GET reader no longer last-match-wins: kv_line_read treats a repeated standing= (or other status key) as StatusUnreadable, so an injected standing=approved line cannot win even if a bad record were already on disk.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67240: approval_issuance_frontier was stale; deleted on 4c2b33dce8.

Its own trigger was mac_sign plus filing/publishing with capability_path_segment links. That consumer is issue_links / file_and_notify on this PR. Leaving an unbound row after the trigger fired was the same honesty defect as keeping the keyring materialization frontier. The store-verify fixture tags remain known answers; they are no longer described as waiting on part A.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67243: both findings were in the tree; closed on 7bed0f428c.

  1. approval_capability_digest_standing / approval_submission_digest_standing and the prefix re-dispatch in approval_keyring_staged_digest_ok are gone. One approval_mac_key_digest_standing(observed_hex, expected_prefix, role) builds the diagnostic from role; the staged-file helper only hashes then calls that fold. The unreachable “not a modeled payload prefix” arm is gone.

  2. The decision-store comment no longer transcribes 113a3274bd95. It names gunbc.auth.approval_keyring_pins approval_mac_key_payload_sha256_prefix.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67247: both findings were in the tree; closed on 3fc37c1e5e.

POST no longer greps "already filed". submission_outcome_admits_poll is the typed fold (already-filed, filed-but-notify-failed, links-not-issued, and capability-key-unavailable after the store write all admit). The HTTP body is filed=yes / filed=no from submission_filed_text; approval_post_admits_poll reads that token only, so a 502 after a durable file still polls.

GET standing emit/parse is one grammar in gunbc.auth.approval_status_wire (filed_approval_status_text / parse_filed_approval_status). escalation_status_text maps the slot to FiledApprovalStatus and emits; it does not join standing= literals itself.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67250: both findings were live in the tree.

  1. approval_post_admits_poll was success == success && submission_body_admits_poll. Deleted the wrapper. Boot and the witness now call submission_body_admits_poll only; the rationale is a // on that function (409 already-filed / 502 after write still poll).

  2. Production admit_submission no longer passes authority_permitted: true. StoredApprovalRequest carries authority; admit_parsed_submission passes request.authority and request.authority == approval_capability_authority. Witness a_signed_request_for_a_foreign_authority_is_refused_before_the_operator_sees_it is the RED.

— sent from tidy-wren-372

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67254: the first finding was in the tree. submission_outcome_admits_poll treated SubmissionFiledLinksNotIssued and SubmissionCapabilityKeyUnavailable as filed=yes, so mtcollins1_boot would poll the full window on a request no operator can approve (boot before approval_keyring_converge).

Those arms now return filed=no. file_and_notify mints the capability and builds the message before occupying the slot, so a missing /etc/gunbc-roadmap/approval-mac-key does not leave a pending un-approvable record (a retry would otherwise 409/AlreadyFiled and poll anyway). The variant is SubmissionLinksNotIssued. Witness: a_request_with_no_minted_capability_does_not_admit_poll. Notify-failed 502 after a durable write with minted links still admits poll.

The dashboard artifact truncated after that finding (Four things I want changed; only the first arrived). If 2–4 are still open, they need the rest of stdout.log.

— sent from tidy-wren-372

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67257: the gap was real. mtcollins1_boot_sol_is_held ran only before filing. After mtcollins1_boot_poll_ticks the same check now runs immediately before mtcollins1_boot_after_gate. Early refuse is unchanged. A collector that exits during the window fails with controller untouched rather than attaching on a dead pid and a prelude capture file.

— sent from tidy-wren-372

gunbai-bot Bot pushed a commit that referenced this pull request Sep 17, 2026
…r asserted beside it (review 67264)

approval_identity_trust = approval_identity_trust_for(srv1_live_dashboard_instance.listen_host):
loopback establishes (the header is tailscale serve's), any other bind or an
unparseable host keeps the gate closed, with the blocker naming the row. On
main and this branch the row is 0.0.0.0, so the standing is NotEstablished;
D's loopback row (#11484) opens it in the same commit that emits the unit.
Witness pins the three arms and the wiring to the production row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67271: both preludes were unmarked hand-shell. They now carry Scaffold + unbound_dissolution the same way gunbc_ci_fleet_key_agent_script does, bind the invoke functions that become the GHA run: bodies, and emit the dissolve-on comment as the first line of those steps in fleet-converge.yml. Trigger is bash-emit / shell→intent (#5828).

— sent from tidy-wren-372

gunbai-bot Bot pushed a commit that referenced this pull request Sep 17, 2026
…overs both key paths; the Mt. Collins BMC secret accessor grant is modeled (review 67280; boot run 35245186544)

- approval_keyring_materialization_frontier names the converge step that
  writes both MAC key files (lands as gunbc.auth.approval_keyring_converge in
  #11484, first live receipt run 35181154197); the path comment no longer
  cites a symbol this tree does not hold.
- gunbc.machine_intake_mtcollins1_secret_access_ensure: the accessor grant
  for bmc-mtcollins1-gunbc on the convergence principal, converged through
  gunbc.auth.gcp_secret_access under the operator's token (same shape as
  gunbc.spark.secret_access_ensure). The first mtcollins1_boot dispatch got
  403 on that secret because no grant was modeled. Witness pins the target
  and the accessor-only cell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67301: the SOL hold witness now supplies the pid-file and cmdline observations (mtcollins1_boot_sol_held_from_observations). Empty path still refuses without a hermetic Filesystem.Read; unreadable pid, empty pid, a live cmdline that is not ipmitool, and a successful ipmitool cmdline are each asserted. Production mtcollins1_boot_sol_is_held still Reads then folds through that same verdict.

@gunbai-bot

gunbai-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

review 67371: dropped the listen_host contrast from the macbook instance annotation. srv1-live is 127.0.0.1; that field is no longer one of the differences from srv1-live. Left the credential-prelude concat as-is — the dissolve-on wall you already accepted.

@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

review 67415:

Keyring files now seal as RemoteFileModeOwnerReadWriteGroupRead (640). The octal string and post-seal chmod are gone; chown is unchanged.

Claim key: did not restore scoped-authorization/ — / still fails cas_key_is_slot_addressable. The shared authorization_claim_store_root is one authorization-claim domain with two callers (srv3-boot-once-cd vs mtcollins1-boot-<run>); those escalation ids already cannot collide. Rewrote the justification so it no longer says the root per-workflow-namespaces the key.

@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

review 67425: mtcollins1_boot_after_gate now takes the request, reads the claim clock, and authorizes on that instant before CAS/attach. Unreadable clock refuses as inability to judge expiry. An execute_by that lapses after poll is AuthorizationExpired on the actuation clock (witness an_execute_by_that_lapses_after_poll_is_expired_on_the_actuation_clock).

…v1 keyring.

fleet-converge gains approval_keyring_converge and mtcollins1_boot: SM-pinned MAC keys land 0640 on srv1, the runner files a signed AuthorizationRequest, polls a bounded GET, and only then attaches virtual media. SOL is held before the stimulus. Trust stays NotEstablished until dashboard_deploy installs the loopback unit.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Review 68456 addressed at 3e535a3: (1) the claim/settle/unterminated-exit triple is now ONE authority, gunbc.authorization_claim_slot (claim_authorization_slot / settle_authorization_claim / unterminated_claim_exit, parameterised by escalation id, attempt, label), consumed by both gunbc.srv3.srv3_boot_once_cd and mtcollins1_boot_run; the two verbatim copies and srv3's private exit renderer are deleted. (2) password@file newline hazard: the contract is stated on OpenSession — the file is the password bytes exactly; the prelude writes the SM payload verbatim (base64-decoded, no trailer); boot 22's successful login is the executed positive control for the stored bytes. If the reviewer wants a structural wall rather than a stated contract, the honest place is the rotation fold that mints the payload (refuse a trailing newline at mint), not a trim at the consumer — say so and I'll add it. — sent from eager-owl-205

…union (drop the retired heal_repo_local_git_config target); regenerate the generated artifacts

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal and others added 3 commits September 19, 2026 18:31
…'s row, satisfied at this branch's merge base); the gate names it as due for deletion on the roster's next touch

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…onverge_workflow rosters are the union of both sides; regenerate fleet-converge.yml

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oped-authorization-' + escalation id) — the earlier repair for the slash-addressability refusal deleted the prefix and moved scoping into caller convention (review 68728); the witness now reds on the bare id and on a slashed key

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Review 68728 addressed at ce0b794: authorization_claim_key carries its scope again — 'scoped-authorization-' + escalation id (the '-' separator is slot-addressable; the earlier repair deleted the prefix instead of re-spelling the separator). The witness a_boot_claim_slot_key_is_scoped_and_slot_addressable reds on the bare id and on a slashed key. Note the srv1 claim store already holds bare-keyed slots from boots 14–22; those are per-run, terminal, and dead, so no migration. — sent from eager-owl-205

@briansrls
briansrls added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 121a4d4 Sep 20, 2026
2 checks passed
@briansrls
briansrls deleted the session/tidy-wren-372-approval-loop-d branch September 20, 2026 01:21
gunbai-bot Bot pushed a commit that referenced this pull request Sep 20, 2026
… missing

review 68826: the scratch entry came back when the second census recreated it
and `git add -A dag` swept it in again. Deleted, and the lane note now says to
stage explicit paths.

The heal job's refusal is NOT this branch's: `fleet_converge_mode_fleet_ssh_key_demand`
is non-exhaustive on origin/main itself. #11736 added the match and #11484 then
added the ApprovalKeyringConverge and MtCollins1Boot variants without arms for
it; neither touched the other's lines, so nothing conflicted and the gap only
appears once both are on main. Both modes open host sessions, and the function's
own note says only the two API-only modes are established as not consuming the
fleet key while every other mode keeps what it held, so both arms are
FleetSshKeyConsumed.

Local verification of that resolve did not complete: the closure takes longer
than a 900s timeout under current fleet load, twice. The heal job that reported
the refusal is the instrument for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Sep 20, 2026
gunbc_run_step_targets listed gunbc_ci_approval_keyring_converge_target
and gunbc_ci_mtcollins1_boot_target twice. Both are main's, from #11484;
my first conflict resolution in this branch unioned the two sides of
that list hunk and kept a copy of each. 41 entries, 39 distinct.

This is the class I predicted and then failed to check here. Every time
main moved I verified fleet_converge_workflow_modes for duplicate arms
and a symmetric difference against its coproduct -- and never applied the
same check to this roster, in a different file, holding the same shape.
Checking one instance of a class is not checking the class.

So the check is now the general one: 39 entries, 39 distinct, every entry
resolves to a declaration. It also reports the converse -- two declared
targets are absent from the roster (org_runner_roster_observe,
spark_runtime_image_probe), both main's and both predating this branch.

KEEPING my gunbc_ci_mtcollins1_fan_observe_target entry, and the reason
is not symmetry for its own sake: gunbc_run_step_targets has no consumer
anywhere in the corpus, so the roster establishes nothing today and
whichever way I go is inert. Omitting mine would make my target the only
one missing on the day it acquires a consumer. The unconsumed fold is a
pre-existing §3c finding against its owner, not something this lane
should fix by writing one fewer row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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