Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,35 @@ jobs:
fi
echo "all kit stubs carry a real SHA pin"

- name: claude.yml system prompt survives tokenization
# The --append-system-prompt value (conduct block + Shopify operator tripwire) is delimited
# by SINGLE QUOTES for claude_args' shell-quote tokenizer. shell-quote does NOT throw on an
# unterminated quote — it silently truncates, dropping the tail of the prompt. Every flag
# still parses, the YAML stays valid, actionlint exits 0, and the wave copies the truncated
# prompt to 18 branches green. One apostrophe typed into the canonical blockquote upstream
# is enough. So assert the quote count directly: 4 is the correct total (allowedTools pair
# + append-system-prompt pair). Any odd count means something ate a delimiter.
run: |
set -euo pipefail
python3 - <<'PY'
import sys, yaml, re
d = yaml.safe_load(open("templates/github/claude.yml"))
steps = d["jobs"]["claude"]["steps"]
args = [s for s in steps if "claude_args" in (s.get("with") or {})][0]["with"]["claude_args"]
q = args.count("'")
if q != 4:
print(f"::error::claude_args holds {q} single quotes, expected 4. An apostrophe in the "
f"system prompt truncates it silently — shell-quote does not error. Remove it.")
sys.exit(1)
m = re.search(r"--append-system-prompt '(.*?)'(?:\s|$)", args, re.S)
if not m:
print("::error::could not locate the --append-system-prompt value in claude_args")
sys.exit(1)
p = m.group(1)
for ch, why in (("'", "truncates the token"), ("$", "shell-expands")):
if ch in p:
print(f"::error::system prompt contains {ch!r}, which {why}")
sys.exit(1)
print(f"claude_args tokenizes cleanly: 4 quotes, {len(p)}-char system prompt")
PY

13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ reusables have been byte-identical since `v1.6.0`'s `0a3934f`.
`graphql_guard.py` does not exist at that SHA, so every fleet runner executes `admin-graphql.sh`
with **no fail-closed allowlist** and the Driver Engineering scope grant is the only control on
destructive mutations. This is a `claude.yml` change, which stays a per-repo copy — so **no client
repo is guarded in CI until the next wave copies it out**.
repo is guarded in CI until the next wave copies it out**. The same release appends the Shopify
**operator tripwire** to `claude.yml`'s static `--append-system-prompt` (the blockquote is copied from
driver-agents `docs/agent-instructions-shopify.md`, which is canonical — edit there first, and preserve
the kit-side scope lead-in that precedes it), so the fleet gets the wrapper and its instruction block in
one wave.

