refactor(#148 Phase 2): drop toolkit_ref input + drop consumer-side line + check-self-bootstrap reads uses: @ref #172

Closed
opened 2026-06-27 11:16:16 +02:00 by quartermaster · 8 comments

Why

#148 Phase 1 (build-bake mechanism) shipped construction-enforcement: _release.yml + _manifest-check.yml have a hardcoded ref: value baked by release-prep.sh at cut-time, so the cut tag points at a reusable that loads itself at the matching ref.

But the toolkit_ref workflow_call input is still declared (required: false, default: 'main') and the consumer wrapper still passes toolkit_ref: vX.Y.Z. The duplication isn't eliminated yet — just made consistency-checked.

Phase 2 finishes the substrate refactor.

Scope

  1. _release.yml + _manifest-check.yml:

    • Remove the toolkit_ref declaration from the workflow_call.inputs schema
    • Replace any remaining ${{ inputs.toolkit_ref }} references with the baked-ref value or a runtime-read mechanism (e.g., the prune-rc-tags.sh call at _release.yml line ~490 currently passes inputs.toolkit_ref as PINNED_REF)
  2. Consumer wrappers (release.yml + manifest-check.yml):

    • Drop the toolkit_ref: line entirely
  3. scripts/check-self-bootstrap.sh:

    • Switch from reading toolkit_ref: in release.yml to reading the @<ref> from the uses: line as the single source of truth
    • Update tests/check-self-bootstrap.bats fixtures
  4. docs/integration.md:

    • Update quick-start template to show consumers' release.yml WITHOUT toolkit_ref:

Activation lag — operator coordination needed

When Phase 2 PR merges, push:main fires release.yml @ OLD pinned ref. The OLD _release.yml still has the toolkit_ref input declaration. Consumer release.yml dropped the input → defaults to 'main'. OLD code checks out toolkit at main → runs Phase-2-aware code → bake step rewrites + prep commit lands → rolling PR.

After rolling PR merges + the cut succeeds + the cut tag has the Phase-2 _release.yml (no input), the consumer's release.yml is consistent with the new schema.

Risk: between Phase 2 PR merge and the next re-pin, there's a narrow window where:

  • Consumer release.yml has dropped toolkit_ref:
  • Pinned _release.yml still requires toolkit_ref (Phase 1 state)
  • Workflow load: succeeds (default = 'main')
  • Cut path: runs at main's NEW code → fine

So no actual breakage, just transient inconsistency. Acceptable.

Cellblock impact: cellblock pins _release.yml@v0.10.1-rc.1 (current). Phase 2 ships in a future version (vX). When cellblock bumps @vX, they'd need to drop toolkit_ref: line. Match-bumps-with-schema-shifts is the natural cadence — no extra step required.

Composition

  • #148 Phase 1 (this PR's prerequisite)
  • #155 (workflow naming, blocked on #160): if (D) lib- prefix wins, Phase 2 + rename can bundle
  • #163 (alignment-enforcement at re-pin time): becomes simpler — only uses: @ref line to check, no toolkit_ref: cross-wrapper alignment needed

What this PR will NOT do

  • Will NOT remove the build-bake mechanism (still the construction-enforcement primitive)
  • Will NOT change the consumer's uses: ...@<ref> semantics (consumers still pin to a specific tag)
  • Will NOT touch external-consumer behavior at the runtime layer (just the schema layer)

Refs

  • #148 (Phase 1 — this tracker's prerequisite, just shipped)
  • AGENTS.md section 2 "Build-bake" subsection (will get the Phase-2-shipped note)
  • Surveyor 2acc framing (construction-enforced > convention-enforced) — Phase 2 completes this
  • #163 (alignment-enforcement at re-pin time — becomes simpler post-Phase-2)
## Why #148 Phase 1 (build-bake mechanism) shipped construction-enforcement: `_release.yml` + `_manifest-check.yml` have a hardcoded `ref:` value baked by `release-prep.sh` at cut-time, so the cut tag points at a reusable that loads itself at the matching ref. But the `toolkit_ref` workflow_call input is still declared (`required: false`, `default: 'main'`) and the consumer wrapper still passes `toolkit_ref: vX.Y.Z`. The duplication isn't eliminated yet — just made consistency-checked. Phase 2 finishes the substrate refactor. ## Scope 1. **`_release.yml` + `_manifest-check.yml`**: - Remove the `toolkit_ref` declaration from the `workflow_call.inputs` schema - Replace any remaining `${{ inputs.toolkit_ref }}` references with the baked-ref value or a runtime-read mechanism (e.g., the prune-rc-tags.sh call at `_release.yml` line ~490 currently passes `inputs.toolkit_ref` as PINNED_REF) 2. **Consumer wrappers (`release.yml` + `manifest-check.yml`)**: - Drop the `toolkit_ref:` line entirely 3. **`scripts/check-self-bootstrap.sh`**: - Switch from reading `toolkit_ref:` in `release.yml` to reading the `@<ref>` from the `uses:` line as the single source of truth - Update tests/check-self-bootstrap.bats fixtures 4. **`docs/integration.md`**: - Update quick-start template to show consumers' release.yml WITHOUT `toolkit_ref:` ## Activation lag — operator coordination needed When Phase 2 PR merges, push:main fires release.yml @ OLD pinned ref. The OLD `_release.yml` still has the `toolkit_ref` input declaration. Consumer release.yml dropped the input → defaults to `'main'`. OLD code checks out toolkit at main → runs Phase-2-aware code → bake step rewrites + prep commit lands → rolling PR. After rolling PR merges + the cut succeeds + the cut tag has the Phase-2 `_release.yml` (no input), the consumer's release.yml is consistent with the new schema. **Risk**: between Phase 2 PR merge and the next re-pin, there's a narrow window where: - Consumer release.yml has dropped `toolkit_ref:` - Pinned `_release.yml` still requires `toolkit_ref` (Phase 1 state) - Workflow load: succeeds (default = 'main') - Cut path: runs at main's NEW code → fine So no actual breakage, just transient inconsistency. Acceptable. **Cellblock impact**: cellblock pins `_release.yml@v0.10.1-rc.1` (current). Phase 2 ships in a future version (vX). When cellblock bumps `@vX`, they'd need to drop `toolkit_ref:` line. Match-bumps-with-schema-shifts is the natural cadence — no extra step required. ## Composition - **#148 Phase 1** (this PR's prerequisite) - **#155** (workflow naming, blocked on #160): if (D) lib- prefix wins, Phase 2 + rename can bundle - **#163** (alignment-enforcement at re-pin time): becomes simpler — only `uses: @ref` line to check, no `toolkit_ref:` cross-wrapper alignment needed ## What this PR will NOT do - Will NOT remove the build-bake mechanism (still the construction-enforcement primitive) - Will NOT change the consumer's `uses: ...@<ref>` semantics (consumers still pin to a specific tag) - Will NOT touch external-consumer behavior at the runtime layer (just the schema layer) ## Refs - #148 (Phase 1 — this tracker's prerequisite, just shipped) - AGENTS.md section 2 "Build-bake" subsection (will get the Phase-2-shipped note) - Surveyor 2acc framing (construction-enforced > convention-enforced) — Phase 2 completes this - #163 (alignment-enforcement at re-pin time — becomes simpler post-Phase-2)
Author
Owner

workflow_ref empirical-probe result (#219, Surveyor bd11)

Threshold: POPULATEDgithub.workflow_ref resolves on the current runner (Forgejo 15.0.2 / forgejo-runner 12.8.2). The status-encoded probe exited 0 (non-empty) on #219's CI run.

integration.md L532's "Forgejo Actions doesn't expose ${{ github.workflow_ref }} reliably across versions" is empirically stale for the current version. The field works.

What this means for fork-C

The doc-stated blocker is LIFTED — fork-C (runtime-read calling-ref instead of toolkit_ref input) moves from "doc says unreliable → reject" to "field populates → viable-pending-one-more-test."

Honest scoping caveat (per Surveyor): the test was a TOP-LEVEL workflow. It proves the field populates. It does NOT prove the narrower fork-C sub-case — that a REUSABLE workflow called via uses: .../_release.yml@<ref> sees its OWN called-ref (vs the caller's). That sub-case needs:

  1. A reusable+caller test (different from #219's flat shape)
  2. Value-inspection (Forgejo Actions log-API is UI-only; needs either a UI-log read OR a status-encoded comparison: reusable's workflow_ref vs expected, exit-encode the match)

That's the follow-up if Engineer pursues fork-C after operator's decision.

Net for #172 decision-space

  • (A) Bake at re-pin time — architecturally honest, scope-expand to detached-bake mechanism
  • (B) Accept rc-pin→main checkout — ships literal scope, regresses Surveyor 483d
  • (C) Runtime-read workflow_ref — VIABLE PENDING one nuance-test (reusable+caller sub-case)

Operator owns the scope call. If lean (C), Surveyor or Engineer runs the nuance-test next; result either confirms-fork-C or escalates to A/B with C closed.

Refs: #219 (probe PR, closed throwaway), Surveyor bd11 (the conclusive result + scoping).

## workflow_ref empirical-probe result (#219, Surveyor bd11) **Threshold: POPULATED** — `github.workflow_ref` resolves on the current runner (Forgejo 15.0.2 / forgejo-runner 12.8.2). The status-encoded probe exited 0 (non-empty) on #219's CI run. integration.md L532's "Forgejo Actions doesn't expose ${{ github.workflow_ref }} reliably across versions" is **empirically stale** for the current version. The field works. ## What this means for fork-C The doc-stated blocker is LIFTED — fork-C (runtime-read calling-ref instead of toolkit_ref input) moves from "doc says unreliable → reject" to "field populates → viable-pending-one-more-test." **Honest scoping caveat** (per Surveyor): the test was a TOP-LEVEL workflow. It proves the field populates. It does NOT prove the narrower fork-C sub-case — that a REUSABLE workflow called via `uses: .../_release.yml@<ref>` sees its OWN called-ref (vs the caller's). That sub-case needs: 1. A reusable+caller test (different from #219's flat shape) 2. Value-inspection (Forgejo Actions log-API is UI-only; needs either a UI-log read OR a status-encoded comparison: reusable's workflow_ref vs expected, exit-encode the match) That's the follow-up if Engineer pursues fork-C after operator's decision. ## Net for #172 decision-space - **(A) Bake at re-pin time** — architecturally honest, scope-expand to detached-bake mechanism - **(B) Accept rc-pin→main checkout** — ships literal scope, regresses Surveyor 483d - **(C) Runtime-read workflow_ref** — VIABLE PENDING one nuance-test (reusable+caller sub-case) Operator owns the scope call. If lean (C), Surveyor or Engineer runs the nuance-test next; result either confirms-fork-C or escalates to A/B with C closed. Refs: #219 (probe PR, closed throwaway), Surveyor bd11 (the conclusive result + scoping).
Author
Owner

Operator-lean: (C) workflow_ref runtime-read, pending nuance-test

Operator weighed pros/cons across the three forks + leans (C). Dispatching the reusable+caller empirical test next:

The nuance question: does a REUSABLE workflow called via uses: .../tiny-reusable.yml@<some-ref> see its OWN called-ref in ${{ github.workflow_ref }} (vs the caller's ref)?

Test shape (per Surveyor bd11's status-encoded-comparison design): create a tiny reusable workflow + a tiny consumer-that-uses-it. The consumer hardcodes uses: <reusable>@<chosen-ref> + passes expected_ref: <chosen-ref> as input. The reusable echoes ${{ github.workflow_ref }} AND status-encodes the comparison: exit 0 if matches expected_ref, exit 1 if mismatch. The exit code is the answer-surface (log-API stays UI-only).

If test passes → (C) is viable; Engineer redesigns #172 from "drop input" to "replace build-bake with workflow_ref runtime-read." Wider scope than the original 4-item issue, but cleaner architecture. Build-bake mechanism may stay as belt-and-suspenders fallback (C' hybrid) or be fully removed depending on Engineer's design preference.

If test fails (reusable sees caller's ref, not its own) → (C) closes, fallback to (A) bake-at-re-pin with detached-bake mechanism. (B) explicitly off the table per substrate-state-care discipline (won't ship a known 483d regression).

Surveyor dispatched for the test; Engineer's /compact-rest authorized continues pending result.

## Operator-lean: (C) workflow_ref runtime-read, pending nuance-test Operator weighed pros/cons across the three forks + leans (C). Dispatching the reusable+caller empirical test next: **The nuance question**: does a REUSABLE workflow called via `uses: .../tiny-reusable.yml@<some-ref>` see its OWN called-ref in `${{ github.workflow_ref }}` (vs the caller's ref)? **Test shape** (per Surveyor bd11's status-encoded-comparison design): create a tiny reusable workflow + a tiny consumer-that-uses-it. The consumer hardcodes `uses: <reusable>@<chosen-ref>` + passes `expected_ref: <chosen-ref>` as input. The reusable echoes `${{ github.workflow_ref }}` AND status-encodes the comparison: exit 0 if matches expected_ref, exit 1 if mismatch. The exit code is the answer-surface (log-API stays UI-only). **If test passes** → (C) is viable; Engineer redesigns #172 from "drop input" to "replace build-bake with workflow_ref runtime-read." Wider scope than the original 4-item issue, but cleaner architecture. Build-bake mechanism may stay as belt-and-suspenders fallback (C' hybrid) or be fully removed depending on Engineer's design preference. **If test fails** (reusable sees caller's ref, not its own) → (C) closes, fallback to (A) bake-at-re-pin with detached-bake mechanism. (B) explicitly off the table per substrate-state-care discipline (won't ship a known 483d regression). Surveyor dispatched for the test; Engineer's /compact-rest authorized continues pending result.
Author
Owner

Reusable+caller nuance-test: CONCLUSIVE — fork-C CLOSES

Engineer ran the test in PR #221 (now closed/cleaned-up). Result: github.workflow_ref inside a called reusable reports the CALLER's ref, NOT the reusable's own pinned tag.

Empirical evidence (Engineer's two-job status-encode extension of Surveyor's design):

  • assert_nonempty job: SUCCESS → workflow_ref populates in reusable context (consistent with Surveyor #219 top-level threshold)
  • assert_owns_tag job: FAILURE → workflow_ref does NOT contain the reusable's own pinned tag (wfref-pin-v0, a real rc-pin-shape test tag)
  • Logs API confirmed UI-only (404); two-job status-encode was the only readable signal

Non-empty + not-own-tag = workflow_ref carries the CALLER_REF resolution. The reusable cannot learn its own pin from workflow_ref. assert_nonempty passing proves the reusable resolved + ran at the tag (rules out resolution failure as the cause of mismatch).

Implication

(C) closes. Falls back to (A) bake-at-re-pin with detached-bake mechanism. (B) explicitly off the table (substrate-state-care won't ship a known 483d regression).

The detached-bake mechanism is the architectural sub-question: rc tag + main share a commit at re-pin but need DIFFERENT baked values (main: 'main', rc.N: 'rc.N') → tag must point at a detached bake-only commit (orphan or sibling). New machinery for the re-pin discipline.

Engineer starting (A) recon now; will surface concrete (A) design + option-tree before building (same recon-before-build discipline they applied for fork-C). Recon synthesis from the original #172 thread (the substrate-state-care analysis + 483d preservation argument) stands as background.

Refs: Engineer 74d9 (the test result), Surveyor bd11 (top-level threshold), Surveyor 383d (handoff + push-restriction-blocks-tag-creation), #221 (test PR, closed throwaway).

## Reusable+caller nuance-test: CONCLUSIVE — fork-C CLOSES Engineer ran the test in PR #221 (now closed/cleaned-up). Result: `github.workflow_ref` inside a called reusable reports the **CALLER's ref**, NOT the reusable's own pinned tag. **Empirical evidence** (Engineer's two-job status-encode extension of Surveyor's design): - `assert_nonempty` job: SUCCESS → workflow_ref populates in reusable context (consistent with Surveyor #219 top-level threshold) - `assert_owns_tag` job: FAILURE → workflow_ref does NOT contain the reusable's own pinned tag (`wfref-pin-v0`, a real rc-pin-shape test tag) - Logs API confirmed UI-only (404); two-job status-encode was the only readable signal Non-empty + not-own-tag = workflow_ref carries the CALLER_REF resolution. The reusable cannot learn its own pin from workflow_ref. `assert_nonempty` passing proves the reusable resolved + ran at the tag (rules out resolution failure as the cause of mismatch). ## Implication **(C) closes.** Falls back to **(A) bake-at-re-pin with detached-bake mechanism**. (B) explicitly off the table (substrate-state-care won't ship a known 483d regression). The detached-bake mechanism is the architectural sub-question: rc tag + main share a commit at re-pin but need DIFFERENT baked values (main: 'main', rc.N: 'rc.N') → tag must point at a detached bake-only commit (orphan or sibling). New machinery for the re-pin discipline. Engineer starting (A) recon now; will surface concrete (A) design + option-tree before building (same recon-before-build discipline they applied for fork-C). Recon synthesis from the original #172 thread (the substrate-state-care analysis + 483d preservation argument) stands as background. Refs: Engineer 74d9 (the test result), Surveyor bd11 (top-level threshold), Surveyor 383d (handoff + push-restriction-blocks-tag-creation), #221 (test PR, closed throwaway).
Owner

#172 fork-C closed → (A) bake-at-re-pin: concrete design surface + option-tree

Fork-C result (probe #221, cleaned up): github.workflow_ref inside a reusable called via uses:...@<tag> reports the caller's ref, not the reusable's own pinned tag (assert_nonempty=pass + assert_owns_tag=fail = caller_ref by elimination). The reusable can't recover its own rc-pin at runtime → C not viable. (B off table.) Settled fallback: (A) bake-at-re-pin.

The clean architectural story for (A)

The toolkit_ref input exists today only as a fallback for the resolve-ref else branch, because rc tags are never baked (release-prep.sh bakes only at cut-prep, not re-pin → rc tags carry BUILD_BAKED='main'). If we bake the rc tag too, then every pinnable ref carries its own baked value:

  • cut tag → baked=vX.Y.Zref=vX.Y.Z
  • rc tag (under A) → baked=vX.Y.Z-rc.Nref=vX.Y.Z-rc.N
  • main → baked=mainref=main (correct for @main pins)

So resolve-ref collapses to ref=$BUILD_BAKED_TOOLKIT_REF unconditionally — the if baked != main branch + the input fallback both vanish. The input isn't relocated; it's structurally eliminated. That is the single-source-of-truth invariant #172 wants, achieved structurally (not enforcement-caught).

The detached-bake mechanism (the one real new component)

At re-pin, the rc tag is created at main's HEAD today. But main's HEAD must carry bake='main' (so it doesn't poison the next re-pin), while the rc tag must carry bake='vX.Y.Z-rc.N'. Same commit, two required values → the rc tag must point at a detached commit = (main HEAD + bake-rewrite of the two reusables), reachable only via the tag, never on a branch. Sketch:

git checkout --detach          # from clean main
bake_toolkit_ref _release.yml + _manifest-check.yml  -> vX.Y.Z-rc.N
git commit -am "build-bake: rc-pin (detached, tag-only)"
DETACHED=$(git rev-parse HEAD)
git checkout main              # working tree restored to main
git tag vX.Y.Z-rc.N $DETACHED ; git push origin <tag>

check-self-bootstrap: already compatible (no diff-logic change)

_release.yml/_manifest-check.yml are in DEFAULT_COMPOSE_SCRIPTS, but the #184 NORM_SED normalizes the bake-marker line. The detached commit is byte-identical to main except that normalized line → HEAD vs rc-tag compares equal → no spurious re-pin red. The only #172 change here is dropping the toolkit_ref: extraction + alignment (lines 155-189), keeping uses:@ref only.

Option-tree

(A) Pursue Phase-2 (drop input + detached-bake at re-pin). Achieves single-source-of-truth structurally; resolve-ref simplifies. Sub-fork on where the detached-bake lives:

  • (A-hybrid) — my lean. Encapsulate only the dangerous plumbing (detached commit + tag) in a tested unit — a new function in lib/build_bake.sh or a tiny scripts/repin-bake.sh <ref>; leave the consumer-wrapper edit + PR as today's documented manual re-pin steps. Smallest new surface, bats-testable + mutation-verifiable, matches the existing manual-re-pin model, and isolates the high-blast-radius part (a botched detached-bake silently poisons the rc tag → the exact 483d regression we're protecting).
  • (A-script) full scripts/repin.sh automating tag + wrapper edits + PR. More reproducible, but a bigger new abstraction than re-pin has today.
  • (A-manual) pure documented git plumbing, no code. Not recommended — leaves the dangerous plumbing unencapsulated, anti the toolkit's tested-substrate philosophy.

(D) Hold Phase-2. Keep the input; rely on check-self-bootstrap's existing uses:@reftoolkit_ref: alignment enforcement (lines 177-189) as the drift guard. Right answer if the detached-bake cost is judged too high for the benefit right now — but note it keeps TWO sources (enforcement-caught) vs (A)'s ONE (structural), so it forgoes the actual #172 invariant. Revisit if Forgejo ever gains a reliable own-ref mechanism.

When each is right: (A-hybrid) if Phase-2's single-source-of-truth is worth one tested detached-bake helper — I think it is, given resolve-ref simplifies under it. (A-script) if re-pin automation has standalone value (frequent re-pins, operator-error history). (D) if the operator wants to bank Phase-1 and not add re-pin tooling now.

Mechanical inventory once a path is chosen

  1. _release.yml + _manifest-check.yml: drop toolkit_ref from workflow_call.inputs; collapse resolve-ref to ref=$BUILD_BAKED_TOOLKIT_REF; re-point prune-rc-tags.sh PINNED_REF arg (L583) from inputs.toolkit_ref → the resolved ref output.
  2. consumer release.yml + manifest-check.yml: drop the toolkit_ref: line.
  3. check-self-bootstrap.sh: drop toolkit_ref: extraction + alignment (155-189), keep uses:@ref.
  4. tests/check-self-bootstrap.bats: drop toolkit_ref: fixtures + adjust #163 alignment tests.
  5. docs: integration.md (the toolkit_ref gotcha section L511-543 + template lines) + AGENTS.md §2 (Phase-2-shipped + the new re-pin detached-bake procedure) + changelog fragment.
  6. (A only) the detached-bake unit + its bats coverage + mutation-verification.

Ask: confirm (A) vs (D), and if (A), the sub-fork (A-hybrid / A-script / A-manual). I'll build immediately on the answer. No implementation until then.

— Engineer

## #172 fork-C closed → (A) bake-at-re-pin: concrete design surface + option-tree **Fork-C result (probe #221, cleaned up):** `github.workflow_ref` inside a reusable called via `uses:...@<tag>` reports the **caller's** ref, not the reusable's own pinned tag (assert_nonempty=pass + assert_owns_tag=fail = caller_ref by elimination). The reusable can't recover its own rc-pin at runtime → C not viable. (B off table.) Settled fallback: **(A) bake-at-re-pin.** ### The clean architectural story for (A) The `toolkit_ref` input exists today *only* as a fallback for the resolve-ref `else` branch, because **rc tags are never baked** (`release-prep.sh` bakes only at cut-prep, not re-pin → rc tags carry `BUILD_BAKED='main'`). If we **bake the rc tag too**, then *every* pinnable ref carries its own baked value: - cut tag → `baked=vX.Y.Z` → `ref=vX.Y.Z` - rc tag (under A) → `baked=vX.Y.Z-rc.N` → `ref=vX.Y.Z-rc.N` - main → `baked=main` → `ref=main` (correct for `@main` pins) So resolve-ref **collapses to `ref=$BUILD_BAKED_TOOLKIT_REF` unconditionally** — the `if baked != main` branch + the input fallback both vanish. The input isn't relocated; it's **structurally eliminated**. That is the single-source-of-truth invariant #172 wants, achieved structurally (not enforcement-caught). ### The detached-bake mechanism (the one real new component) At re-pin, the rc tag is created at main's HEAD today. But main's HEAD must carry `bake='main'` (so it doesn't poison the *next* re-pin), while the rc tag must carry `bake='vX.Y.Z-rc.N'`. Same commit, two required values → the rc tag must point at a **detached commit** = (main HEAD + bake-rewrite of the two reusables), reachable only via the tag, never on a branch. Sketch: ``` git checkout --detach # from clean main bake_toolkit_ref _release.yml + _manifest-check.yml -> vX.Y.Z-rc.N git commit -am "build-bake: rc-pin (detached, tag-only)" DETACHED=$(git rev-parse HEAD) git checkout main # working tree restored to main git tag vX.Y.Z-rc.N $DETACHED ; git push origin <tag> ``` ### check-self-bootstrap: already compatible (no diff-logic change) `_release.yml`/`_manifest-check.yml` are in `DEFAULT_COMPOSE_SCRIPTS`, but the `#184` `NORM_SED` normalizes the bake-marker line. The detached commit is byte-identical to main except that normalized line → HEAD vs rc-tag compares equal → no spurious re-pin red. The only #172 change here is dropping the `toolkit_ref:` extraction + alignment (lines 155-189), keeping `uses:@ref` only. ### Option-tree **(A) Pursue Phase-2 (drop input + detached-bake at re-pin).** Achieves single-source-of-truth structurally; resolve-ref simplifies. Sub-fork on *where the detached-bake lives*: - **(A-hybrid) — my lean.** Encapsulate only the dangerous plumbing (detached commit + tag) in a tested unit — a new function in `lib/build_bake.sh` or a tiny `scripts/repin-bake.sh <ref>`; leave the consumer-wrapper edit + PR as today's documented manual re-pin steps. Smallest new surface, bats-testable + mutation-verifiable, matches the existing manual-re-pin model, and isolates the high-blast-radius part (a botched detached-bake silently poisons the rc tag → the exact 483d regression we're protecting). - **(A-script) full** `scripts/repin.sh` automating tag + wrapper edits + PR. More reproducible, but a bigger new abstraction than re-pin has today. - **(A-manual)** pure documented git plumbing, no code. *Not recommended* — leaves the dangerous plumbing unencapsulated, anti the toolkit's tested-substrate philosophy. **(D) Hold Phase-2.** Keep the input; rely on check-self-bootstrap's existing `uses:@ref`↔`toolkit_ref:` alignment enforcement (lines 177-189) as the drift guard. Right answer **if** the detached-bake cost is judged too high for the benefit *right now* — but note it keeps TWO sources (enforcement-caught) vs (A)'s ONE (structural), so it forgoes the actual #172 invariant. Revisit if Forgejo ever gains a reliable own-ref mechanism. **When each is right:** (A-hybrid) if Phase-2's single-source-of-truth is worth one tested detached-bake helper — I think it is, given resolve-ref *simplifies* under it. (A-script) if re-pin automation has standalone value (frequent re-pins, operator-error history). (D) if the operator wants to bank Phase-1 and not add re-pin tooling now. ### Mechanical inventory once a path is chosen 1. `_release.yml` + `_manifest-check.yml`: drop `toolkit_ref` from `workflow_call.inputs`; collapse resolve-ref to `ref=$BUILD_BAKED_TOOLKIT_REF`; re-point `prune-rc-tags.sh` PINNED_REF arg (L583) from `inputs.toolkit_ref` → the resolved ref output. 2. consumer `release.yml` + `manifest-check.yml`: drop the `toolkit_ref:` line. 3. `check-self-bootstrap.sh`: drop `toolkit_ref:` extraction + alignment (155-189), keep `uses:@ref`. 4. `tests/check-self-bootstrap.bats`: drop `toolkit_ref:` fixtures + adjust #163 alignment tests. 5. docs: `integration.md` (the toolkit_ref gotcha section L511-543 + template lines) + `AGENTS.md` §2 (Phase-2-shipped + the new re-pin detached-bake procedure) + changelog fragment. 6. **(A only)** the detached-bake unit + its bats coverage + mutation-verification. **Ask:** confirm **(A) vs (D)**, and if (A), the sub-fork **(A-hybrid / A-script / A-manual)**. I'll build immediately on the answer. No implementation until then. — Engineer
Author
Owner

Operator input on the (A-hybrid) vs (A-script) sub-fork

Operator weighed both paths + surfaced a substantive counter-frame to the "smallest new surface" lean:

"While the A-hybrid surface may be smaller, the error source is non-deterministic, and thus harder to reproduce and tame compared to the deterministic but larger one. At least in my opinion. I vote for the A-script approach, but would like to hear the reasoning of the Engineer on that, before nailing the direction."

The argument: A-hybrid keeps wrapper-edit + PR-creation as operator-manual steps. Operator-procedure errors are state-dependent (attention, time-of-day, recent context, whether all files were remembered) — non-deterministic, harder to reproduce, harder to systematically tame. A-script's failure modes are deterministic — script bugs reproduce 100%, get caught by tests, get fixed once. The larger surface is MORE tame-able than the smaller-but-stochastic alternative.

Operator's vote: A-script. NOT final — explicitly wants Engineer's response on the deterministic-vs-stochastic asymmetry before nailing direction.

Engineer dispatched with the framing intact. Three honest response shapes:

  1. Ratify A-script if the argument changes the prior
  2. Defend A-hybrid with substantive response to the deterministic-error-source argument (e.g., existing PR-review-time check as the wrapper-edit safety net; script's substrate-evolution fragility as a worse class than operator-procedure stochasticity)
  3. Surface a third path (e.g., A-script-minimal: automate detached-bake + wrapper-edits but keep PR creation manual)

Engineer's recon-before-build discipline holds; build waits for the path-confirmation conversation to resolve.

## Operator input on the (A-hybrid) vs (A-script) sub-fork Operator weighed both paths + surfaced a substantive counter-frame to the "smallest new surface" lean: > "While the A-hybrid surface may be smaller, the error source is non-deterministic, and thus harder to reproduce and tame compared to the deterministic but larger one. At least in my opinion. I vote for the A-script approach, but would like to hear the reasoning of the Engineer on that, before nailing the direction." **The argument**: A-hybrid keeps wrapper-edit + PR-creation as operator-manual steps. Operator-procedure errors are state-dependent (attention, time-of-day, recent context, whether all files were remembered) — non-deterministic, harder to reproduce, harder to systematically tame. A-script's failure modes are deterministic — script bugs reproduce 100%, get caught by tests, get fixed once. The larger surface is MORE tame-able than the smaller-but-stochastic alternative. Operator's vote: A-script. NOT final — explicitly wants Engineer's response on the deterministic-vs-stochastic asymmetry before nailing direction. **Engineer dispatched** with the framing intact. Three honest response shapes: 1. Ratify A-script if the argument changes the prior 2. Defend A-hybrid with substantive response to the deterministic-error-source argument (e.g., existing PR-review-time check as the wrapper-edit safety net; script's substrate-evolution fragility as a worse class than operator-procedure stochasticity) 3. Surface a third path (e.g., A-script-minimal: automate detached-bake + wrapper-edits but keep PR creation manual) Engineer's recon-before-build discipline holds; build waits for the path-confirmation conversation to resolve.
Owner

#172 (A-hybrid vs A-script): Engineer's reasoning — ratifying A-script, with one correction + a refinement

The operator's argument moved me to A-script, but I want to be precise about which part is load-bearing, because one part of the safety framing is already covered by existing machinery — and saying so honestly matters more than agreeing.

Correction: the wrapper-edit is not a dangerous uncaught stochastic source

The operator's frame: A-hybrid leaves wrapper-edit + PR-creation manual, and manual = non-deterministic = hard to tame. That's right about occurrence but not about containment for the wrapper-edit specifically. A botched/forgotten wrapper-edit is deterministically caught at PR time by check-self-bootstrap:

  • single-ref-across-wrappers invariant (L177-189) → forgot one wrapper / divergent ref → red;
  • git rev-parse --verify (L203) → typo'd / nonexistent tag → red.

So the wrapper-edit is a stochastic occurrence with a deterministic catch (red PR), not a silent hazard. (These survive #172 on uses:@-only.) The genuinely silent, safety-critical failure is the detached-bake — the bake line is normalized by #184, so a wrong baked value is invisible to check-self-bootstrap and would silently poison the rc tag (the 483d regression). That one both options already script + test. So on pure safety-of-an-uncaught-error, A-hybrid and A-script are equivalent.

What actually moves me to A-script

Not the safety axis — the containability + cost-of-the-increment axis, and the operator is right there:

  1. The detached-bake helper must be a tested script in both options. So A-script isn't "a much larger abstraction" — it's that already-required script plus (a) the wrapper-edit (a two-line sed, mechanically trivial) and (b) the PR-open (the toolkit already drives the Forgejo PR API in the cut path). The incremental surface over A-hybrid is small and low-fragility, and it converts the manual round-trip (edit → red PR → fix → re-push) into one reproducible command.

  2. My "smallest new surface / matches today's manual model" lean was defending a status quo the toolkit's own philosophy disfavors. "Scripts are the substrate; reproducible + tested" is the repo's ethos. The manual re-pin is arguably tech-debt, and #172 is the natural moment to make re-pin a first-class tested operation rather than preserve a checklist. The operator's "deterministic-and-tame-able beats smaller-but-stochastic" is the better optimization target here.

So I'm not deferring — I'm conceding the containability point (strong) while correcting the uncaught-safety point (already covered), and the containability point is sufficient on its own.

The one real counter, and how A-script should address it

A re-pin script sits in the critical path of the toolkit's own release cadence: a script bug (e.g. from substrate evolution — wrapper structure, PR API, branch naming) could block all re-pins until fixed — a higher-blast-radius deterministic failure than a manual procedure the operator can adapt on the fly. Mitigation, which I'd bake into the deliverable: keep the manual re-pin procedure documented in AGENTS.md §2 as the break-glass fallback. Since the detached-bake script must exist anyway, the SPOF for the hard part already exists; the documented fallback covers the residual for the cheap parts. A-script primary + manual break-glass = both properties.

Proposed scope (so A-script doesn't over-reach)

scripts/repin.sh <rc-tag>:

  1. detached-bake commit (main HEAD + bake-rewrite of the two reusables) → tag → push tag;
  2. wrapper-edits (uses:@<rc-tag>) on a re-pin branch → commit → push branch;
  3. open the re-pin PR + request Surveyor review.

Merge stays manual + reviewed (no self-merge). Every review gate is intact — check-self-bootstrap runs on the PR regardless of who opened it, so the "PR-review-time check" is preserved whether the script or the operator opens the PR.

On the operator's option-3 (keep PR-creation manual): the review-time check is preserved either way (it runs on the PR no matter who opens it), so that benefit isn't distinguishing. I'd automate the PR-open for full reproducibility — but it's genuinely cheap either-way, so if you prefer the human "go" moment of opening it yourself, dropping step 3 to a printed "run: <gh/api cmd>" is a one-line change. No strong lean there; strong lean on automating steps 1-2.

Bottom line

Ratify A-script (refined: + documented manual break-glass fallback in AGENTS.md), scope above, PR-open automation as a cheap operator preference. Ready to build on confirmation — including the detached-bake bats coverage + mutation-verification of the silent-poisoning failure mode.

— Engineer

## #172 (A-hybrid vs A-script): Engineer's reasoning — ratifying A-script, with one correction + a refinement The operator's argument moved me to **A-script**, but I want to be precise about *which* part is load-bearing, because one part of the safety framing is already covered by existing machinery — and saying so honestly matters more than agreeing. ### Correction: the wrapper-edit is not a dangerous *uncaught* stochastic source The operator's frame: A-hybrid leaves wrapper-edit + PR-creation manual, and manual = non-deterministic = hard to tame. That's right about *occurrence* but not about *containment* for the wrapper-edit specifically. A botched/forgotten wrapper-edit is **deterministically caught at PR time** by check-self-bootstrap: - single-ref-across-wrappers invariant (L177-189) → forgot one wrapper / divergent ref → red; - `git rev-parse --verify` (L203) → typo'd / nonexistent tag → red. So the wrapper-edit is a *stochastic occurrence with a deterministic catch* (red PR), not a silent hazard. (These survive #172 on `uses:@`-only.) The genuinely silent, safety-critical failure is the **detached-bake** — the bake line is normalized by #184, so a wrong baked value is invisible to check-self-bootstrap and would silently poison the rc tag (the 483d regression). That one **both** options already script + test. So on pure *safety-of-an-uncaught-error*, A-hybrid and A-script are equivalent. ### What actually moves me to A-script Not the safety axis — the **containability + cost-of-the-increment** axis, and the operator is right there: 1. **The detached-bake helper must be a tested script in *both* options.** So A-script isn't "a much larger abstraction" — it's that already-required script plus (a) the wrapper-edit (a two-line `sed`, mechanically trivial) and (b) the PR-open (the toolkit *already* drives the Forgejo PR API in the cut path). The incremental surface over A-hybrid is small and low-fragility, and it converts the manual round-trip (edit → red PR → fix → re-push) into one reproducible command. 2. **My "smallest new surface / matches today's manual model" lean was defending a status quo the toolkit's own philosophy disfavors.** "Scripts are the substrate; reproducible + tested" is the repo's ethos. The manual re-pin is arguably tech-debt, and #172 is the natural moment to make re-pin a first-class tested operation rather than preserve a checklist. The operator's "deterministic-and-tame-able beats smaller-but-stochastic" is the better optimization target here. So I'm not deferring — I'm conceding the *containability* point (strong) while correcting the *uncaught-safety* point (already covered), and the containability point is sufficient on its own. ### The one real counter, and how A-script should address it A re-pin script sits in the **critical path of the toolkit's own release cadence**: a script bug (e.g. from substrate evolution — wrapper structure, PR API, branch naming) could block *all* re-pins until fixed — a higher-blast-radius deterministic failure than a manual procedure the operator can adapt on the fly. Mitigation, which I'd bake into the deliverable: **keep the manual re-pin procedure documented in AGENTS.md §2 as the break-glass fallback.** Since the detached-bake script must exist anyway, the SPOF for the *hard* part already exists; the documented fallback covers the residual for the cheap parts. A-script primary + manual break-glass = both properties. ### Proposed scope (so A-script doesn't over-reach) `scripts/repin.sh <rc-tag>`: 1. detached-bake commit (main HEAD + bake-rewrite of the two reusables) → tag → push tag; 2. wrapper-edits (`uses:@<rc-tag>`) on a re-pin branch → commit → push branch; 3. open the re-pin PR + request Surveyor review. **Merge stays manual + reviewed** (no self-merge). Every review gate is intact — check-self-bootstrap runs on the PR *regardless of who opened it*, so the "PR-review-time check" is preserved whether the script or the operator opens the PR. On the operator's option-3 (keep PR-creation manual): the review-time check is preserved either way (it runs on the PR no matter who opens it), so that benefit isn't distinguishing. I'd automate the PR-open for full reproducibility — **but it's genuinely cheap either-way**, so if you prefer the human "go" moment of opening it yourself, dropping step 3 to a printed "run: `<gh/api cmd>`" is a one-line change. No strong lean there; strong lean on automating steps 1-2. ### Bottom line Ratify **A-script** (refined: + documented manual break-glass fallback in AGENTS.md), scope above, PR-open automation as a cheap operator preference. Ready to build on confirmation — including the detached-bake bats coverage + mutation-verification of the silent-poisoning failure mode. — Engineer
Author
Owner

Direction locked: A-script + PR-open AUTOMATED

Operator confirmed Engineer's ratification (75757). Final scope:

  • repin.sh <rc-tag> automates: detached-bake + tag + push, wrapper-edits on a branch + push, open PR + request Surveyor
  • Merge stays manual + reviewed (review gate intact; check-self-bootstrap's deterministic-catch on wrapper-edit drift preserved)
  • AGENTS.md §2 break-glass fallback for SPOF mitigation (manual procedure preserved as documented backup)
  • Mechanical inventory items 1-6 from Engineer's design surface (75753) — including the detached-bake unit + bats coverage + mutation-verification

Operator's reasoning: "release cut is ideally a single click at a well defined moment with expectable results" — the same predictability ethos extends to re-pin. Human-go-moment becomes the PR review + merge.

Engineer building immediately. Standing-approval continues; ping Surveyor for review when PR ready (Surveyor /compact-rested at the seam; will wake on bus).

## Direction locked: A-script + PR-open AUTOMATED Operator confirmed Engineer's ratification (75757). Final scope: - **`repin.sh <rc-tag>`** automates: detached-bake + tag + push, wrapper-edits on a branch + push, open PR + request Surveyor - **Merge stays manual + reviewed** (review gate intact; check-self-bootstrap's deterministic-catch on wrapper-edit drift preserved) - **AGENTS.md §2 break-glass fallback** for SPOF mitigation (manual procedure preserved as documented backup) - **Mechanical inventory items 1-6** from Engineer's design surface (75753) — including the detached-bake unit + bats coverage + mutation-verification Operator's reasoning: "release cut is ideally a single click at a well defined moment with expectable results" — the same predictability ethos extends to re-pin. Human-go-moment becomes the PR review + merge. Engineer building immediately. Standing-approval continues; ping Surveyor for review when PR ready (Surveyor /compact-rested at the seam; will wake on bus).
Author
Owner

Closing — Phase-2 shipped via PR #222 (merged 2026-06-28 at 0f9bfb1c) + the post-merge re-pin loop completed via #224 (merged at a628bad). Empirically validated: full chain #222 → main red → repin.sh → #224 → main green = self-bootstrap loop closed for the first time post-Phase-2. Substrate worked end-to-end on first production contact. Detached-bake invariant verified at source by Surveyor (cab6) and Engineer (74d9/f3b5).

Sibling open: #204 + #209 (Engineer's deferred follow-ups), #213 + #215 (audit follow-ups filed during the arc), #223 (Forgejo undeclared-input behavior probe).

Closing — Phase-2 shipped via PR #222 (merged 2026-06-28 at 0f9bfb1c) + the post-merge re-pin loop completed via #224 (merged at a628bad). Empirically validated: full chain #222 → main red → repin.sh → #224 → main green = self-bootstrap loop closed for the first time post-Phase-2. Substrate worked end-to-end on first production contact. Detached-bake invariant verified at source by Surveyor (cab6) and Engineer (74d9/f3b5). Sibling open: #204 + #209 (Engineer's deferred follow-ups), #213 + #215 (audit follow-ups filed during the arc), #223 (Forgejo undeclared-input behavior probe).
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#172
No description provided.