Single source for Driver's Bonsai→GitHub pipeline reusable workflows and the onboarding kit
(templates/github/ in this repo). Each consuming repo installs a thin caller stub per workflow that pins
an immutable commit SHA; a bot (Dependabot/Renovate) bumps the SHAs as new tags ship. This repo is
public so cross-repo reusable calls resolve from any consuming repo (the org enforces a
selected-actions allowlist at the org/enterprise tier).
- workflows (this repo, public) — the reusable GitHub workflows + this onboarding kit. Fleet repos carry thin SHA-pinned caller stubs; these run the GitHub side (the implementer and the Dependabot rails) once an issue exists.
- driver-agents (private) — the Shopify
admin-tool wrapper and the canonical operator instructions, cloned at the pinned
DRIVER_AGENTS_REF; also the pipeline dispatcher (pipeline-dispatch.yml, scheduled) that triages ready Bonsai tasks, opens those GitHub issues, and polls the Bonsai status back. It was also the box's headlessclaude -pcron runner. - driver-bonsai-mcp — archived 2026-09-11; it held the dispatcher and the Bonsai bridge server until 2026-09-10. Mentions of it below are history.
Flow: a Bonsai ticket assigned to Agents → the dispatcher triages it and opens a GitHub issue →
the target repo's claude.yml implements it and opens the PR → the dispatcher moves the Bonsai
task to Internal Review.
Latest tag v1.16.0 (ff3ff34, 2026-09-10) — every third-party action floats on its major tag,
in the kit and the reusables, and Dependabot proposes majors only; hours after v1.15.0, which made
the issue path unable to end green without a PR. Both waved to all 20 targets the same day — see
v1.16.0 below. The three Dependabot stubs are pinned to ff3ff34.
State of play, open decisions and next steps: docs/HANDOFF.md.
Deployed fleet stubs are repinned by the wave, tools/fleet-wave.sh. Dependabot does bump these
pins in a repo that has a github-actions block, but the wave repins within minutes of every tag so
it rarely gets the chance —
docs/fleet-operations.md. Org Actions secrets
(AGENTS_GH_PAT, CLAUDE_CODE_OAUTH_TOKEN, SHOPIFY_ALERT_WEBHOOK) and cross-repo Actions access
are already in place — no per-repo secret setup.
Tags are human labels + the bot's bump target; the caller stubs pin the SHA. History: v1.0.0 (initial
rail) → v1.0.1 (no-ticket detection fix) → v1.0.2 (dependabot-report bot-actor fix) → v1.1.0 (add
dependabot-keep-current + claude-code-action bump) → v1.1.1 (keep-current fail-loud fix) → v1.2.0
(PR-first house-style review) → v1.3.1 (PR-first: REQUIRE the allow-list naming the inline-comment
poster) → v1.4.0 (PR-first outcome-aware marker) → v1.5.0 (add the ticketed-review reviewer-loop
rail) → v1.5.1 (drop the gh-based author re-check that skipped every real PR) → v1.5.2
(allowed_bots: claude[bot], so the bot-opened round 1 actually reviews) → v1.5.3 (always-latest
resilient Claude Code self-install in the three agent reusables) → v1.5.4 (dependabot-validate:
npm-install fallback for lockfile-less repos + actions/checkout v7) → v1.5.5 (claude-code-action
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 →
v1.11.0 → v1.12.0 → v1.13.0 → v1.14.0 → v1.15.0 → v1.16.0 (all below). v1.3.0 was never tagged.
Every third-party action floats on its major tag, in the kit and the reusables, and Dependabot
proposes majors only. Canaried on vite-plugin-shopify-clean, then waved 2026-09-10 to all 20
targets (19 pushed, the canary already current). Audit the same day: 51 pins at ff3ff344, 88
files matching templates/, 2 drifted — the hand-installed lint.yml copies in driver-agents and
driver-engineering-app, which the wave never reaches (neither carries claude.yml or a stub); a
to-do is filed in each and the rows clear when they land.
- Why. A SHA in
templates/is a pin nothing bumps (Dependabot scans.github/workflows/only), while a fleet repo's Dependabot bumps its deployed copy when the action releases, so the fleet always ran ahead of the kit and each wave rolled it back — foundrae-blackridge #174 tookclaude-code-action1.0.195 → 1.0.210 and the v1.15.0 wave put it back to 1.0.201. Maria's policy (2026-09-10): action refs stay current with no commit anywhere, and a breaking change is fixed after the fact rather than every patch shepherded across 20 branches. - Refs:
actions/checkout@v7,actions/upload-artifact@v7,actions/download-artifact@v8andanthropics/claude-code-action@v1— inclaude.yml,lint.ymlandshopify-tool-smoke.yml, the three Dependabot reusables, the two retired review rails, and this repo's ownlint.yml. A run resolves the tag fresh, so every minor and patch arrives on its own; Dependabot opens a PR only for a new major, and Anthropic re-pointsv1within seconds of each release. - Dependabot, here and in the kit: minor and patch bumps of
actions/*andanthropics/*are ignored (a floating ref already has them, and it closes a reported edge where Dependabot rewrites a major-tag ref into a pinned patch). The kit block names the two owners rather than*, so theDriverDigital/workflowsstub bumps it exists for still flow — the tagged kit file said*; corrected onmainthe same day, before any repo installed it. Here,github-ownedmajors still auto-merge andclaude-code-actionmajors land by hand. - The trade, recorded in
claude.yml:v1is a mutable tag a bot repoints, and the SHA covered one of two channels into a job holding the OAuth token — the CLI install was already unpinned — so a small reduction, not none. Macroscope's Critical onactions/checkout@v7(#55) was assessed and kept, by decision, in its thread. - Docs updated in place:
CLAUDE.md's invariant, README's "Template pins are manual", the audit's normalization note,fleet-operations.md, the kit README,HANDOFF.md's watch-items, and the conversion scope doc, whose "three action pins" secondary win is gone.
The wave that carries #50's issue-path guard, plus the ride-alongs decided 2026-09-10. Canaried on
vite-plugin-shopify-clean, then waved 2026-09-10 to all 20 targets (19 pushed, the canary
already current). Audit the same day: 51 pins at 15a34e98, 90 files matching templates/, zero
drift — the first audit that counts the PR template.
- An issue run can no longer end green without a PR (#50): a guard step fails the job when no
open PR exists from
issue-<n>(Avara #195 pushed its branch and stopped, and nothing noticed).show_full_output: truekeeps the transcript in the masked job log — the log, not an artifact, so registered secrets stay redacted (Macroscope on #50).CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1is set at the step env and through thesettingsenv (claude-code-action#1499: a run that backgrounds subagents exits green with the work orphaned; the step-env form is what three independent reports measured, the settings path is what the CLI reads from disk). claude-code-action→c81e3bc(v1.0.201) in the kit, matching the reusables since #47. foundrae-blackridge@staging had been moved to 1.0.210 by its own Dependabot (#174), not by hand; the wave brought it back to the kit's pin, and when Dependabot bumps it again the audit will read that as the kit being behind.DRIVER_AGENTS_REF→b4ea94e(driver-agentsmain, 2026-09-10) in both kit files. driver-agents PR #7: the guard reports a value-clearing write (valueof"",[]or{}) on the allow path — a stdout line, not a gate — and the canonical blockquote names three exit-3 refusal kinds and adds two paragraphs (an empty value is a delete; theaccess.adminenum). Kit copy re-copied, 2,370 → 3,480 chars, parity verified whitespace-collapsed at the pin.templates/github/dependabot.ymlgains acooldownblock: GitHub's default 3 days stays for third-party actions;DriverDigital/workflows*is exempt, so a repo the wave skipped bumps the day the tag lands.- The wave replaces every kit file whole-file, the three stubs included (the pin-line sed let
per-repo stub edits survive, but the audit reports those as drift and none existed), and
pull_request_template.mdjoins the file set at.github/— the fleet's copies still credited the retired status sync, invisible to the audit until it learned the path. Presence-based as before: replaced where present, never installed. tools/fleet-pin-audit.shselects the newestvX.Y.Zby semver across every tag page and fails closed on a failed page or an empty list (#51 — three Macroscope findings, each fixed).dependabot-reportheaders (reusable and stub) no longer claim a reusable cannot seegithub.event.workflow_run; the explicit inputs exist for the provenance assertion (#51).
The queued DRIVER_AGENTS_REF bump and tripwire re-copy, plus the kit's first Dependabot updater.
Waved 2026-08-22 to 19 of the 20 repin targets — vite-plugin-shopify-clean deliberately
skipped (--skip) to prove Dependabot bumps the stub pins unaided. Audit the same day: 48 pins
at 539d7eab, 68 files matching templates/, and exactly the expected drift — 3 stale pins and 4
content rows, all vite-plugin-shopify-clean@main. fleet-pin-audit.sh --stale reads red by design
until that repo catches up.
DRIVER_AGENTS_REF→919ca79(driver-agentsmain, committed 2026-08-07) in both kit files. Undertools/the diff is one stderr message, jq summary phrases and a comment — no file, mode, env var, argument or exit-code change.- Tripwire blockquote re-copied from canonical, 978 → 2,370 chars: the two exit-3 refusals are
split (allowlist vs argument guard),
menuUpdate/ metaobjectvalues/collectionUpdateruleSetare flagged as replace-not-patch,redirectNewHandleis required on a handle change, and the false "extend the allowlist with one reviewed line" blanket is gone. Parity verified whitespace-collapsed at the pin. templates/github/dependabot.yml— a daily groupedgithub-actionsblock, hand-installed (it lives at.github/and merges into a repo's existing file; the three install cases are in the kit README). The fleet's existing blocks are monthly and a Dependabot PR waits on a human merge, so the wave stays primary —docs/fleet-operations.md.tools/fleet-wave.sh --skip <repo>, repeatable, resolved like--onlyso case or a typo cannot wave the repo it was meant to spare; both flags refuse an empty value. The default commit message no longer names the v1.13.0 retirement.claude.ymlride-alongs: the danglingdocs/phase2-github-setup.mdpointer is gone and a Figma MCP caveat sits beside the WebSearch one above--allowedTools(docs/figma-mcp-in-ci.md).- this repo's own CI: the kit actionlint pass excludes
dependabot.yml(not a workflow).CLAUDE.mdadded (#41).
The pass that sets the rail up to run without the box — see the headline for what and why. Piloted
on vite-plugin-shopify-clean (canary below), then waved 2026-08-22 to all 20 repin targets (19
pushed, the pilot already current). Audit clean the same day: 51 pins at f6d25d34, 72 files
matching templates/, zero drift across the 23 pairs audited.
claude.ymlreads the dispatcher's issue body.Target branch:is the base for bothgh issue develop --baseandgh pr create --base;Reviewer:is requested withgh pr edit --add-reviewerafter the PR exists, non-fatally — a bad handle can no longer abort PR creation;## Instructions from the ticketdirects the task but never overrides the steps, the repo rules or a system-prompt guard, and never authorises touching.github/or exposing credentials. Branch name and handle are opaque data: one quoted argument each, never spliced into a larger command.- In-run pre-review. Push, then
/code-review high <base>...HEAD— the explicit range is load-bearing, a bare invocation reviews commits ahead of upstream, which is nothing once the branch is pushed. Fix, push again, and the short PR body endsPre-review: N findings, M fixed, K dismissed(orPre-review: skipped (reason)). - Quality standard added to the static
--append-system-prompt(9 items: CLAUDE.md/HANDOFF first, design before code, one author per file, adversarial pre-review, verify before done, conventional commits with no trailers, short PR body, repo conventions win, judgment over compliance). It is global — it reaches the ticketed revision rail too, so watch the first revision round against the 90-minute cap. - Attribution off mechanically:
settings: { "attribution": { "commit": "", "pr": "", "sessionUrl": false } }— noCo-Authored-By, no "Generated with" line on the agent rails. A human@claude(tag mode) still gets the action's own co-author text. --model fable --effort xhigh, andclaude-code-action→d40ddef(v1.0.195), matching the reusables.bonsai-status-syncretired — template and reusable both deleted, and the org secretBONSAI_BEARER_TOKENdeleted after a direct probe of every non-archived repo (+ Palmersmain*) found no copy of the workflow left anywhere. The dispatcher polls the two remaining legs instead (issue opened → In Progress; non-draft dev-linked PR → Internal Review). The kit is six.ymlfiles; five reusables remain — three Dependabot rails plus the two retired review rails;ticketed-review.yml's banner now says re-activation needsBONSAI_BEARER_TOKENrecreated.tools/fleet-wave.sh— the wave is a checked-in script now. It discovers targets by presence (claude.ymlor a Dependabot stub, which is what reaches the two stub-only pairs) and repins the pin line's SHA and its# vX.Y.Ztrailer together. Guards: a real wave only from a cleanmaincontaining the tag, kit stubs must pin the latest tag, never waves this repo, actionlint before every write, no destination path twice, store handle survives,--dry-runtouches nothing, zero targets is an error. Mechanics:docs/fleet-operations.md.- this repo's own CI:
lint.ymlnow also fails on a stray brace inclaude.yml'sprompt:scalar — actionlint is blind to it, and a stray{is a silent dispatch failure fleet-wide. DRIVER_AGENTS_REFheld at4d63371, re-checked against canonical at that pin (whitespace-collapsed parity: match). driver-agentsmainis 8 commits ahead with a much longer blockquote and small tool fixes; the queued re-copy + ref bump is indocs/HANDOFF.md.- Design: driver-bonsai-mcp
docs/superpowers/specs/2026-08-21-box-retirement-dispatcher-design.md§5a.
Canary (vite-plugin-shopify-clean, 2026-08-21 23:12–23:17 UTC, torn down afterwards): Bonsai
TSK-00923 → dispatcher run 32536010404 → issue #91 by driver-digital-agents carrying the target
branch, reviewer and ticket instructions → claude.yml run 32536083273, success in 3m24s on
claude-fable-5 → PR #92 with ktdriverdigital requested 4 s after creation, a 10-line body ending
Pre-review: 1 finding, 1 fixed, 0 dismissed, no footer, 0 trailers on 2 commits, and the
ticket's changelog instruction honoured. Timestamps prove the order: feat commit 23:15:12 →
pre-review fix 23:16:41 → PR 23:16:49. Dispatcher reconcile run 32536361182 moved Bonsai to
Internal Review.
The review-rail retirement: PR review became Macroscope's job alone (decided 2026-08-08) — the two
review stubs left the kit, their reusables stayed here caller-less with retirement banners, and
bonsai-status-sync lost its review leg (formal review → Revisions Requested / Ready for QA); the
Dependabot rails, claude.yml and the two remaining status legs were untouched. Context and the
Macroscope→Bonsai build plan:
docs/macroscope-integration-scope.md. Waved 2026-08-08 to
all 23 pairs: the two review stubs deleted from every fleet pair that carried them (41 pin rows
removed), bonsai-status-sync.yml whole-file replaced on 18 pairs, remaining stubs repinned.
Audit clean same day: 69 pins at b1fcb78c, 90 files matching templates/, zero drift. Three
pairs' only pinned stub was pr-first-review.yml — driver-agents@main, driver-engineering-app@main,
driver-bonsai-mcp@main — so they now carry no pins at all (still content-checked) and the
repin-target count lands at 20 (see docs/fleet-operations.md).
- Review rails retired, not removed. Stubs deleted (kit + fleet); reusables preserved inert
with retirement banners carrying the re-activation path (restore stubs from git history, tag,
wave). Deletion over
gh workflow disablebecause disabled-state is invisible to the pin audit — the fleet's files should say what actually runs. bonsai-status-syncreview leg removed — the stub'spull_request_reviewtrigger AND the reusable's handler, deliberately not actor-gated: Macroscope's bot behavior (formal reviews vs. comments) is unobserved as of this tag, and if it submits formal reviews the old mapping would flip statuses with the wrong semantics (a bot approval is not "Ready for QA"). Statuses past Internal Review are a PM's job until the Macroscope→Bonsai integration lands.claude.ymldeliberately untouched, including the ticketed-loop machinery (round-marker prompt branch, actor carve-out, re-request step). It looks dead withticketed-reviewgone; it is the designed re-entry point for the Macroscope-driven revise loop — do not strip it in a future wave.DRIVER_AGENTS_REFdid not move. The queued canonical-blockquote re-copy stays pending for the nextclaude.yml-touching release — deliberately not folded in here, so this wave changes review behavior and nothing else.
Waved to all 21 pairs on 2026-08-02; fleet uniform, 108 pins, zero stale.
bonsai-status-sync.ymlconversion completed. The 190-line per-repo copy became a 67-line caller stub — the status machine, actor gate, linkage logic and cascade caveat now live in one central file. The reusable itself landed one tag earlier (seev1.10.0), so the conversion spans the two tags: a new reusable's stub cannot be pinned until the tag containing it exists.- kit
claude.yml+shopify-tool-smoke.yml:DRIVER_AGENTS_REF→4d63371. The previous pin0bbb125predatedgraphql_guard.py, so every fleet runner executedadmin-graphql.shwith no fail-closed allowlist and the Driver Engineering scope grant was the only control on destructive mutations. - Shopify operator tripwire appended to
claude.yml's static--append-system-prompt, pairing with that wrapper. The blockquote is copied verbatim from driver-agentsdocs/agent-instructions-shopify.md(canonical — edit there first); a non-canonical kit-side lead-in precedes it, un-scoping the block from the conduct rules above and telling the model how to report a trip on a rail that cannot set a job exit code. - this repo's own CI:
lint.ymlgained a tokenization guard assertingclaude_argsholds exactly four single quotes and the system prompt contains no apostrophe or$. One apostrophe typed into canonical upstream silently truncates the prompt —shell-quotedoes not throw, every flag still parses, and the wave would copy the truncated prompt fleet-wide green. - Piloted before the wave:
vars.BONSAI_URLproven to resolve against the caller, so the per-repo tunnel override survives the conversion. Seedocs/fleet-operations.md.
- New sixth reusable:
.github/workflows/bonsai-status-sync.yml. Itsjobs:body is byte-identical to the old per-repo copy except one added comment. Deliberately shipped without its caller stub — seev1.11.0. lint.ymlplaceholder-pin guard: fails the build on any kit stub still carrying an all-zero pin, so a stub that cannot resolve can never reachmain.- Never waved on its own; superseded by
v1.11.0two commits later.
- Store app secrets renamed
DRIVER_AGENTS_SCOPES_CLIENT_ID/_SECRET→DRIVER_ENGINEERING_APP_CLIENT_ID/_SECRET, tied to the per-org "Driver Engineering" app that replaced "Driver Agents Scopes" (retired 2026-08-01). Waved to all 21 pairs; Avara's smoke test green on the new names, old-name secrets deleted. - Reusables unchanged. Note this release never got its kit repin commit —
templates/sat atv1.8.0's SHA while the deployed fleet was waved toa54c91e, leaving the fleet a release ahead of the kit templates untilv1.11.0closed it. That gap was invisible totools/fleet-pin-audit.shat the time; its reference check now catches exactly this shape — seedocs/fleet-operations.md.
- kit
claude.yml+shopify-tool-smoke.yml:DRIVER_AGENTS_REF→0bbb125(the audit pipeline:SHOPIFY_AUDIT_CONTEXTon the tool, plus the box's nightlyaudit-publish.sh— design at driver-agentsdocs/audit-data-model.md). Lockstep as always. - Audit context export: the provisioning step now builds
SHOPIFY_AUDIT_CONTEXT(ticket/issue/run/host) so every tool call's audit line says on whose behalf it ran. The Bonsai task uuid is resolved from the ISSUE body (uuid=param — the same linkagebonsai-status-sync.ymlgreps), with aclosingIssuesReferencesfallback for the @claude-on-a-PR rails; a human's ad-hoc@claudehas no ticket, correctly. - Audit artifact upload: a final
if: always()actions/upload-artifactstep (pinnedv7.0.1) ships the runner's throwaway audit log to the box's nightly collector —github.run_attemptin the artifact name dodges the immutable-artifact collision on re-runs. Skips cleanly on repos without store tooling. Same step on the smoke test. - reusables: unchanged; stubs repinned to the new tag SHA for the pin-audit invariant.
- kit
claude.yml+shopify-tool-smoke.yml:DRIVER_AGENTS_REF→0404c4e(driver-agents main @ 2026-07-31) — picks up the human-readable Slack alert wording (driver-agents PR #2). The pin moves in both files together, per the lockstep rule. - CI alerting leg: the same provisioning step now writes the org-level
SHOPIFY_ALERT_WEBHOOKsecret (the#driver-agents-statusincoming webhook) to the runner's throwaway disk and exportsSHOPIFY_ALERT_WEBHOOK_FILE+SHOPIFY_ALERT_HOST_LABEL, so the admin tool's destructive/failed- call alerts post from Actions runs exactly as they did from the box. The alert's "where to look" label is the run URL — the runner's audit log doesn't outlive the job. Absent secret = alerts silently off, nothing else changes (the tool's own best-effort posture). - reusables: unchanged (byte-identical to
v1.6.0's). The stubs are repinned to3966041regardless, purely sofleet-pin-audit.sh's latest-tag comparison stays meaningful.
- reusables:
actions/checkout→v7.0.1,claude-code-action→v1.0.183; each Claude Code self-install attempt is now bounded bytimeout— a stalled download used to hang one attempt until the job's wall-clock cap while the retry loop never advanced. - kit
claude.yml: human@claudecomments always get tag mode (never prompt-hijacked — foundrae-blackridge PR #148); the ticketed round-marker branch is author-gated ondriver-digital-agents+ id261291955; optional self-skipping Shopify admin tool provisioning — pinned to a revieweddriver-agentsrevision, verified before any credential is written, and gated off the read-only/code-reviewrail (Avara PR #161); the same bounded self-install;actions/checkout→v7.0.1andclaude-code-action→v1.0.183. - new kit file
shopify-tool-smoke.yml(store repos only): a manual diagnostic for the Shopify admin-tool wiring, upstreamed from Avara PR #161 so it is maintained here rather than reinvented per repo. It duplicatesclaude.yml's provisioning step by design — same wiring, loud failures instead of degrade — so the two must be kept in lockstep. - this repo's own CI: new
lint.ymlruns actionlint — plus shellcheck over everyrun:block — across the reusables and the kit, so a broken workflow can no longer reach consumer repos.
Release + repin order (don't skip a step — a wave is only safe once all three are done):
- Merge to
main, then cut the new tag.- If the release moves
DRIVER_AGENTS_REF, re-run the tripwire parity check first: extract the>lines from driver-agentsdocs/agent-instructions-shopify.mdat the new pin, strip the>prefixes, NFC-normalize, collapse whitespace, and diff against the blockquote portion ofclaude.yml's--append-system-prompt. The whitespace collapse is mandatory — the kit flattens canonical's paragraph break to a single space (forced by the no-newline constraint), so a strict byte compare reports a false failure. Nothing else re-checks this.fleet-pin-audit.shproves the fleet matchestemplates/github/claude.yml— it cannot prove that file's blockquote still matches canonical at the new pin, andDRIVER_AGENTS_REFis a raw SHA in an env var that no bot can bump. The audit catches a fleet that fell behindtemplates/; only this step catchestemplates/falling behind driver-agents.
- If the release moves
- Repin every caller stub in
templates/github/to that tag's SHA and its# vX.Y.Ztrailer, then commit. Until this lands, the kit's stubs still point at the PREVIOUS tag's reusables.- If the release ADDS a reusable, its stub lands in this step, not in the PR that added the
reusable — the tag it must pin does not exist until step 1. That is why
dependabot-keep-current's reusable and its stub landed in different commits, and howbonsai-status-sync.yml's stub landed atv1.11.0.lint.ymlfails the build on any stub left carrying a placeholder pin, so this step cannot be silently skipped.
- If the release ADDS a reusable, its stub lands in this step, not in the PR that added the
reusable — the tag it must pin does not exist until step 1. That is why
- Only then re-copy
templates/github/into consumer repos (tools/fleet-pin-audit.sh --staleto confirm the fleet converged afterwards — it now checks waved file content againsttemplates/, not just the pin line, and exits non-zero on any drift, so a wave can gate on it).- The wave is now a checked-in script:
tools/fleet-wave.sh --dry-runfirst, then without. - Dependabot also bumps the stub pins in any repo with a
github-actionsblock (the kit now ships one,templates/github/dependabot.yml, for the repos that had none) — on its schedule and through a PR a human merges, so the wave stays the primary path and Dependabot the backstop.--skip <repo>leaves every branch of a repo to it on purpose — only sound where Dependabot covers each kit branch (it scans the default branch unless atarget-branchentry exists, so not Palmers as configured). Seedocs/fleet-operations.md. - When a full workflow becomes a stub (as
bonsai-status-sync.ymldid — this applies to the v1.11.0 wave specifically), the wave diff contains atemplates/github/path AND a.github/workflows/path with the SAME basename. The wave script rewritestemplates/github/→.github/workflows/, so assert the rewritten diff touches no destination path twice before applying — otherwise the reusable can land in a client repo as the workflow, where it isworkflow_call-only, fires on nothing, and looks green. - Sed pin lines; never
git applythem. A pin hunk patches from whatever SHA the kit held, which is not necessarily what the fleet holds — at v1.11.0 the kit diff patched from80c35fe(v1.8.0) while every deployed stub helda54c91e(v1.9.0), a SHA no kit revision had ever carried in a pin line, so no diff base produced a matching-line andgit applywould have rejected all five files on target #1. The sed rewrites the SHA and the# vX.Y.Ztrailer in one substitution (asfleet-wave.shdoes) — the two halves are one pin, and a stale comment names the wrong release. - Wave mechanics, the guards worth keeping, and what the pin audit cannot see:
docs/fleet-operations.md.
- The wave is now a checked-in script:
Template pins are manual — so the kit's third-party actions float. .github/dependabot.yml uses
directory: "/", which only scans .github/workflows/ — nothing will ever bump a pin inside
templates/, while a fleet repo running the github-actions updater bumps its deployed copy when the
action itself releases, so a SHA there guaranteed the fleet ran ahead of the kit and each wave rolled
it back (foundrae-blackridge #174). The three whole-file kit workflows therefore reference
actions/checkout@v7, actions/upload-artifact@v7 and anthropics/claude-code-action@v1, which
Dependabot leaves alone until a new major exists. The reusables float the same way, so the only action
bump that ever gets a PR — here or in a fleet repo — is a major. DRIVER_AGENTS_REF stays manual — it appears in two
kit files, claude.yml and shopify-tool-smoke.yml, and must carry the same pin in both or the
smoke test verifies a revision the implementer never runs — and to the VERSION + SHA256 pair in
lint.yml, which must be bumped together or the checksum check fails the job.
Onboarding a new repo: copy the matching stubs from this repo's templates/github/ into the
repo's .github/workflows/, run a test PR (human + Dependabot), then pin the required check
validate / validate + add a human-approver rule (see First-run / required-check below). Caller stubs
MUST carry their own permissions: block (a repo whose default workflow token is read-only otherwise
produces a silent startup_failure — no check run, no notification).
Reusable (.github/workflows/) |
Privilege | Trigger (in the caller) | Job |
|---|---|---|---|
dependabot-validate.yml |
none (credential-less) | pull_request |
mechanical install/build/test (+ optional theme/dev-smoke) → upload artifact |
dependabot-report.yml |
secrets (PAT + OAuth) | workflow_run |
reason over the inert artifact → verdict comment + request a human reviewer |
dependabot-keep-current.yml |
PAT only | pull_request (closed) |
rebase out-of-date Dependabot PRs on strict (require-up-to-date) repos; inert elsewhere |
Two more reusables sit in .github/workflows/ retired (v1.12.0, 2026-08-08): pr-first-review.yml
(human no-ticket PR → /code-review + reviewer request) and ticketed-review.yml (claude[bot] ticketed
PR → capped revise loop → Bonsai reviewer handoff). Macroscope reviews all PRs now; both files are
workflow_call-only with no callers anywhere, preserved for re-activation — see the banner in each
and docs/macroscope-integration-scope.md.
The onboarding kit lives here: templates/github/ (moved from driver-bonsai-mcp 2026-07-15). It
carries the three caller stubs above plus claude.yml (the implementer, still a full per-repo workflow),
shopify-tool-smoke.yml (store repos only), lint.yml (actionlint over the installing repo's own
workflows), pull_request_template.md (waved since v1.15.0) and dependabot.yml (the github-actions updater that bumps
the stub pins between waves — installed by hand, merged into an existing file).
Not every repo takes the whole kit. A repo that is not on the Bonsai → PR pipeline can install
lint.yml alone and skip the rest as inert weight.
driver-agents and
driver-engineering-app run that subset (they took
pr-first-review.yml + lint.yml on 2026-08-02; the review stub was deleted in the v1.12.0
retirement wave). The trade-off is written up in templates/github/README.md under Partial install.
A kit-only addition does not need a tag or a wave. lint.yml shipped without either, and that was
correct: it changes no reusable, repins no stub, and carries no uses: DriverDigital/workflows@<sha> of
its own, so nothing deployed had to move. Note the direction of the obligation — cutting the tag is what
creates the wave, because the moment the latest tag moves the audit's reference check fires
against every stub in templates/ and they must be repinned and re-copied everywhere. Let a kit-only file
ride along with the next release that actually changes a reusable.
claude.yml stays a per-repo copy — converting it to a reusable is tabled pending the OIDC spike (whether
Claude App token minting survives inside a cross-repo reusable; scope in
docs/reusable-conversion-scope.md), so it remains the kit's main drift
surface and the reason re-copies still need care.
Two files in .github/workflows/ are this repo's own CI, not products — they are workflow_call-free
and never ship to the fleet: lint.yml (actionlint + shellcheck over the reusables and the kit, so a
broken workflow can't reach consumer repos) and dependabot-auto-merge.yml (auto-merges this repo's own
github-owned Dependabot bumps; the claude-code-action group is deliberately excluded, so those land by
hand). Both are quiet below a major: every third-party action here and in the kit floats on its major
tag, so a run already has each minor and patch, and .github/dependabot.yml ignores everything but majors.
actionlint is a required status check on main (set 2026-08-02) — before that, lint.yml could
report red without being able to block. Note the name collision: this repo's own lint.yml and the kit's
templates/github/lint.yml are different files. The kit one runs actionlint over the installing repo's
.github/workflows/ and nothing else; this one additionally lints templates/github/, gates on placeholder
pins, and asserts claude.yml's system prompt still tokenizes. Both use the job id actionlint, so the
required-check context string is the same either way. enforce_admins stays false here, deliberately
— which means an admin can still merge past a red actionlint. Requiring the check makes it binding for
everyone else and puts a red X in front of an admin who previously had nothing to override; that was worth
having on its own. Flipping the flag would break this repo's own release habit — six commits on main,
v1.9.0's included, were pushed directly with no PR. Detail in
docs/fleet-operations.md.
claude[bot]— the implementer (Phase 2claude.yml), distinct from any reviewer.driver-digital-agents(theAGENTS_GH_PATfine-grained PAT) — the agent-rail actor:GH_TOKENon every agent-actingghstep (the defaultGITHUB_TOKENis used only where a post must not cascade —claude.yml's failed-run notice),dependabot-report's comment/reviewer-request identity, and the author ofclaude.yml's sentinel comments. It was the reviewer on the retired review rails.- Anthropic billing —
CLAUDE_CODE_OAUTH_TOKEN(Max). Never setanthropic_api_key(it overrides OAuth and bills at API rates).
All three secrets must be org-level Actions secrets available to each consuming repo.
dependabot-validate runs untrusted Dependabot code (install + PR-modifiable build) but holds no
secrets — three independent layers: Dependabot forces a read-only token + no Actions secrets; the caller
stub passes no secrets:; this reusable declares no workflow_call.secrets. dependabot-report holds the
secrets but never checks out PR head and never runs PR code — its agent reads ONLY the inert artifact
(build.log/result.json, never interpolated into a run: line) with a read/write-file-only tool surface
(no gh), after a provenance assertion (event pull_request, actor dependabot[bot], non-empty PR
number, same-repo head). Never use pull_request_target.
Install the matching stubs from this repo's templates/github/ into a repo's .github/workflows/.
Pin every uses: of this repo's reusables to an immutable commit SHA (decided 2026-06-17); a bot (Renovate/Dependabot) bumps the
SHAs. The dependabot-validate stub's name: MUST stay byte-identical (Dependabot validate) across all
repos — the dependabot-report stub's workflow_run trigger name-matches it exactly, and a drift silently
disables the human-ping.
{
"install": "npm ci",
"build": "npm run build",
"test": "npm test",
"themeCheck": "npx @shopify/cli theme check --fail-level error",
"dev": "npm run dev",
"devTimeoutSeconds": 90,
"devReadyRegex": "compiled|ready|built in|Local:"
}All keys optional. Defaults: package manager from the lockfile (npm is the house default), build/test
run only if those package.json scripts exist, themeCheck/dev run only if configured.
Retired with the review rails at v1.12.0 — nothing here reassigns a Bonsai task or requests a reviewer
on review completion any more; the status moves are a PM's job until the Macroscope→Bonsai
integration lands. Since v1.13.0 claude.yml requests the GitHub reviewer named on the issue body's
Reviewer: line when it opens the PR, and since 2026-09-11 the dispatcher (driver-agents) assigns the
reviewer in Bonsai when the PR reaches Internal Review.
The bridge server that carried /tasks/reviewer-handoff is retired too; what replaces it for that
build — the Bonsai public API, and the reviewer read off the issue body instead of the Reviewer
field — is in docs/macroscope-integration-scope.md.
dependabot-report still requests a human reviewer on Dependabot PRs (default mcarter-astronautdev,
per-repo override via the PR_REVIEWER_HANDLE Actions variable). The live reviewer map is
driver-agents/config/reviewers.json, read by the dispatcher to write the issue's
**Reviewer:** line; this repo no longer carries a copy.
The dependabot-validate job always runs and branches internally (non-Dependabot PRs no-op green), and
must never be if:-skipped. The mechanism is worth stating precisely, because the intuitive version is
wrong: GitHub does accept a check run whose conclusion is skipped. The problem is that if:-skipping
the caller job means the reusable never starts, so the nested validate / validate context is never
created at all — and a required context with no check run for the head commit blocks forever. Reason
about whether a check run exists for the head SHA, not about the word "skipped".
After the first run on a test PR: pin the exact required-check context GitHub reports — for a
reusable-workflow job it is <caller-job-id> / <reusable-job-id>, expected validate / validate (the
workflow display name is NOT part of the context; copy the literal string from the first run's checks list).
Require a human approver (e.g. CODEOWNERS) so no bot signal satisfies the merge gate.