**Decided 2026-08-02: this ships as `v1.11.0`, not folded into the v1.10.0 wave.** Folding it in would
put `claude.yml` content on 18 branches that exists in no tag, and `tools/fleet-pin-audit.sh` compares
Expand Down Expand Up @@ -113,6 +117,13 @@ npm-install fallback for lockfile-less repos + `actions/checkout` v7) → `v1.5.
**Release + repin order (don't skip a step — a wave is only safe once all three are done):**

1. 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-agents `docs/agent-instructions-shopify.md` at the new pin, strip the `> `
prefixes, NFC-normalize, **collapse whitespace**, and diff against the blockquote portion of
`claude.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.sh` greps only
stub pin lines, and `DRIVER_AGENTS_REF` is a raw SHA in an env var that no bot can bump.
2. Repin every caller stub in `templates/github/` to that tag's SHA and 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
Expand Down
15 changes: 9 additions & 6 deletions docs/identity-unification-scope.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Scope: drop the Claude App, unify the GitHub surface on `driver-digital-agents`

**Status:** scoped, not started. **Written:** 2026-07-31 against `main` @ `9b70acf` (tag `v1.6.0`).
**Refreshed:** 2026-08-02 against `main` @ `a54c91e` (tag `v1.9.0`). Line references re-verified and now
**Refreshed:** 2026-08-02 against `main` @ `a54c91e` (tag `v1.9.0`), then re-verified again the same day
against the **v1.11.0** release branch (`claude.yml` 460 → 477 lines). Line references re-verified and now
**path-qualified** — several filenames exist in both `templates/github/` and `.github/workflows/` with
different content and lengths, and the original draft cited both under one bare name. **Six corrections change
what someone would build** — they are listed in *Provenance* and marked **Correction** where they appear. The
Expand Down Expand Up @@ -251,8 +252,9 @@ first time it appears is the first time it can disagree with the workflow gate,
> the first time — a repo can sit with a new `claude.yml` and an old pinned reusable, which means the
> implementer runs while the Bonsai task never leaves its prior status, green everywhere. Sequence both into
> one wave, and re-verify these line numbers before starting.
*(The five numeric references above were re-verified unchanged at `a54c91e`: every edit to `claude.yml` since
v1.6.0 landed at line 142 or later, leaving the whole actor-gate and trigger region untouched.)*
*(The five numeric references above were re-verified unchanged at `a54c91e`, and again on the v1.11.0 release
branch: every edit to `claude.yml` since v1.6.0 landed at line 142 or later — the v1.11.0 pair land at `:183`
(`DRIVER_AGENTS_REF`) and `:403` (the tripwire) — leaving the whole actor-gate and trigger region untouched.)*

**Therefore: identity and phrase are separable, and should be separate waves.** Swapping the token is a
zero-UX-change move. Flipping the phrase is a coordinated one-literal cutover including a repo this scope
Expand Down Expand Up @@ -380,7 +382,7 @@ echoing a human's request (which contains the trigger phrase) re-enters the gate
> two apart at `:30-31` — *"the sentinel token is DISTINCT from the round marker, so it never inflates the
> count"* — and the sentinel is never *matched* in the reusable, appearing there only as a header comment
> at `:14`; the live matches are `templates/github/ticketed-review.yml:40` (mentioned again at `:5`) and
> `templates/github/claude.yml:450`. Collapsing the two in the scope doc invites collapsing them in the
> `templates/github/claude.yml:462`. Collapsing the two in the scope doc invites collapsing them in the
> implementation, which is the one thing that design forbids.
>
> **The missed site:** the `select(.user.login=="driver-digital-agents")` hardcode at `:137` has a **twin at
Expand Down Expand Up @@ -488,7 +490,7 @@ independently without touching the other 20.
job status. This is the assertion that covers risk 4 (`Contents: read` on a private repo), and leg 2 is
the only leg that exercises it.
10. **Store provisioning succeeds** — on a store repo, `SHOPIFY_STORE_NAME` is non-empty, the env file is
written (`:238`), and the audit artifact uploads under a name containing the store handle (`:463`).
written (`:238`), and the audit artifact uploads under a name containing the store handle (`:475`).
*False pass:* the same silent self-skip — the missing-secret early-exit at `:200-202` is deliberate
degrade-quietly behaviour, and an artifact named `shopify-audit--<run_id>-…` uploads perfectly happily.

Expand Down Expand Up @@ -528,7 +530,8 @@ five findings are silent-failure class and are treated as requirements above. So
`anthropics/claude-code-action` at pinned SHA `be7b93b1907a4abad570368f3c74b6fe3807510b`, live `gh api` queries
against the DriverDigital org, and this repo's own files.

**Refreshed 2026-08-02** against `main` @ `a54c91e` (v1.9.0), with the rate-limit section re-derived from live
**Refreshed 2026-08-02** against `main` @ `a54c91e` (v1.9.0) and re-verified against the v1.11.0 release
branch, with the rate-limit section re-derived from live
org data and current GitHub documentation. **The recommendation is unchanged and the headline finding
survives** (58.0–61.2%). Six corrections changed what someone would build — the self-authored guard, the
dual-accept id pairing, the measurement instrument, the loop invariant's scope, the second round-counter site,
Expand Down
28 changes: 16 additions & 12 deletions docs/reusable-conversion-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
**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`).
**Refreshed:** 2026-08-02 against `main` @ `a54c91e` (tag `v1.9.0`). Three releases landed underneath the
original draft — v1.7.0 (Slack alerting), v1.8.0 (audit context + artifact leg), v1.9.0 (store-secret rename).
Every `file:line` citation below is re-verified against `a54c91e`; every number is recomputed. Citations are
Every `file:line` citation below was re-verified again on 2026-08-02 against the **v1.11.0** release branch
(`templates/github/claude.yml` is 477 lines there, not the 460 it was at `a54c91e`); every number is
recomputed. Citations are
now **path-qualified**, because several filenames exist in both `templates/github/` (short caller stubs) and
`.github/workflows/` (long reusables) with entirely different content — the original draft cited both under
one bare name.
Expand Down Expand Up @@ -33,12 +35,12 @@ The README has flagged this as "future work" since the repo split. This document
## Why

