Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bonsai-status-sync reusable (phase 1) + refreshed conversion/identity scopes - #21

Merged
mcarter-astronautdev merged 6 commits into
mainfrom
docs/reusable-conversion-scope
Aug 2, 2026
Merged

mcarter-astronautdev merged 6 commits into
mainfrom
docs/reusable-conversion-scope

Conversation

@mcarter-astronautdev

@mcarter-astronautdev mcarter-astronautdev commented Jul 31, 2026

Copy link
Copy Markdown
Member

Two things, in dependency order: the scope docs get refreshed against current main, then the first phase of the conversion they describe actually ships.

What changed since this branch was opened

The branch was written against main @ 9b70acf (v1.6.0). main is now a54c91e (v1.9.0) — three releases landed underneath it. The branch has been rebased onto main and both docs re-verified line-by-line.

1. feat: the bonsai-status-sync reusable

.github/workflows/bonsai-status-sync.yml — the Bonsai status-sync job, converted from the 190-line per-repo copy that every fleet repo carries.

  • Job body is byte-identical to the copy it replaces, except one added comment. Triggers, permissions and concurrency semantics unchanged.
  • actionlint + shellcheck clean on both halves.
  • BONSAI_BEARER_TOKEN declared required: true (org-level secret, no legitimate caller without it — fails at startup rather than as a 401 mid-run).

The caller stub is deliberately not in this PR. A new reusable's stub cannot be pinned until the tag containing that reusable exists, so it lands in step 2 of the release order. Same as dependabot-keep-current, whose reusable landed in c362604 with its stub following later already carrying a real SHA. The kit keeps installing the working 190-line copy until then; the ready-to-land stub is parked verbatim in the scope doc.

New lint guard: fails the build on any kit stub left carrying an all-zero placeholder pin. An unresolvable uses: ref fails a consumer's run as startup_failure, which this repo's README documents as producing no check run and no notification — the one failure shape a client repo must never get silently.

2. docs: both scope docs refreshed and decided

Decisions recorded: identity unification deferred; the claude.yml half of the conversion tabled pending the OIDC spike; the bonsai-status-sync half approved and now in progress.

Version drift picked up: kit is 916 lines / 650 copied, claude.yml 404 → 460, store secrets renamed, SHOPIFY_ALERT_WEBHOOK was missing from the proposed interface (org-level, so an explicit map that omits it kills Slack alerting fleet-wide on a green run).

Errors found that were wrong when written, independent of the version bump:

  • the self-authored guard as drafted (github.event.*.user.id) is valid syntax that returns an array, coerces to NaN, and is therefore unconditionally true — it fails open silently on every event
  • the dual-accept fix paired both logins with one id; claude[bot] is 209825114, so it would have matched no real claude[bot] event
  • the measurement spike polled /rate_limit, which cannot see the secondary limit it was meant to measure, and consumes that budget while polling
  • the [created]-everywhere loop invariant was wrong on two of four events
  • the round-counter has a second hardcoded site that decides revise-vs-handoff
  • pilot assertions never required the private clone or store provisioning to succeed — both degrade to ::warning:: + exit 0

Verified against the org, closing two open decisions

  • No branch anywhere pins sync or claude as a required status check, so the syncsync / sync rename breaks nothing. All 42 protected branches across 15 repos checked — both contexts[] and the newer checks[], plus org and repo rulesets.
  • The wave is 18 repo@branch pairs across 11 repos, not 21. 21 is the repin target list, which additionally counts three stub-only repos carrying neither full workflow. claude.yml sits on exactly the same 18 pairs.

Two incidental findings worth knowing: studio-sulzer@main and Team-Laird@develop are live kit branches with no branch protection at all; and README.md still said v1.8.0 while main was v1.9.0 (fixed here).

Next

Phase 2 (pilot) → Phase 3 (wave, 18 targets) + tag/repin. ~6–7h. Only the pull_request leg is testable pre-merge — the issues and pull_request_review legs are default-branch-only and need validating after merge.

@coderabbitai

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/reusable-conversion-scope.md`:
- Around line 190-201: Correct the Total estimate in the phase plan to 28–35h,
matching the sum of the listed phase maxima; leave the individual phase
estimates unchanged.
- Around line 20-22: Update the drift statement in the conversion discussion to
scope “structurally impossible” drift specifically to reusable implementation
logic. Explicitly acknowledge that per-repository caller configuration—such as
SHOPIFY_STORE_NAME, permissions, concurrency, secret mapping, and possibly
DRIVER_AGENTS_REF in shopify-tool-smoke.yml—can still drift.
- Around line 176-180: Update the sequencing plan so the OIDC spike gates only
the Claude conversion, while the bonsai-status-sync wave proceeds independently
even if the spike fails. In docs/reusable-conversion-scope.md lines 176-180,
clarify this independent ordering; in README.md lines 96-100, explicitly state
that the spike gates claude.yml only and bonsai-status-sync.yml may proceed on
failure.
- Around line 58-71: Update the spike steps to record the exact stale SHA-pinned
uses reference for the reusable speak-code.yml/claude.yml workflow and specify
the caller workflow event that triggers it. Tie the recorded SHA to the
deliberately advanced workflows/main state, ensuring the pilot invokes the
pinned reusable workflow rather than the default main pin and validates the
caller-to-reusable OIDC path.
- Around line 170-172: Revise the reusable-workflow guidance around
DRIVER_AGENTS_REF and ANTHROPIC_API_KEY so it does not require every inherited
or explicitly passed secret to be declared under on.workflow_call.secrets. State
that declarations are required only when enforced by this repository’s lint.yml
policy or the chosen calling style, while preserving the existing
secret-handling guidance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b263fa9b-63a5-4c90-9dc4-8273ecd957e8

📥 Commits

Reviewing files that changed from the base of the PR and between 9b70acf and 3e3fb16.

📒 Files selected for processing (2)
  • README.md
  • docs/reusable-conversion-scope.md

Comment thread docs/reusable-conversion-scope.md Outdated
Comment on lines +20 to +22
**Conversion does not eliminate waves** — SHA-pinned stubs still need a pin bump per change. It changes what a
wave *is*: from re-copying 594 lines into 18 targets while hand-preserving per-repo edits, to changing one SHA
string. Drift becomes structurally impossible because there is no downstream logic to edit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Limit the drift claim to reusable implementation logic.

The proposed design still leaves per-repository settings such as SHOPIFY_STORE_NAME, permissions, concurrency, secret mapping, and possibly DRIVER_AGENTS_REF in shopify-tool-smoke.yml. State that implementation drift becomes impossible. Caller configuration drift remains possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reusable-conversion-scope.md` around lines 20 - 22, Update the drift
statement in the conversion discussion to scope “structurally impossible” drift
specifically to reusable implementation logic. Explicitly acknowledge that
per-repository caller configuration—such as SHOPIFY_STORE_NAME, permissions,
concurrency, secret mapping, and possibly DRIVER_AGENTS_REF in
shopify-tool-smoke.yml—can still drift.

