Skip to content

Commit 151ed73

Browse files
docs: record the v1.11.0 release and move fleet ops knowledge out of local notes
Everything durable that had accumulated in the gitignored CLAUDE.local.md is now tracked. That file is a capture buffer for to-dos, not a notebook — anything living only there is invisible to a fresh clone and to every other session. New docs/fleet-operations.md — the operational knowledge around the release order, which README stays authoritative for: - the fleet inventory and why 21 and 18 are both correct numbers depending on the question (the 3-pair delta carries stub rails only) - why waves are direct pushes with a skip-ci marker rather than 21 PRs: zero review runs, and zero theme deploys on the ~10 branches that have them - the three traps — same basename in both halves of the diff, pin hunks that patch from a base the fleet was never on, and per-repo state that must survive - what fleet-pin-audit.sh cannot see: files with no `uses:` line, file content, and drift in its own reference. All three bit during v1.9.0 -> v1.11.0. - how to pilot a cross-repo reusable, including the two traps this pilot hit README: release-history sections for v1.9.0, v1.10.0 and v1.11.0, and the chain extended past v1.8.0. v1.9.0's entry records that it never got its kit repin commit, since that is the gap the audit could not see. The v1.11.0-specific wave warning in the release order is generalised into the durable rule (sed pin lines, never git apply them) and points at fleet-operations for the rest. reusable-conversion-scope.md: Phases 2 and 3 marked done, status flipped to COMPLETE, and the Phase 2 assertion resolved in place with the evidence — the pilot proved vars.BONSAI_URL resolves against the caller, so the per-repo tunnel override survives. The two pilot-design corrections are recorded there and in fleet-operations, because the claude.yml pilot would otherwise repeat them. The driver-agents canonical-text follow-up moved to that repo's CLAUDE.local.md, where the session that can act on it will see it. (Commit message deliberately avoids the literal skip-ci token: GitHub scans the whole message, not just the subject, so spelling it out here suppressed CI on this very PR the first time round.)
1 parent 331a322 commit 151ed73

3 files changed

Lines changed: 226 additions & 13 deletions

File tree

README.md

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,53 @@ rail) → `v1.5.1` (drop the `gh`-based author re-check that skipped every real
6464
(`allowed_bots: claude[bot]`, so the bot-opened round 1 actually reviews) → `v1.5.3` (always-latest
6565
resilient Claude Code self-install in the three agent reusables) → `v1.5.4` (`dependabot-validate`:
6666
npm-install fallback for lockfile-less repos + `actions/checkout` v7) → `v1.5.5` (claude-code-action
67-
1.0.161 → 1.0.168 in the agent reusables) → `v1.6.0``v1.7.0`**`v1.8.0`** (all below).
68-
`v1.3.0` was never tagged.
67+
1.0.161 → 1.0.168 in the agent reusables) → `v1.6.0``v1.7.0``v1.8.0``v1.9.0``v1.10.0`
68+
**`v1.11.0`** (all below). `v1.3.0` was never tagged.
69+
70+
### `v1.11.0` (`90f0d06`, 2026-08-02)
71+
72+
Waved to all 21 repin targets on 2026-08-02; fleet uniform, 108 pins, zero stale.
73+
74+
- **`bonsai-status-sync.yml` conversion completed.** The 190-line per-repo copy became a 67-line
75+
caller stub — the status machine, actor gate, linkage logic and cascade caveat now live in one
76+
central file. The reusable itself landed one tag earlier (see `v1.10.0`), so the conversion spans
77+
the two tags: a new reusable's stub cannot be pinned until the tag containing it exists.
78+
- **kit `claude.yml` + `shopify-tool-smoke.yml`:** `DRIVER_AGENTS_REF``4d63371`. The previous pin
79+
`0bbb125` predated `graphql_guard.py`, so every fleet runner executed `admin-graphql.sh` with no
80+
fail-closed allowlist and the Driver Engineering scope grant was the only control on destructive
81+
mutations.
82+
- **Shopify operator tripwire** appended to `claude.yml`'s static `--append-system-prompt`, pairing
83+
with that wrapper. The blockquote is copied verbatim from driver-agents
84+
`docs/agent-instructions-shopify.md` (canonical — edit there first); a non-canonical kit-side
85+
lead-in precedes it, un-scoping the block from the conduct rules above and telling the model how to
86+
report a trip on a rail that cannot set a job exit code.
87+
- **this repo's own CI:** `lint.yml` gained a tokenization guard asserting `claude_args` holds
88+
exactly four single quotes and the system prompt contains no apostrophe or `$`. One apostrophe
89+
typed into canonical upstream silently *truncates* the prompt — `shell-quote` does not throw, every
90+
flag still parses, and the wave would copy the truncated prompt fleet-wide green.
91+
- **Piloted before the wave:** `vars.BONSAI_URL` proven to resolve against the caller, so the
92+
per-repo tunnel override survives the conversion. See
93+
[`docs/fleet-operations.md`](docs/fleet-operations.md).
94+
95+
### `v1.10.0` (`b394c6d`, 2026-08-02)
96+
97+
- **New sixth reusable: `.github/workflows/bonsai-status-sync.yml`.** Its `jobs:` body is
98+
byte-identical to the old per-repo copy except one added comment. Deliberately shipped without its
99+
caller stub — see `v1.11.0`.
100+
- **`lint.yml` placeholder-pin guard:** fails the build on any kit stub still carrying an all-zero
101+
pin, so a stub that cannot resolve can never reach `main`.
102+
- Never waved on its own; superseded by `v1.11.0` two commits later.
103+
104+
### `v1.9.0` (`a54c91e`, 2026-08-01, kit-only)
105+
106+
- **Store app secrets renamed** `DRIVER_AGENTS_SCOPES_CLIENT_ID/_SECRET`
107+
`DRIVER_ENGINEERING_APP_CLIENT_ID/_SECRET`, tied to the per-org "Driver Engineering" app that
108+
replaced "Driver Agents Scopes" (retired 2026-08-01). Waved to all 21 targets; Avara's smoke test
109+
green on the new names, old-name secrets deleted.
110+
- **Reusables unchanged.** Note this release **never got its kit repin commit**`templates/` sat at
111+
`v1.8.0`'s SHA while the deployed fleet was waved to `a54c91e`, leaving the fleet a release *ahead*
112+
of the kit templates until `v1.11.0` closed it. That gap is invisible to
113+
`tools/fleet-pin-audit.sh`; see [`docs/fleet-operations.md`](docs/fleet-operations.md).
69114