*As of the Phase 1 conversion (2026-08-02) this argument is half-resolved: `bonsai-status-sync.yml` now has a
central reusable, and the kit copy dropped from 190 lines to a 67-line stub at the v1.10.0 repin. **`claude.yml`'s 465
central reusable, and the kit copy dropped from 190 lines to a 67-line stub at the v1.10.0 repin. **`claude.yml`'s 477
lines remain copied** — still the single largest thing in the kit, and the drift surface that outlives this
conversion. The original framing follows.*

Of the 916-line kit, **650 lines (71%) were the two files copied verbatim rather than called** —
`claude.yml` (465) and `bonsai-status-sync.yml` (190). The five caller stubs totalled 154 lines and are
`claude.yml` (460) and `bonsai-status-sync.yml` (190). The five caller stubs totalled 154 lines and are
mechanical.

*At v1.6.0 this read 594 of 837 — the same 71%. The flat ratio hides the trend: over three releases the
Expand All @@ -64,7 +66,7 @@ file (open decision 1). What conversion removes is the 650 lines of *logic* that
subset of — which is the specific failure that produced the Avara incident.

Secondary win: `actions/checkout` (`templates/github/claude.yml:130`), `claude-code-action` (`:309`) and
`actions/upload-artifact` (`:461`, added by v1.8.0) move out of `templates/` and into `.github/workflows/`,
`actions/upload-artifact` (`:473`, added by v1.8.0) move out of `templates/` and into `.github/workflows/`,
which `.github/dependabot.yml` (`directory: "/"`) actually scans — converting **three** documented manual pins
into bot-managed ones.

Expand Down Expand Up @@ -195,7 +197,7 @@ to degrade quietly when unset.

> **Correction (2026-08-02).** `SHOPIFY_STORE_NAME` now has a **second consumer**. At v1.6.0 it was read
> only by the provisioning script (`:200`, `:207-211`, `:238`). v1.8.0 also interpolates it into the audit
> **artifact name** at `templates/github/claude.yml:463`. The `inputs.shopify_store_name` value must be
> **artifact name** at `templates/github/claude.yml:475`. The `inputs.shopify_store_name` value must be
> threaded to **both** sites — wiring only the provisioning step leaves the artifact named
> `shopify-audit--<run_id>-<attempt>`, which uploads successfully and is therefore another silent failure.

Expand Down Expand Up @@ -287,7 +289,7 @@ declares both secrets at `.github/workflows/pr-first-review.yml:42-44`. The cons

### The v1.8.0 artifact leg — new since the original draft