Comment on lines +58 to +71
**Spike design (~3–4h):**
1. In `DriverDigital/workflows`, add a throwaway `.github/workflows/spike-claude.yml` — `workflow_call`,
`id-token: write`, the real `claude-code-action` step, nothing else.
2. In `vite-plugin-shopify-clean`, add `.github/workflows/spike-claude-caller.yml` with the **full**
permissions block. **Merge to the default branch** — mandatory, see "no pre-merge test path" below.
3. **Push one trivial commit to `workflows/main` first**, so the pinned SHA is provably behind `main` HEAD.
This is the assertion nobody thinks to make: a pilot run at a pin that happens to equal `main` HEAD passes
and then breaks the fleet on the next commit to `main`. *(As of writing, `main` @ `9b70acf` is already one
untagged commit past `v1.6.0` @ `0a3934f` — the condition exists naturally; make it deliberate.)*
4. Open one issue as `driver-digital-agents` containing `@claude`.
5. **PASS** = log shows the OIDC exchange succeeding **and** a `pull_request` `opened` webhook whose
`user.login` is literally `claude[bot]`. **FAIL** = a green run with
`::warning::Skipping action due to workflow validation` and no PR.
6. Delete both files. Record the run URL in the release notes either way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)docs/reusable-conversion-scope\.md$|workflows' || true

echo
echo "== target section =="
sed -n '1,120p' docs/reusable-conversion-scope.md | cat -n

echo
echo "== workflow_sha references in repo =="
rg -n "spike-claude|uses:\s*[^@\s]+@\{?#?\s*[\da-fA-F]+\"?\s*$|@[\da-fA-F]{40}|id-token:\s*write|permissions:\s*\{|workflow_call|pull_request|opened" -S .

Repository: DriverDigital/workflows

Length of output: 20002


Record the stale reusable SHA and caller trigger in the pilot steps.

The spike must use a SHA that is behind main, but step 3 only mentions pushing to workflows/main. Add the exact uses: reference for speak-code.yml/claude.yml and the caller workflow's event, so the pilot proves the caller-to-reusable OIDC path without accidentally running the default main pin.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~59-~59: The official name of this software platform is spelled with a capital “H”.
Context: ...iverDigital/workflows, add a throwaway .github/workflows/spike-claude.ymlworkflow...

(GITHUB)


[uncategorized] ~61-~61: The official name of this software platform is spelled with a capital “H”.
Context: ... 2. In vite-plugin-shopify-clean, add .github/workflows/spike-claude-caller.yml with...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reusable-conversion-scope.md` around lines 58 - 71, Update the spike
steps to record the exact stale SHA-pinned uses reference for the reusable
speak-code.yml/claude.yml workflow and specify the caller workflow event that
triggers it. Tie the recorded SHA to the deliberately advanced workflows/main
state, ensuring the pilot invokes the pinned reusable workflow rather than the
default main pin and validates the caller-to-reusable OIDC path.

Comment thread docs/reusable-conversion-scope.md Outdated
Comment on lines +170 to +172
`DRIVER_AGENTS_REF` moves **into** the reusable (a win — it removes half the two-files-must-match hazard).
`ANTHROPIC_API_KEY` stays comment-only. Every secret the reusable references must be **declared** under
`on.workflow_call.secrets` or this repo's own `lint.yml` goes red.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
git ls-files | rg '(^|/)(docs/reusable-conversion-scope\.md|lint\.ya?ml|.*\.ya?ml)$' || true

echo
echo "== docs excerpt =="
if [ -f docs/reusable-conversion-scope.md ]; then
  sed -n '150,190p' docs/reusable-conversion-scope.md
fi

echo
echo "== lint files =="
for f in lint.yml .github/workflows/lint.yml lint.yaml .github/workflows/lint.yaml; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,220p' "$f"
  fi
done

echo
echo "== reusable workflow secret declarations in workflows =="
rg -n 'workflow_call|DRIVER_AGENTS_REF|ANTHROPIC_API_KEY|secrets:|secrets: inherit|GITHUB_TOKEN|ANTHROPIC_API_KEY|DRIVER_AGENTS_REF' -S .github lint.yml lint.yaml docs/reusable-conversion-scope.md 2>/dev/null || true

Repository: DriverDigital/workflows

Length of output: 9109


🌐 Web query:

GitHub Actions reusable workflows inherited secrets referenced without declaring on.workflow_call.secrets documentation

💡 Result:

In GitHub Actions, you can reference secrets in a reusable workflow without explicitly declaring them under on.workflow_call.secrets if, and only if, you use the secrets: inherit keyword in the calling workflow [1][2]. When you use secrets: inherit in the caller's job configuration, all secrets accessible to the calling workflow—including organization, repository, and environment secrets—are implicitly passed to the reusable workflow [1][3][4]. This allows the reusable workflow to access these secrets directly via the ${{ secrets.SECRET_NAME }} context, even if the on.workflow_call section does not define them [1][5]. Key considerations regarding this approach include: 1. Automatic Availability: The inherit keyword bypasses the need for individual mapping [1][6]. Once inherited, the secrets are available within the reusable workflow's steps as if they were native to the environment [7]. 2. Scope: Secrets are only passed to directly called workflows. If you have a chain of workflows (e.g., A calls B, which calls C), secrets must be explicitly passed or inherited at each transition point [1][2]. 3. Security/Best Practices: While convenient, using secrets: inherit provides the reusable workflow access to all secrets available to the caller [6]. Explicitly declaring secrets is often preferred for reusable workflows intended for use by external repositories or to adhere to the principle of least privilege, as it limits the "blast radius" by ensuring only necessary secrets are provided [6]. 4. Parse-time vs. Runtime: Attempts to reference inherited secrets within a with: block (as inputs) will fail at parse time because with: blocks are evaluated in the context of the caller, where the inherited secrets are not yet available [7]. Inherited secrets should only be referenced directly within the steps of the reusable workflow [7].

Citations:


Don’t require all inherited secrets to be declared.

Lines [170-172] say every referenced reusable secret must be declared under on.workflow_call.secrets. GitHub reusable workflows can use secrets: inherit, and declared secrets can be passed explicitly without listing each reused secret in the callee. Keep this as a lint policy only if lint.yml enforces the repository’s preferred calling style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reusable-conversion-scope.md` around lines 170 - 172, Revise the
reusable-workflow guidance around DRIVER_AGENTS_REF and ANTHROPIC_API_KEY so it
does not require every inherited or explicitly passed secret to be declared
under on.workflow_call.secrets. State that declarations are required only when
enforced by this repository’s lint.yml policy or the chosen calling style, while
preserving the existing secret-handling guidance.

Source: MCP tools

Comment thread docs/reusable-conversion-scope.md Outdated
Comment on lines +176 to +180
## Sequencing — two independent waves, `bonsai-status-sync` first