70115
### `v1.8.0` (2026-07-31, kit-only)
71116

@@ -140,13 +185,13 @@ npm-install fallback for lockfile-less repos + `actions/checkout` v7) → `v1.5.
140185
wave script rewrites `templates/github/``.github/workflows/`, so assert the rewritten diff
141186
touches no destination path twice before applying — otherwise the reusable can land in a client
142187
repo *as* the workflow, where it is `workflow_call`-only, fires on nothing, and looks green.
143-
- **This wave only — the five pre-existing stubs' pin hunks will not apply.** They patch from
144-
`80c35fe` (`v1.8.0`), but every deployed stub holds `a54c91e` (`v1.9.0`): `v1.9.0` shipped without
145-
a kit repin commit while the 2026-08-01 wave repinned the fleet anyway, so **no kit revision has
146-
ever carried `a54c91e` in a pin line** and no diff base produces a matching `-` line. `git apply`
147-
rejects all five on target #1. Apply the kit diff restricted to
148-
`templates/github/bonsai-status-sync.yml` (a whole-file replacement) and let the wave script's
149-
existing `sed` repin handle the other five pin lines.
188+
- **Sed pin lines; never `git apply` them.** A pin hunk patches from whatever SHA the kit held,
189+
which is not necessarily what the fleet holds — at v1.11.0 the kit diff patched from `80c35fe`
190+
(v1.8.0) while every deployed stub held `a54c91e` (v1.9.0), a SHA no kit revision had ever
191+
carried in a pin line, so no diff base produced a matching `-` line and `git apply` would have
192+
rejected all five files on target #1.
193+
- Wave mechanics, the guards worth keeping, and what the pin audit cannot see:
194+
[`docs/fleet-operations.md`](docs/fleet-operations.md).
150195

151196
**Template pins are manual.** `.github/dependabot.yml` uses `directory: "/"`, which only scans
152197
`.github/workflows/` — nothing will ever bump an action pin inside `templates/`. Check

docs/fleet-operations.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Fleet operations — waves, pilots, and what the audit cannot see
2+
3+
How kit changes actually reach the fleet, and the traps that have bitten. The **authoritative
4+
release sequence is [`README.md`](../README.md)*Release + repin order*** — this document does not
5+
restate it. What lives here is the operational knowledge around it: how a wave is executed, what a
6+
pilot can and cannot prove, and where the drift detector is blind.
7+
8+
Written 2026-08-02 from the v1.7.0 → v1.11.0 waves.
9+
10+
---
11+
12+
## The fleet
13+
14+
**21 repo@branch pairs**, and the split matters because two different numbers are correct depending
15+
on the question:
16+
17+
| Set | Size | What it is |
18+
|---|---|---|
19+
| **Repin-wave targets** | **21** | Every pair carrying any kit caller stub. What `tools/fleet-pin-audit.sh` enumerates, and what a pin-only wave must cover — miss one and `--stale` never reads clean. |
20+
| **Full-kit targets** | **18** | Pairs carrying `claude.yml` *and* `bonsai-status-sync.yml`. Verified branch-by-branch across all 618 org branches: zero rows where one is present without the other, so a wave touching one can touch both. |
21+
| **Difference** | **3** | `Team-Laird@develop`, `The-Gathery@develop`, `driver-bonsai-mcp@main` — stub rails only, neither full workflow. They still need the pin repin. |
22+
23+
Palmers contributes **8** of the 18 (one per country branch: `main`, `-au`, `-ca`, `-in`, `-ma`,
24+
`-me`, `-sa`, `-uk`); the other 10 are single-branch repos including Avara.
25+
26+
**Avara is the only provisioned store repo** — the only pair carrying `shopify-tool-smoke.yml`, and
27+
the only one whose `claude.yml` has a non-empty `SHOPIFY_STORE_NAME`.
28+
29+
---
30+
31+
## Waves are direct pushes, not PRs
32+
33+
Decided at the v1.7.0 wave (2026-07-31) and used for every wave since. A mechanical,
34+
centrally-reviewed kit change is pushed **straight to each branch with `[skip ci]` in the commit
35+
message**, rather than opening 21 PRs.
36+
37+
Why:
38+
39+
- **Zero review runs.** 21 PRs would each fire `pr-first-review` and burn quota on a change that was
40+
already reviewed centrally.
41+
- **Zero theme deploys.** Recon found `develop`/`staging` deploy workflows on ~10 fleet branches that
42+
a bare push *would* have fired. `[skip ci]` suppresses them.
43+
- Branch protection does not enforce for admins (`enforce_admins: false` fleet-wide), so the push
44+
lands as Maria without a review round-trip.
45+
46+
**Reserve PR waves for changes that genuinely want per-repo review.** A kit change that is
47+
byte-identical everywhere does not.
48+
49+
`[skip ci]` suppresses workflow triggers but **not** GitHub's own "Dependabot Updates" scheduler —
50+
seeing one of those fire after a wave is expected and benign.
51+
52+
### Execution shape
53+
54+
One atomic commit per branch via the Git Data API (blobs → tree → commit → ref patch), not one
55+
commit per file. Per target:
56+
57+
1. `claude.yml` ← kit version, with the repo's own `SHOPIFY_STORE_NAME` restored.
58+
2. `bonsai-status-sync.yml` ← kit stub, **whole-file replacement**.
59+
3. The other five stubs ← **sed the pin line only**, so any per-repo edit survives.
60+
4. `shopify-tool-smoke.yml` (Avara only) ← kit version, store handle restored.
61+
5. `actionlint` every file about to be written, then commit `[skip ci]` and patch the ref.
62+
63+
Guards worth keeping in any wave script: assert no destination path is written twice, assert the
64+
store handle survived, assert no stale pin remains, and dry-run the whole fleet before writing
65+
anything.
66+
67+
---
68+
69+
## Three traps
70+
71+
**1. Same basename in both halves of the diff.** When a full workflow becomes a stub, the kit diff
72+
carries `templates/github/<name>.yml` *and* `.github/workflows/<name>.yml`. The wave rewrites
73+
`templates/github/``.github/workflows/`, so both collapse onto one destination. Apply them
74+
blindly and the *reusable* can land in a client repo **as** the workflow — where it is
75+
`workflow_call`-only, fires on nothing, and looks green. Assert no destination is touched twice.
76+
77+
**2. Pin hunks patch from a base the fleet was never on.** At v1.11.0 the kit diff patched from
78+
`80c35fe` (v1.8.0) while every deployed stub held `a54c91e` (v1.9.0) — because v1.9.0 shipped
79+
without a kit repin commit even though the wave repinned the fleet. No kit revision had *ever*
80+
carried `a54c91e` in a pin line, so no diff base produced a matching `-` line and `git apply` would
81+
have rejected all five files on target #1. **Sed the pin; don't patch it.**
82+
83+
**3. Per-repo state that must survive.** `SHOPIFY_STORE_NAME` in `claude.yml` and
84+
`shopify-tool-smoke.yml`, and any Dependabot-bumped action pins. Surveyed at v1.11.0: the fleet's
85+
`claude.yml` copies were byte-identical to the kit except Avara's store handle, and there was no
86+
Dependabot drift — but survey, don't assume.
87+
88+
---
89+
90+
## What the pin audit cannot see
91+
92+
`tools/fleet-pin-audit.sh` greps only
93+
`DriverDigital/workflows/.github/workflows/<name>@<sha>` and compares the SHA to the latest tag.
94+
Three consequences:
95+
96+
- **A file with no `uses:` line is invisible.** An unconverted 190-line copy has none, so the audit
97+
cannot tell a repo that was skipped by a stub conversion from one that never carried the file.
98+
- **Content is never compared.** `DRIVER_AGENTS_REF` is a raw SHA in an `env:` block, and the
99+
system-prompt text is just text. A fleet running kit content from no tag reports clean.
100+
- **The reference itself can drift.** The audit compares against the latest *tag*, never against
101+
`templates/`. When those disagree the audit reports uniform while real drift sits in the source of
102+
truth — which is exactly how the v1.9.0 gap went unnoticed for a day.
103+
104+
This is why the release order requires the tag to contain what gets waved, and why a
105+
`DRIVER_AGENTS_REF` bump must re-run the canonical parity check by hand.
106+
107+
---
108+
109+
## Piloting a cross-repo reusable
110+
111+
The v1.11.0 pilot proved `vars.BONSAI_URL` resolves against the **caller**, so a per-repo tunnel
112+
override still works after conversion. Two things made it harder than expected, both worth knowing
113+
before designing the next one.
114+
115+
**The `issues` leg is not pilotable.** `bonsai-status-sync`'s issues gate greps the issue body for
116+
`@claude`, and `claude.yml`'s issues gate does the same — deliberately mirrored. Any issue that
117+
trips the status flip also wakes a real implementer run on a client repo. Use the PR leg.
118+
119+
**`closingIssuesReferences` only populates for PRs targeting the default branch.** A PR into a
120+
scratch base dodges the theme-deploy workflows (they filter on `branches: [staging, dev-staging]`)
121+
but resolves `uuid=<none>`, so the run never reaches the `curl` and passes green having tested
122+
nothing. If the assertion needs the network call, the PR must target the default branch.
123+
124+
**Split the legs by what each can actually prove.** Leg 1 on a private consumer
125+
(`foundrae-blackridge@staging`) proves a private repo resolves the public cross-repo reusable and
126+
reads the caller's event payload — that is the visibility question. Variable resolution is
127+
repo-agnostic, so leg 2 belongs wherever it is cheapest: `vite-plugin-shopify-clean` is public,
128+
single-branch, and has no Shopify store attached, so nothing but node tests fire.
129+
130+
**Assert on the log line, not the colour.** Setting `BONSAI_URL` to a bogus host and checking for a
131+
red run is not sufficient — a wrong-way resolution falls back to the hardcoded default and *also*
132+
fails. The discriminator is which host the log names:
133+
134+
```
135+
BONSAI_URL: https://pilot-bogus-host.invalid
136+
curl: (6) Could not resolve host: pilot-bogus-host.invalid
137+
```
138+
139+
Clean up afterwards: delete the variable, close the issue and PR, delete the scratch branches. Leave
140+
the installed stub — the wave covers it anyway.
141+
142+
---
143+
144+
## Branch protection
145+
146+
`enforce_admins` is `false` fleet-wide, which is what makes direct-push waves work. Two live kit
147+
branches have **no protection at all**`studio-sulzer@main` and `Team-Laird@develop` (404 on the
148+
protection endpoint). Every other kit branch is protected. The kit's onboarding steps assume a
149+
human-approver rule exists, so on those two a bot signal alone could satisfy a merge.
150+
151+
On this repo, `main` has `required_status_checks` with `strict: true` but empty `contexts` — so
152+
`lint.yml` reports red without being able to block. The context string to add is **`actionlint`**
153+
(the job id at `.github/workflows/lint.yml:28`; the workflow-level `name:` is not part of it).

docs/reusable-conversion-scope.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Scope: convert `claude.yml` + `bonsai-status-sync.yml` into reusable workflows
22

3-
**Status:** `bonsai-status-sync` half IN PROGRESS (Phase 1 done); `claude.yml` half TABLED. **Written:** 2026-07-31 against `main` @ `9b70acf` (tag `v1.6.0` = `0a3934f`).
3+
**Status:** `bonsai-status-sync` half **COMPLETE** (shipped `v1.11.0`, waved 2026-08-02); `claude.yml` half TABLED. **Written:** 2026-07-31 against `main` @ `9b70acf` (tag `v1.6.0` = `0a3934f`).
44
**Refreshed:** 2026-08-02 against `main` @ `a54c91e` (tag `v1.9.0`). Three releases landed underneath the
55
original draft — v1.7.0 (Slack alerting), v1.8.0 (audit context + artifact leg), v1.9.0 (store-secret rename).
66
Every `file:line` citation below was re-verified again on 2026-08-02 against the **v1.11.0** release branch
@@ -345,10 +345,10 @@ so in the PR body. Merge on review of the diff alone; validate after merge.
345345
| Phase | Work | Est. | Status |
346346
|---|---|---|---|
347347
| 1 | Convert `bonsai-status-sync.yml` + stub + docs + lint | 3h | **done 2026-08-02** |
348-
| 2 | Pilot it (only the `pull_request` leg is testable pre-merge — see *Sequencing*) | 2h | **next** |
349-
| 3 | Fleet wave for `bonsai-status-sync` — 18 repo@branch pairs across 11 repos | 3–4h | after Phase 2 |
348+
| 2 | Pilot it (only the `pull_request` leg is testable pre-merge — see *Sequencing*) | 2h | **done 2026-08-02** — passed |
349+
| 3 | Fleet wave for `bonsai-status-sync` — 18 repo@branch pairs across 11 repos | 3–4h | **done 2026-08-02** — waved 21 targets |
350350
| 5 | Tag + repin the kit stubs (README's mandatory 3-step release order) | 1h | **done 2026-08-02** — tag `v1.11.0` + PRs #26/#27 |
351-
| | **Approved subtotal** | **9–10h** | Phases 1 + 5 done → **pilot + wave left** |
351+
| | **Approved subtotal** | **9–10h** | **COMPLETE 2026-08-02** — shipped as `v1.11.0` |
352352
| 0 | Spike: go/no-go on OIDC-in-reusable | 3–4h | **tabled** |
353353
| 4 | Convert `claude.yml` — move the 477 lines **faithfully** | 7–9h | **tabled** |
354354
| 6 | Pilot `claude.yml` with the four assertions incl. pin-vs-HEAD | 4–6h | **tabled** |
@@ -461,6 +461,21 @@ the per-repo escape hatch being the untested path.
461461
**Assertion:** on the pilot repo, set `BONSAI_URL` as a repository variable to a deliberately bogus host and
462462
confirm the run goes **red**. Thirty seconds, and it converts the assumption into evidence.
463463

464+
> **RESOLVED 2026-08-02 — the assumption held.** Run log on `vite-plugin-shopify-clean`:
465+
> `BONSAI_URL: https://pilot-bogus-host.invalid` then `curl: (6) Could not resolve host`, exit 6.
466+
> The variable resolves against the **caller**, so the per-repo tunnel override survives the
467+
> conversion. Leg 1 on `foundrae-blackridge@staging` separately proved a *private* consumer resolves
468+
> the public cross-repo reusable and reads the caller's event payload
469+
> (`event=pull_request action=opened draft=false -> status=Internal Review`).
470+
>
471+
> Two design corrections worth carrying into the `claude.yml` pilot, both recorded in
472+
> [`fleet-operations.md`](fleet-operations.md): the `issues` leg is **not pilotable** (its `@claude`
473+
> grep is mirrored by `claude.yml`'s gate, so tripping it wakes a real implementer on a client repo),
474+
> and `closingIssuesReferences` only populates for PRs targeting the **default** branch — so a
475+
> scratch-base PR dodges the theme deploy but resolves `uuid=<none>` and passes green having tested
476+
> nothing. Assert on which host the log names, not on the run colour: a wrong-way resolution falls
477+
> back to the hardcoded default and fails too.
478+
464479
**Phase 5 moved up.** It was written as "tag + repin" after the `claude.yml` conversion, but the
465480
`bonsai-status-sync` half needs its own tag and repin to be usable at all — the new stub ships with a
466481
placeholder pin. Do it as part of the Phase 3 wave, not after it.

0 commit comments

Comments
 (0)