v1.8.0 added an audit-artifact upload (`templates/github/claude.yml:459-465`, mirrored at
v1.8.0 added an audit-artifact upload (`templates/github/claude.yml:471-477`, mirrored at
`templates/github/shopify-tool-smoke.yml:106-112`). The step itself moves into a reusable unchanged —
`always()`, `env.*` read from `$GITHUB_ENV`, and `upload-artifact`'s own `ACTIONS_RUNTIME_TOKEN` auth are all
unaffected by `workflow_call`. Two things do change:
Expand All @@ -296,7 +298,7 @@ unaffected by `workflow_call`. Two things do change:
- **A called workflow does not get its own run id.** `github.run_id` and `github.run_attempt` resolve to the
**caller's** run. That is the *desirable* outcome for the collector — the artifact lands in the consuming
repo's run, where the box's nightly `audit-publish.sh` already looks. But it degrades the collision guard
the file calls load-bearing at `:454-457`: `run_id` + `run_attempt` no longer disambiguate *jobs within one
the file calls load-bearing at `:466-469`: `run_id` + `run_attempt` no longer disambiguate *jobs within one
run*. What makes that safe today is simply that `claude.yml` declares **exactly one job** (`jobs.claude`,
`:65-66`) — not the concurrency group at `:61-63`, which serializes *runs* within a group and says nothing
about jobs inside a run. Conversion removes that structural guarantee: **call the reusable from two jobs in
Expand Down Expand Up @@ -347,7 +349,7 @@ so in the PR body. Merge on review of the diff alone; validate after merge.
| 5 | Tag + repin the kit stubs (README's mandatory 3-step release order) | 1h | **done 2026-08-02** — tag `v1.10.0` + PR #26 |
| | **Approved subtotal** | **9–10h** | Phases 1 + 5 done → **pilot + wave left** |
| 0 | Spike: go/no-go on OIDC-in-reusable | 3–4h | **tabled** |
| 4 | Convert `claude.yml` — move the 465 lines **faithfully** | 7–9h | **tabled** |
| 4 | Convert `claude.yml` — move the 477 lines **faithfully** | 7–9h | **tabled** |
| 6 | Pilot `claude.yml` with the four assertions incl. pin-vs-HEAD | 4–6h | **tabled** |
| 7 | Fleet wave for `claude.yml`, same 18 pairs (10 single-branch repos incl. Avara → Palmers ×8) | 4–5h | **tabled** |
| 8 | Optional: convert `shopify-tool-smoke.yml` | 2–3h | **tabled** |
Expand Down Expand Up @@ -465,11 +467,11 @@ placeholder pin. Do it as part of the Phase 3 wave, not after it.
**If identity unification ships first, Phase 0 disappears and the total is 26–33h.** And Phases 1–3 (8–11h)
depend on neither Phase 0 nor the identity decision — that portion is startable now.

*Estimates grew on the 2026-08-02 refresh: Phase 4's payload is 465 lines rather than 404 (6–8h → 7–9h) and
*Estimates grew on the 2026-08-02 refresh: Phase 4's payload is 477 lines rather than 404 (6–8h → 7–9h) and
Phase 8's `shopify-tool-smoke.yml` went 89 → 112 lines (2h → 2–3h). The v1.6.0 table also stated 28–36h while
its own max column summed to 35.*

Phase 4 note: **63%** of `claude.yml` is comments (296 of 465 lines — it was 67% at v1.6.0), and they are the
Phase 4 note: **64%** of `claude.yml` is comments (308 of 477 lines — it was 67% at v1.6.0), and they are the
institutional memory — the 2026-06-19 actor-gate incident, the `persist-credentials` 403 on private repos, the
foundrae #148 prompt-hijack, the Avara #143 install blip. Budget for moving them faithfully, not cut-and-paste.

Expand Down Expand Up @@ -586,8 +588,10 @@ then put to an adversarial challenge agent instructed to refute it; **all six we
sourced to official GitHub Actions docs, `anthropics/claude-code-action` source at the pinned SHA
`be7b93b1907a4abad570368f3c74b6fe3807510b`, issue #443, and this repo's own files.

**Refreshed 2026-08-02** against `main` @ `a54c91e` (v1.9.0), after three releases landed underneath the draft.
Every in-repo `file:line` citation was re-read at that SHA and every arithmetic claim recomputed; citations are
**Refreshed 2026-08-02** against `main` @ `a54c91e` (v1.9.0), after three releases landed underneath the draft,
and **re-verified again the same day against the v1.11.0 release branch** — the `bonsai-status-sync` stub
conversion and the `DRIVER_AGENTS_REF` + tripwire commits both moved `claude.yml` line numbers after that
first pass. Every in-repo `file:line` citation was re-read and every arithmetic claim recomputed; citations are
now path-qualified. Five findings from the CodeRabbit review of PR #21 were adopted and one **rejected on
evidence** — see *On declaring secrets* above. The **external** citations into `anthropics/claude-code-action`
were *not* re-verified; they remain as originally researched at the pinned SHA.
8 changes: 8 additions & 0 deletions templates/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ Ready to Deploy → Delivered / Deployed / Completed. The workflows never set th
`#driver-agents-status` from CI too — the org-level `SHOPIFY_ALERT_WEBHOOK` secret (already
set org-wide, nothing per repo) is provisioned to the runner and the alert is labeled with the
run URL; if that secret is ever absent, alerts are silently off and nothing else changes.
The implementer's system prompt carries the Shopify operator tripwire (never bypass the
wrapper; never evade an exit-3 refusal) — the blockquote is copied verbatim from driver-agents
`docs/agent-instructions-shopify.md`, which is canonical: edit there first, re-copy here on
the next kit bump, **preserving the kit-side scope lead-in that precedes it** (it is not
canonical text — it un-scopes the block from the conduct rules above and tells the model how to
report a trip on a rail with no exit code; see the comment in `claude.yml`). The whole value
rides inside a **single-quoted** CLI token: **no apostrophes anywhere in it** — one apostrophe
silently truncates the prompt instead of erroring. `lint.yml` asserts the quote count.
3. **Orchestrator PAT (the cascade requirement).** GitHub does **not** re-trigger workflows from
events caused by the default `GITHUB_TOKEN`. The cron orchestrator must create issues with a
**single fine-grained PAT owned by the `driver-digital-agents` machine-user account** —
Expand Down
Loading