`bonsai-status-sync.yml` goes first because **it can be proven pre-merge**. Its `pull_request` trigger runs
from the PR merge ref, so its own cutover PR tests it. It has no OIDC path, one secret, and its 18 deployed
copies are currently **byte-identical** to the template (sha256 `811148d0…`, verified) — a genuinely known

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the OIDC spike gate only the Claude conversion.

The plan describes independent waves, but the phase order and README wording imply that the spike blocks both workflows.

  • docs/reusable-conversion-scope.md#L176-L180: run the bonsai-status-sync wave independently, or explicitly make Phase 0 a global gate.
  • README.md#L96-L100: state that the spike gates only claude.yml; bonsai-status-sync.yml can proceed if the spike fails.
📍 Affects 2 files
  • docs/reusable-conversion-scope.md#L176-L180 (this comment)
  • README.md#L96-L100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reusable-conversion-scope.md` around lines 176 - 180, Update the
sequencing plan so the OIDC spike gates only the Claude conversion, while the
bonsai-status-sync wave proceeds independently even if the spike fails. In
docs/reusable-conversion-scope.md lines 176-180, clarify this independent
ordering; in README.md lines 96-100, explicitly state that the spike gates
claude.yml only and bonsai-status-sync.yml may proceed on failure.

Comment thread docs/reusable-conversion-scope.md Outdated
Comment on lines +190 to +201
| Phase | Work | Est. |
|---|---|---|
| 0 | Spike: go/no-go on OIDC-in-reusable (see above) | 3–4h |
| 1 | Convert `bonsai-status-sync.yml` + stub + docs + lint | 3h |
| 2 | Pilot it (public leg + private leg; testable pre-merge) | 2h |
| 3 | Fleet wave for `bonsai-status-sync`, 18 targets | 3–4h |
| 4 | Convert `claude.yml` — move the 404 lines **faithfully** | 6–8h |
| 5 | Tag + repin kit stubs (README's mandatory 3-step release order) | 1h |
| 6 | Pilot `claude.yml` with the four assertions incl. pin-vs-HEAD | 4–6h |
| 7 | Fleet wave: 10 single-branch repos → Avara → Palmers as one 8-branch batch | 4–5h |
| 8 | Optional: convert `shopify-tool-smoke.yml` | 2h |
| | **Total** | **28–36h** |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the total effort estimate.

The phase maxima sum to 35 hours, not 36:

4 + 3 + 2 + 4 + 8 + 1 + 6 + 5 + 2 = 35

Change the total to 28–35h, or add the missing hour to a phase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reusable-conversion-scope.md` around lines 190 - 201, Correct the Total
estimate in the phase plan to 28–35h, matching the sum of the listed phase
maxima; leave the individual phase estimates unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/identity-unification-scope.md`:
- Around line 97-99: Update the release-wave stub’s dual-login acceptance so
each login is matched with its own immutable ID: retain claude[bot] with
209825114 and driver-digital-agents with 261291955 during the overlap, then
remove the claude[bot]/209825114 pair after migration.
- Around line 224-234: Update the schedule table’s estimate to match the
authoritative PR estimate of 28–36h across all eight phases, or revise the PR
objectives to 25–28h if that lower scope is intentional; document any resulting
scope change and ensure the phase count and total are consistent.
- Around line 160-163: The workflow documentation incorrectly claims every event
uses created and that changing any types list causes an infinite loop. Rewrite
the section to enumerate the actual event types, including opened for issues,
submitted for pull-request reviews, and created for issue comments, and state
that the loop invariant is specifically that issue_comment must not subscribe to
edited.
- Around line 249-263: Add explicit pilot assertions for the private
driver-agents clone and Shopify provisioning flow, including a valid
SHOPIFY_STORE_NAME, successful store provisioning, and successful completion of
both operations before the fleet wave begins; ensure failures cannot degrade to
warnings with a successful exit.
- Around line 176-177: Update the self-authored guard in the actor-gate
documentation to use event-specific user ID fields instead of the invalid
github.event.*.user.id wildcard: use github.event.issue.user.id for issue
branches, github.event.comment.user.id for comment branches, and the
corresponding exact actor field for every other covered event branch.
- Around line 40-45: Update the measurement plan in the documented workflow
scope to track the secondary content-generation limit separately from GET
/rate_limit: add a spike step that counts every content-generating write and
records 403/429 responses, response bodies, and Retry-After values, alongside
the existing primary-limit measurements.
- Around line 25-28: Update the rate-limit explanation in the installation-token
section to include the measured organization user count and GitHub App
user-based scaling above the 20-user base. Show the complete calculation
alongside the existing repository-based calculation so the 6,850–7,850 range is
fully derivable, preserving the existing plan and token-scope details.

In `@docs/reusable-conversion-scope.md`:
- Around line 97-100: Update the Phase 0 reference in the sequencing paragraph
to explicitly say “the Phase 0 spike” instead of “Phase 0 below,” while leaving
the surrounding sequencing guidance unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 718b5357-2f22-48bf-9f6c-0efdfd3e928c

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3fb16 and 1e48d5e.

📒 Files selected for processing (2)
  • docs/identity-unification-scope.md
  • docs/reusable-conversion-scope.md

Comment thread docs/identity-unification-scope.md Outdated
Comment on lines +25 to +28
DriverDigital is on the **Team** plan (not Enterprise Cloud; verified via `gh api /orgs/DriverDigital`). The
Claude App is installed org-wide across 57 repos, giving its installation token roughly **6,850–7,850 req/hr**
(5,000 base + 50/repo above 20, capped at 12,500). A user PAT is a **flat 5,000/hr** with no scaling — and
critically it is **per-account, not per-token**, so the proposed two-PAT split does *not* buy two buckets.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^docs/identity-unification-scope\.md$|README|doc)' || true

echo "== target snippet =="
if [ -f docs/identity-unification-scope.md ]; then
  nl -ba docs/identity-unification-scope.md | sed -n '1,80p'
fi

echo "== repository/user references =="
rg -n "DriverDigital|installed|installation token|7,?850|6,?850|Team plan|Enterprise Cloud|repo above 20|organization users|GitHub rate|rate limit|per-account" .

Repository: DriverDigital/workflows

Length of output: 340


🌐 Web query:

GitHub REST API rate limits for verified organizations paid plans installation limits organization members

💡 Result:

GitHub REST API rate limits vary based on authentication method, the type of application, and whether the organization is using GitHub Enterprise Cloud [1][2]. Standard Rate Limits For most authenticated requests, the standard rate limit is 5,000 requests per hour [1][2]. Requests made on your behalf by an OAuth app or a GitHub App may be subject to this limit, though limits are higher for applications owned by or installed on GitHub Enterprise Cloud organizations [1][2]. GitHub Apps and Scalable Limits GitHub Apps using an installation access token provide significant advantages for organizations: 1. Scalability: Unlike OAuth apps, which have fixed limits, the rate limit for GitHub Apps using an installation access token scales based on the number of repositories and the number of organization users [1][3]. 2. Enterprise Cloud: When an installation is on a GitHub Enterprise Cloud organization or enterprise, the minimum rate limit for the installation increases to 15,000 requests per hour [1][2]. Organization Membership and Specific Limits - API Requests: Generally, an individual user's membership status in an organization does not increase their personal 5,000 requests-per-hour limit [1][2]. However, if you are a member of a GitHub Enterprise Cloud organization, requests made on your behalf by an OAuth app approved or owned by that organization may benefit from a 15,000 requests-per-hour limit [1][2]. - Secondary Rate Limits: Regardless of primary limits, all users are subject to secondary rate limits to prevent abuse and ensure system stability [4][5]. These are triggered by rapid, high-volume requests (e.g., creating too many resources quickly) [4][6]. - Specific Operational Limits: Certain actions have non-API rate limits. For example, organization owners can create 50 invitations per 24 hours, which increases to 500 invitations per 24 hours if the organization is older than one month or on a paid plan [6]. Best Practices To manage these limits, GitHub recommends: - Using GitHub Apps instead of personal access tokens for automation [1][2]. - Using webhooks instead of polling the API [7]. - Implementing conditional requests with ETag or Last-Modified headers, which do not count against your limit if the content has not changed [7]. - Handling rate limit responses (403/429) by respecting the Retry-After, X-RateLimit-Remaining, and X-RateLimit-Reset headers [4].

Citations:


🌐 Web query:

GitHub Apps installation limits 5000 requests per hour 50 per repository organizations users rate limits

💡 Result:

GitHub Apps that use an installation access token are subject to rate limits that scale based on the installation's activity, specifically the number of repositories and users in an organization [1][2]. Key details regarding GitHub App rate limits include: Base Limits For GitHub App installations not on a GitHub Enterprise Cloud organization, the base rate limit is 5,000 requests per hour per installation [2][3]. Scalability The rate limit scales beyond the base amount: - Repositories: Installations with more than 20 repositories receive an additional 50 requests per hour for each repository [2][3]. - Users: Installations on an organization with more than 20 users receive an additional 50 requests per hour for each user [2][3]. - Maximum: These scalable limits have an upper bound, meaning they cannot increase beyond 12,500 requests per hour [2][3]. GitHub Enterprise Cloud If an installation is on a GitHub Enterprise Cloud organization or enterprise, it receives a higher rate limit of 15,000 requests per hour [4][2]. Additional Considerations - GraphQL: GitHub Apps also have limits on the point value of queries made to the GraphQL API, which follow similar scaling rules (e.g., 5,000 points per hour base, scaling up to 12,500 points per hour) [5][3]. - Secondary Rate Limits: In addition to the primary limits mentioned above, GitHub may apply secondary rate limits to prevent abuse and ensure system availability [5][6]. - Best Practices: To stay within these limits, developers are encouraged to use webhooks instead of polling, implement conditional requests, and use consolidated GraphQL queries where possible [7]. If a limit is hit, applications should respect the x-ratelimit-reset or Retry-After headers [7].

Citations:


🌐 Web query:

GitHub Apps rate limits 6850 7850 50 per repository users

💡 Result:

For GitHub Apps using installation access tokens, the rate limit is not a fixed number like 6850 or 7850; rather, it is a scalable system based on the size of the installation [1][2]. The standard rate limits for GitHub Apps using installation access tokens are as follows: 1. Minimum Limit: Every installation starts with a baseline rate limit of 5,000 requests per hour [1][2][3]. 2. Scaling: If an installation is on an organization with more than 20 repositories, it receives an additional 50 requests per hour for each repository [1][2][3]. Similarly, if the organization has more than 20 users, it receives an additional 50 requests per hour for each user [1][2][3]. 3. Maximum Limit: The rate limit for an installation cannot increase beyond 12,500 requests per hour [1][2][3]. 4. GitHub Enterprise Cloud: For installations on a GitHub Enterprise Cloud organization, the rate limit is 15,000 requests per hour [1][2][3]. The numbers "6850" and "7850" do not correspond to standard GitHub App rate limits. If you are encountering these specific values in API response headers, they likely reflect the calculated limit for a specific installation based on its number of repositories and users at that time. GitHub Apps using user access tokens are instead subject to the primary rate limits of the authenticated user (typically 5,000 requests per hour) [1][2][3].

Citations:


Document the complete installation-rate-limit calculation.

The 6,850–7,850 range cannot be derived from repo count alone. GitHub Apps also scale by organization users above the 20-user base, so add the measured user count and the full calculation here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 25 - 28, Update the
rate-limit explanation in the installation-token section to include the measured
organization user count and GitHub App user-based scaling above the 20-user
base. Show the complete calculation alongside the existing repository-based
calculation so the 6,850–7,850 range is fully derivable, preserving the existing
plan and token-scope details.

Comment thread docs/identity-unification-scope.md Outdated
Comment on lines +40 to +45
1. **Move every read that does not need the machine-user identity onto `github.token`** — a separate
**1,000/hr *per repository*** bucket, so across 21 targets that is ~21,000 req/hr of free capacity going
unused today. Concretely: `ticketed-review.yml:63,126` and `pr-first-review.yml:78` are pure reads.
2. **Measure before committing.** Add a step to one `claude.yml` and one `ticketed-review` run that hits
`GET /rate_limit` at job start and end and echoes `x-ratelimit-used` — that endpoint does not count against
the limit. Multiply by wave size.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== doc excerpt =="
if [ -f docs/identity-unification-scope.md ]; then
  nl -ba docs/identity-unification-scope.md | sed -n '1,80p'
else
  echo "docs/identity-unification-scope.md not found"
fi

echo
echo "== search rate_limit usage in repo =="
rg -n "rate_limit|x-ratelimit|Retry-After|403|429|content-generation|secondary" -S . \
  --glob '!**/.git/**' --glob '!node_modules/**' || true

echo
echo "== relevant workflow files if present =="
for f in ticketed-review.yml pr-first-review.yml claude.yml; do
  echo "--- $f occurrences ---"
  git ls-files | rg "(^|/)${f}$|${f}"
done

Repository: DriverDigital/workflows

Length of output: 219


🌐 Web query:

GitHub REST API /rate_limit requests to check secondary rate limit status Retry-After x-ratelimit-remaining documentation

💡 Result:

The GitHub REST API does not provide a dedicated endpoint or method to check the current status of your secondary rate limit [1][2]. Secondary rate limits are triggered dynamically based on activity and are separate from the primary, documented rate limits [3][2]. To manage secondary rate limits, you must handle the error responses provided by the API: 1. Handling 403 or 429 Errors: When you exceed a secondary rate limit, the API will return a 403 Forbidden or 429 Too Many Requests response [1][4]. This response will typically include an error message indicating that a secondary rate limit has been triggered [4][5]. 2. Using the Retry-After Header: If the response includes a Retry-After header, you must wait for the number of seconds specified in that header before retrying your request [1][4]. 3. Exponential Backoff: If a Retry-After header is not provided, or if your request continues to fail, you should implement an exponential backoff strategy, increasing the wait time between retries [1][2][5]. 4. Best Practices: To avoid triggering secondary rate limits, the recommended practice is to perform requests serially rather than concurrently, and, for write operations (POST, PATCH, PUT, DELETE), to wait at least one second between requests [5]. Regarding the GET /rate_limit endpoint: - While you can use GET /rate_limit to check your primary rate limit status without it counting against that primary limit, this call can still count against your secondary rate limit [1][3]. - You should prioritize using the primary rate limit response headers (such as x-ratelimit-remaining and x-ratelimit-reset) included with standard API responses to monitor your primary limits instead of frequently calling /rate_limit [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== doc excerpt =="
python3 - <<'PY'
from pathlib import Path
p = Path("docs/identity-unification-scope.md")
if not p.exists():
    print("docs/identity-unification-scope.md not found")
    raise SystemExit
for i in range(1, 81):
    print(f"{i:5d}: {p.read_text(encoding='utf-8').splitlines()[i-1]}")
PY

echo
echo "== search rate_limit usage in repo =="
rg -n "rate_limit|x-ratelimit|Retry-After|403|429|content-generation|secondary" -S . \
  --glob '!**/.git/**' --glob '!node_modules/**' || true

echo
echo "== relevant workflow files if present =="
for f in ticketed-review.yml pr-first-review.yml claude.yml; do
  echo "--- $f occurrences ---"
  git ls-files | rg "^${f}$|^\\..*${f}$|/${f}$" || true
done

Repository: DriverDigital/workflows

Length of output: 7570


Track the secondary content-limit separately.

GET /rate_limit and x-ratelimit-used only instrument the primary GitHub REST request limit. The 500/hr content-generation limit has no separate status endpoint, so add a spike step that counts each content-generating write and records 403/429 errors, response text, and Retry-After data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 40 - 45, Update the
measurement plan in the documented workflow scope to track the secondary
content-generation limit separately from GET /rate_limit: add a spike step that
counts every content-generating write and records 403/429 responses, response
bodies, and Retry-After values, alongside the existing primary-limit
measurements.

Comment on lines +97 to +99
**Fix:** dual-accept both logins in the stub for one release wave so in-flight `claude[bot]` PRs keep their
revise loop mid-cutover, then drop the old literal. Pair the login with the immutable id `261291955`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pair each accepted login with its own immutable ID.

The release-wave plan accepts both claude[bot] and driver-digital-agents, but then pairs the login with only 261291955. Existing claude[bot] events would fail the gate if both values are required. Preserve the claude[bot] ID 209825114 during the overlap, then remove that pair after migration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 97 - 99, Update the
release-wave stub’s dual-login acceptance so each login is matched with its own
immutable ID: retain claude[bot] with 209825114 and driver-digital-agents with
261291955 during the overlap, then remove the claude[bot]/209825114 pair after
migration.

Comment thread docs/identity-unification-scope.md Outdated
Comment on lines +160 to +163
**What still terminates — and this is load-bearing and documented nowhere:** the agent's final response is an
`updateComment` (`issue_comment: edited`), and every `on:` block subscribes to **`[created]` only**. That
single fact is what stops the human-reply path. **Adding `edited` to any `types:` list is an instant infinite
loop.** Write that invariant into the file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

State the loop invariant for the correct event.

The supplied workflow does not use [created] for every event. It uses [opened] for issues and [submitted] for pull-request reviews. The relevant invariant is that issue_comment does not subscribe to edited. Rewrite this section to enumerate the event types and avoid implying that changing any unrelated types: list creates the same loop risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 160 - 163, The workflow
documentation incorrectly claims every event uses created and that changing any
types list causes an infinite loop. Rewrite the section to enumerate the actual
event types, including opened for issues, submitted for pull-request reviews,
and created for issue comments, and state that the loop invariant is
specifically that issue_comment must not subscribe to edited.

Comment thread docs/identity-unification-scope.md Outdated
Comment on lines +176 to +177
1. **Self-authored guard** — every actor-gate branch gains `github.event.*.user.id != 261291955`. Cost: the
agent can no longer hand off to itself across events. Accept that; it is the point.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
git ls-files | rg '(^|/)docs/identity-unification-scope\.md$' || true

echo "== relevant lines =="
if [ -f docs/identity-unification-scope.md ]; then
  nl -ba docs/identity-unification-scope.md | sed -n '140,205p'
fi

echo "== search for guard fields =="
rg -n "issue\.user\.id|comment\.user\.id|review\.user\.id|github\.event\.\*\.user\.id|github\.event\.|user\.id|261291955|actor-gate|gate" docs/identity-unification-scope.md || true

Repository: DriverDigital/workflows

Length of output: 278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file stats =="
wc -l docs/identity-unification-scope.md || true

echo "== relevant lines =="
awk '{printf "%6d\t%s\n", NR, $0}' docs/identity-unification-scope.md | sed -n '140,205p'

echo "== search for guard fields =="
grep -n "issue\.user\.id\|comment\.user\.id\|review\.user\.id\|github\.event\.\*\.user\.id\|github\.event\.\|user\.id\|261291955\|actor-gate\|gate" docs/identity-unification-scope.md || true

Repository: DriverDigital/workflows

Length of output: 6158


🌐 Web query:

GitHub event payload issue_comment user id field github.event.comment.user.id

💡 Result:

In the GitHub Actions environment, when a workflow is triggered by an issue_comment event, the payload structure provided to the workflow includes a comment object [1][2][3]. Within the GitHub Actions context, you can access the user ID of the commenter using the expression ${{ github.event.comment.user.id }} [4]. The underlying structure is provided by the GitHub webhook payload for the issue_comment event [2][5]. Inside the payload, the comment object contains a user object, which holds information about the user who authored the comment [1][6]. The user object includes the following relevant fields: - id: The unique integer identifier for the GitHub user [1][6]. - login: The username of the user [1][6]. When using GitHub Actions, the event payload is accessible via the github context [1]. The hierarchy is typically: github.event -> comment -> user -> id [1][4]. This data is consistent with the GitHub REST API representation of an issue comment, where the user field contains the metadata for the comment author [7][8][6].

Citations:


Use event-specific fields for the self-authored guard.

The wildcard github.event.*.user.id does not match any GitHub Actions event context. Put github.event.issue.user.id != 261291955 on the issue branch, github.event.comment.user.id != 261291955 on the comment branch, and the equivalent exact field on any other actor branch the guard covers.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~176-~176: The official name of this software platform is spelled with a capital “H”.
Context: ...guard** — every actor-gate branch gains github.event.*.user.id != 261291955. Cost: th...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 176 - 177, Update the
self-authored guard in the actor-gate documentation to use event-specific user
ID fields instead of the invalid github.event.*.user.id wildcard: use
github.event.issue.user.id for issue branches, github.event.comment.user.id for
comment branches, and the corresponding exact actor field for every other
covered event branch.

Comment thread docs/identity-unification-scope.md Outdated
Comment on lines +224 to +234
| Phase | Work | Est. |
|---|---|---|
| 0 | Dump live `AGENTS_GH_PAT` scope; mint + test the two PATs | 2h |
| 1 | Rate-limit measurement spike (`GET /rate_limit` instrumentation) | 2h |
| 2 | `claude.yml` identity swap + containment set | 4–6h |
| 3 | Rail gate rework (3 sites + counters + sentinel matching) | 3–4h |
| 4 | Move reads onto `github.token`; add write retry/backoff | 3h |
| 5 | Pilot (two legs, assertions below) | 4h |
| 6 | Fleet wave, 21 targets | 4h |
| 7 | Wave 2: trigger phrase, incl. the orchestrator | 3h |
| | **Total** | **25–28h** |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the schedule total with the PR estimate.

This table totals 25–28h, while the PR objectives state 28–36h across eight phases. Choose one authoritative estimate and document the scope change if the lower total is intentional.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~230-~230: The official name of this software platform is spelled with a capital “H”.
Context: ...atching) | 3–4h | | 4 | Move reads onto github.token; add write retry/backoff | 3h | ...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 224 - 234, Update the
schedule table’s estimate to match the authoritative PR estimate of 28–36h
across all eight phases, or revise the PR objectives to 25–28h if that lower
scope is intentional; document any resulting scope change and ensure the phase
count and total are consistent.

Comment thread docs/identity-unification-scope.md Outdated
Comment on lines +249 to +263
**Assertions, each with its false-pass named:**

1. **Authorship** — the PR's `user.login` is `driver-digital-agents`. *False pass:* a prefill PR link, or
`gh pr view` reporting `app/claude`. Read the webhook.
2. **Commit attribution** — commits are authored by `driver-digital-agents`, not `github-actions[bot]`.
*False pass:* checking the PR author instead of the commits.
3. **Cascade** — a `bonsai-status-sync` run exists triggered by `pull_request`/`opened`, and the Bonsai task
reads Internal Review. *False pass:* the task already being in that state.
4. **Ticketed rail fires** — exactly one `ticketed-review` run for the PR, and it actually reviews.
*False pass:* a green skip; assert a check run exists.
5. **Tag mode intact** — a human `@` gets a visible tracking comment *and* a final reply. *False pass:*
a green run with no comment — the #148 signature.
6. **No self-trigger** — after the agent replies, assert zero further `claude.yml` runs on that thread.
7. **`gh issue develop` works** on the implementer PAT's fine-grained scope.
8. **Rate-limit headroom** — `x-ratelimit-used` recorded at job end, extrapolated to wave size.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add explicit assertions for private cloning and store provisioning.

The document says that a failed private driver-agents clone and Shopify provisioning step can degrade to a warning and exit successfully. The pilot assertions do not require either operation to succeed. Add checks for the private clone, SHOPIFY_STORE_NAME provisioning, and successful completion before the fleet wave.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/identity-unification-scope.md` around lines 249 - 263, Add explicit
pilot assertions for the private driver-agents clone and Shopify provisioning
flow, including a valid SHOPIFY_STORE_NAME, successful store provisioning, and
successful completion of both operations before the fleet wave begins; ensure
failures cannot degrade to warnings with a successful exit.

Comment thread docs/reusable-conversion-scope.md Outdated
Comment on lines +97 to +100
**If that project ships first, Phase 0 below ceases to exist** — no App token means no OIDC exchange, no
default-branch validation, and no `job_workflow_ref` question. The `claude.yml` stub also stops needing
`id-token: write`, which was what made it the most privileged stub in the kit. Sequencing identity-first is
therefore the cheaper order.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the Phase 0 cross-reference.

Line 97 says “Phase 0 below”, but the Phase 0 spike is defined above, at Lines 36-74. Replace this with “the Phase 0 spike” to avoid ambiguity in the sequencing instructions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reusable-conversion-scope.md` around lines 97 - 100, Update the Phase 0
reference in the sequencing paragraph to explicitly say “the Phase 0 spike”
instead of “Phase 0 below,” while leaving the surrounding sequencing guidance
unchanged.

The README has carried 'converting those two full workflows into reusables
remains future work' since the repo split. This is the scope to execute it.

The case, in one number: those two files are 594 of the kit's 837 lines, and
they are the two that get COPIED rather than called. Every drift incident so far
traces to that — most recently Avara, where a repo hand-carried a subset of
upstream changes and shipped the prompt-hijack bug that had already been fixed
centrally. Conversion does not remove waves (SHA pins still need bumping); it
changes a wave from re-copying 594 lines into 18 targets into changing one SHA
string, and makes drift structurally impossible because there is no downstream
logic left to edit.

Gated on one spike. claude-code-action mints the claude[bot] App token through
an endpoint that validates the workflow file against the default branch. It is
undocumented whether that check reads the caller stub or also the SHA-pinned
reusable. If the latter, every pin breaks the moment workflows/main moves and
the immutable-SHA policy cannot survive. Evidence from claude-code-action#443
says caller-only, but nobody there reports a SHA-pinned cross-repo reusable, so
Phase 0 settles it empirically — deliberately run at a pin behind main HEAD,
because a pilot at a pin equal to HEAD passes and then breaks the fleet later.

Also written down: the github_token workaround is an explicit NON-option (it
changes the PR author and silently misroutes every ticketed PR to the wrong
rail), the claude.yml stub cannot be thin in the privilege sense, the actor gate
belongs inside the reusable, SHOPIFY_STORE_NAME must become a with: input (which
fixes a latent data-loss bug — the next re-copy would silently reset Avara's
handle), and claude.yml has no pre-merge test path, so its cutover PR must say
that an @claude test on it is expected to fail.

Sequenced as two independent waves with bonsai-status-sync first, because it is
the only one provable before merge. 28-36h total across 8 phases.

Researched by five parallel agents; all six hard-blocker claims were put to an
adversarial challenge pass and all six were refuted. Sourced to official GitHub
docs, the action source at its pinned SHA, issue #443, and this repo's files.
…ents

Scopes the GitHub surface running entirely as the machine user, inside
Actions. A Slack agent is a separate entity and out of scope.

Ten hard-blocker claims were each put to an adversarial challenge pass and
all ten were refuted, so nothing structurally blocks this. Five findings are
silent-failure class and are written up as requirements rather than risks.

The one that could change the decision: the aggregate API ceiling drops
~58-61%. The Claude App installation token scales with repo count (~6,850-
7,850/hr across 57 repos); a user PAT is a flat 5,000/hr and the limit is
per-ACCOUNT, so the two-PAT split buys no extra headroom. Worse, the 500/hr
content-creation secondary limit is also per-account, so two independent
comment budgets collapse into one. A full ticketed cycle is 30-60 content
ops; the 141-run wave earlier today would have blown through it. Mitigable
by moving reads onto github.token (a separate 1,000/hr PER REPO bucket,
~21,000/hr of unused capacity across the fleet), but it is a real regression
and is flagged for a decision, not buried.

The nastiest silent failure: a PAT-authored implementer PR matches NEITHER
review rail. The ticketed stub gates round 1 on the claude[bot] literal so
the reusable is never invoked, and pr-first's guard passes but its has_ticket
step skips on the uuid. Both rails go green having done nothing. This also
corrects reusable-conversion-scope.md, which claimed such PRs route to
PR-first; they route to nothing.

Loop containment is the centrepiece, and today's safety turns out to be
entirely accidental identity asymmetry: claude[bot] comments carry
author_association NONE and type Bot, so they fail the actor gate twice over
and the action's own checkHumanActor rejects them. A machine User passes
both. What still terminates the human-reply path is that the final response
is an updateComment and every trigger subscribes to [created] only - load
bearing, and documented nowhere until now. Adding 'edited' to any types list
would be an instant infinite loop.

Also: bot_id/bot_name default to github-actions[bot] so commits are
misattributed unless set; AGENTS_GH_PAT's live scope is undocumented and the
three descriptions of it in this repo contradict each other; and identity is
separable from the trigger phrase, so ship the token swap first with zero UX
change and flip @claude -> @driver-digital-agents as its own wave.

Sequencing identity-first also deletes Phase 0 of the reusable conversion.
Both docs were written against main @ 9b70acf (v1.6.0). Main is now a54c91e
(v1.9.0) — v1.7.0 added Slack alerting, v1.8.0 an audit context + artifact leg,
v1.9.0 renamed the store secrets. Every in-repo file:line citation re-read at
a54c91e, every number recomputed, citations path-qualified (several filenames
exist in both templates/github/ and .github/workflows/ with different content).

Version drift picked up:
- kit is 916 lines / 650 copied (still 71%); claude.yml 404 -> 460
- DRIVER_AGENTS_SCOPES_* -> DRIVER_ENGINEERING_APP_* in the proposed interface
- SHOPIFY_ALERT_WEBHOOK missing from that interface; it's org-level, so an
  explicit secrets map that omits it kills Slack alerting fleet-wide, green
- SHOPIFY_STORE_NAME gained a second consumer (the v1.8.0 artifact name)
- the "pin is naturally behind HEAD" reassurance is now false — main == v1.9.0

Errors found on re-checking, independent of the version bump:
- the self-authored guard as drafted (github.event.*.user.id) is valid syntax
  that returns an array, coerces to NaN, and is therefore unconditionally true.
  It fails open silently on every event.
- the dual-accept fix paired both logins with one id; claude[bot] is 209825114,
  not 261291955, so it would have matched no real claude[bot] event
- the measurement spike polled /rate_limit, which cannot see the secondary
  limit it was meant to measure and consumes that budget while polling
- the "[created] everywhere" loop invariant was wrong on two of four events
- the round-counter has a second hardcoded site (:252) that decides
  revise-vs-handoff; changing only :137 forces a silent handoff every run
- pilot assertions never required the private clone or store provisioning to
  succeed, both of which degrade to a warning and exit 0
- reusable phase table max summed to 35 while stating 36

Estimates now 29-37h (26-33h if identity ships first) and 26-30h. Phases 1-3 of
the reusable conversion — the bonsai-status-sync half, 8-11h — turn out to
depend on neither project's decision and could start now.
@mcarter-astronautdev
mcarter-astronautdev force-pushed the docs/reusable-conversion-scope branch from 1e48d5e to 1376a61 Compare August 2, 2026 00:29
Converts the Bonsai status-sync job into a cross-repo reusable. The 190-line
per-repo copy is the kit's second-largest drift surface; this moves its logic
into one file.

The job body is byte-identical to the copy it replaces except for one added
comment. Triggers, permissions and concurrency semantics are unchanged.
actionlint + shellcheck clean on both halves.

Deliberately NOT in this commit: the caller stub. A new reusable's stub cannot
be pinned until the tag containing that reusable exists, so it lands in step 2
of the release order. That is the house precedent — dependabot-keep-current's
reusable landed in c362604 and its stub arrived later already carrying a real
SHA. The kit keeps installing the working 190-line copy until then. The
ready-to-land stub is parked verbatim in the scope doc.

Also adds a lint guard that fails the build on any kit stub left carrying an
all-zero placeholder pin: an unresolvable `uses:` ref fails a consumer's run as
startup_failure, which this repo's README documents as producing no check run
and no notification. That is the one failure shape a client repo must never
get silently, and a release checklist alone would not catch it.

Verified against the org before committing, closing two open decisions:
- no branch anywhere pins `sync` or `claude` as a required status check, so the
  `sync` -> `sync / sync` context rename breaks nothing (all 42 protected
  branches across 15 repos checked, both contexts[] and the newer checks[], plus
  org and repo rulesets)
- the wave is 18 repo@branch pairs across 11 repos, not 21 — 21 is the repin
  target list, which additionally counts three stub-only repos carrying neither
  full workflow. claude.yml sits on exactly the same 18 pairs.

Decisions recorded in the scope docs: identity unification deferred, and the
claude.yml half of this conversion tabled pending the OIDC spike.
@mcarter-astronautdev mcarter-astronautdev changed the title docs: scope the claude.yml + bonsai-status-sync reusable conversion bonsai-status-sync reusable (phase 1) + refreshed conversion/identity scopes Aug 2, 2026
@mcarter-astronautdev

Copy link
Copy Markdown
Member Author

@claude please run /code-review:code-review

…-first

The two-leg pilot section was written for claude.yml, whose public-first order
follows from its App-token path and the persist-credentials private-fetch 403.
Neither exists in bonsai-status-sync.yml. What is untested for that file is a
private consumer resolving a public cross-repo reusable, and 10 of the 11
targets are private — so the private leg is the one that can actually fail.
@mcarter-astronautdev

Copy link
Copy Markdown
Member Author

Code review

First off — this is a genuinely careful PR. I checked the load-bearing claims rather than taking them on faith, and they hold up: the reusable's jobs: body really is byte-identical to the old per-repo copy except for the one added comment, the sha256 cited for the still-live template matches, the v1.9.0 DRIVER_ENGINEERING_APP_* rename is swept through cleanly with no stale old names, and BONSAI_BEARER_TOKEN is correctly declared required: true with no undeclared secrets or unused inputs. Deferring the caller stub until a tag exists is the right call, and adding the placeholder-pin guard to lint.yml in the same breath is a nice touch — it closes the exact silent-failure hole that deferral opens.

Found 3 issues, all documentation accuracy rather than behaviour:

1. The scope doc's decision banner says the caller stub already shipped, but it didn't — and the same doc says so 340 lines later.

The banner states, present tense, that "the 190-line per-repo copy is now a 51-line caller stub," and the Why section repeats it as "bonsai-status-sync.yml is now a 51-line stub calling a central reusable."

>
> **APPROVED and IN PROGRESS — `bonsai-status-sync.yml` (Phases 1–3).** The copy-per-repo cost is real
> and this half was never gated on anything. Phase 1 landed 2026-08-02: the reusable is
> `.github/workflows/bonsai-status-sync.yml`, the 190-line per-repo copy is now a 51-line caller stub.
>

*As of the Phase 1 conversion (2026-08-02) this argument is half-resolved: `bonsai-status-sync.yml` is now a
51-line stub calling a central reusable. **`claude.yml`'s 460 lines remain copied** — still the single largest
thing in the kit and its only remaining drift surface. The original framing follows.*

That's contradicted by the doc's own Phase 1 landed section — "Deliberately NOT shipped: the caller stub. … The kit therefore still installs the 190-line copy, which still works, until the repin" — and by ground truth: templates/github/bonsai-status-sync.yml is untouched by this PR and still 190 lines with no uses: in it.

**Deliberately NOT shipped: the caller stub.** A new reusable's stub cannot be pinned until the tag containing
that reusable exists, so it lands in step 2 of the release order, not here. This is the house precedent —
`dependabot-keep-current`'s reusable landed in `c362604` and its stub arrived later already carrying a real
SHA. The kit therefore still installs the 190-line copy, which still works, until the repin.

This one matters more than a normal doc typo because this document is the sequencing record a future session executes the wave from, and the banner is the first thing that session reads. README.md gets it right ("the kit still installs the 190-line copy, and swaps to a thin stub at the next repin") — the banner should match the README, not lead it.

Same root cause one file over: the new reusable's header says "Callers install the stub from templates/github/", which today sends a reader to the full 190-line copy rather than a stub.

# endpoint — serving AI-opened and human-opened PRs alike.
# Holds BONSAI_BEARER_TOKEN. Callers install the stub from templates/github/.
#

2. The identity doc's occurrence count for 261291955 is now short by a whole file — and it's this PR's own new file that's missing.

The doc states "all 11 occurrences of 261291955 — across README.md, templates/github/claude.yml, bonsai-status-sync.yml and ticketed-review.yml".

>
> Note `209825114` currently appears **nowhere** in this repo (`git grep` → zero hits), while all **11**
> occurrences of `261291955` — across `README.md`, `templates/github/claude.yml`, `bonsai-status-sync.yml` and
> `ticketed-review.yml` — are bound to `driver-digital-agents`. The overlap wave introduces the first use of
> the `claude[bot]` id, and removes it again at the end.

That count was correct before this PR, but the new .github/workflows/bonsai-status-sync.yml carries the actor gate too, adding two more occurrences at lines 131 and 141 — so the real number as of this commit is 13 across 5 files. The paragraph exists specifically to hand the identity-unification project a complete list of edit sites, so an undercount here is the kind of thing that leaves one file un-migrated. Worth path-qualifying the bare bonsai-status-sync.yml in that list too, since the doc's own preamble makes a point of path-qualifying everything else precisely because that filename now exists in both directories with different contents.

3. Stale line citation in the new reusable's header.

The "REUSABLE CONTEXT SEMANTICS" block points at :110-113 for the gh pr view / gh issue view reads, but those are at lines 115 and 118 — 110-113 is the set -e error-handling comment block, which contains no gh call at all. Inserting the header shifted everything down and the self-citation didn't move with it.

# (issues / pull_request / pull_request_review) reads it directly and is unaffected.
# • `github.token` — the CALLER repo's installation token, so the `gh pr view` /
# `gh issue view` reads at :110-113 resolve against the caller's repo, as before.
# • `vars.BONSAI_URL` — resolves against the CALLER's repository variables, so a per-repo tunnel

Small, but this repo leans hard on line citations as navigation, and the PR body itself makes re-verifying citations a selling point.


Two things I checked and deliberately did not flag: the gh pr view / gh issue view calls have no timeout wrapper and the job has no timeout-minutes:, which echoes guidance from #19 and #24 — but the old per-repo copy has exactly the same shape, and so do pr-first-review.yml and ticketed-review.yml, so that's a pre-existing repo-wide pattern rather than something this PR introduces. Worth its own pass sometime, not this one. And the if: guard on the update step reads as an always-truthy string at a glance, but it contains operators, so Actions evaluates it as a real expression — not a bug.

- reusable-conversion-scope.md: the decision banner and the Why section both
  claimed the 190-line kit copy "is now a 51-line caller stub". It isn't — a
  stub can't be pinned until a tag containing the reusable exists, which the
  doc's own "Phase 1 landed" section says 340 lines further down. The banner
  now agrees with that section and with the README.

- identity-unification-scope.md: the 261291955 edit-site list read 11 across
  4 files. The Phase 1 reusable carries the actor gate too, so it's 13 across
  5. Recounted against this branch, path-qualified (the basename now exists in
  both halves), and the 209825114 "zero hits" claim scoped to non-docs, since
  it now appears in that doc itself.

- bonsai-status-sync.yml: header cited :110-113 for the gh pr view / gh issue
  view reads, which are at :115-118 (110-113 is the set -e comment block); and
  said callers install a stub from templates/github/ that doesn't exist yet.

Both workflow edits are line-count preserving, so the :115-118 citation and
the identity doc's :139 / :141 moving-target cites stay valid. actionlint 1.7.12
clean across .github/workflows/ and templates/github/.
@mcarter-astronautdev
mcarter-astronautdev merged commit b394c6d into main Aug 2, 2026
2 checks passed
mcarter-astronautdev added a commit that referenced this pull request Aug 2, 2026
…y tripwire (v1.11.0)

Step 2 of the release order for the bonsai-status-sync conversion, plus the two kit content changes that ship with it.

- templates/github/bonsai-status-sync.yml: 190-line per-repo copy replaced by a 67-line caller stub. Completes the Phase 1 conversion started in #21.
- All six stubs repinned. Note they jump v1.8.0 -> v1.10.0: v1.9.0 never got a kit repin commit while the 2026-08-01 wave repinned the fleet anyway, so the fleet had been a release ahead of the kit templates. Closed here.
- DRIVER_AGENTS_REF -> 4d63371 in claude.yml and shopify-tool-smoke.yml (lockstep). The old pin 0bbb125 predated graphql_guard.py, so fleet runners had no fail-closed allowlist on destructive Shopify mutations.
- The Shopify operator tripwire in claude.yml's static system prompt, pairing with that wrapper.
- lint.yml: new guard asserting the system prompt survives shell-quote tokenization.
- Both scope docs de-staled and every claude.yml citation re-anchored by content.

Reviewed across two multi-agent passes; all findings above the bar applied. actionlint and the new tokenization guard green.

Ships as v1.11.0 — tagged at this merge commit, then the six stubs repin to that SHA, then one wave to 18 repo@branch pairs. The fleet is NOT waved by this merge.
@mcarter-astronautdev
mcarter-astronautdev deleted the docs/reusable-conversion-scope branch September 9, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant