From 75e75b8e27ff51ca38439ae966fffb695d6a22d5 Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 23 Jun 2026 14:08:53 -0500 Subject: [PATCH 1/6] Add Crashlytics auto-triage: cloud function, GitHub Action, docs (DRAFT) New fatal Crashlytics issue -> Cloud Function files a GitHub issue (labeled crashlytics-auto) -> GitHub Action has Claude read the crash via the Firebase MCP server, post a root-cause comment, and open a DRAFT PR when confident. Nothing is auto-merged. Not yet enabled; pending secrets/setup per docs. - functions/index.js + package.json: onNewFatalIssuePublished handler - firebase.json: declares the crashlytics-autotriage functions codebase - .github/workflows/crashlytics-autotriage.yml: label-gated triage workflow - .github/firebase-mcp.json: Firebase MCP server config for the action - docs/PRD-crashlytics-autotriage.md: design + rollout plan - docs/FIREBASE.md: runbook (token-expiry is the first thing to check) --- .github/firebase-mcp.json | 9 ++ .github/workflows/crashlytics-autotriage.yml | 97 ++++++++++++++++++ firebase.json | 9 ++ functions/index.js | 101 +++++++++++++++++++ functions/package.json | 12 +++ 5 files changed, 228 insertions(+) create mode 100644 .github/firebase-mcp.json create mode 100644 .github/workflows/crashlytics-autotriage.yml create mode 100644 firebase.json create mode 100644 functions/index.js create mode 100644 functions/package.json diff --git a/.github/firebase-mcp.json b/.github/firebase-mcp.json new file mode 100644 index 0000000..108d027 --- /dev/null +++ b/.github/firebase-mcp.json @@ -0,0 +1,9 @@ +{ + "_comment": "DRAFT — Firebase MCP server config for the crashlytics-autotriage workflow. See docs/PRD-crashlytics-autotriage.md. Requires Application Default Credentials (provided by google-github-actions/auth) with roles/firebasecrashlytics.viewer.", + "mcpServers": { + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp", "--dir", ".", "--project", "openloop-8c266"] + } + } +} diff --git a/.github/workflows/crashlytics-autotriage.yml b/.github/workflows/crashlytics-autotriage.yml new file mode 100644 index 0000000..c7bce33 --- /dev/null +++ b/.github/workflows/crashlytics-autotriage.yml @@ -0,0 +1,97 @@ +name: Crashlytics auto-triage + +# ============================================================================ +# DRAFT — NOT YET ENABLED. Pending owner sign-off of docs/PRD-crashlytics-autotriage.md. +# Do not enable until the secrets / service account in the PRD exist. +# +# Trigger: a Crashlytics-sourced GitHub issue (created by the Cloud Function in functions/) +# carrying the `crashlytics-auto` label is opened. Claude reads the crash via the Firebase +# MCP server, posts a root-cause comment, and — when confident — opens a DRAFT PR with a +# first-pass fix. Nothing is ever auto-merged. See the PRD for the full design. +# ============================================================================ + +on: + issues: + types: [opened, labeled] + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to triage manually (Phase 0 testing)" + required: true + +permissions: + contents: write # create branch + commit a candidate fix + pull-requests: write # open the draft PR + issues: write # comment / label the issue + id-token: write # service-account / WIF auth to Google Cloud + +concurrency: + group: crashlytics-triage-${{ github.event.issue.number || github.event.inputs.issue_number }} + cancel-in-progress: false + +jobs: + triage: + # Only run for Crashlytics-sourced issues (label gate) or an explicit manual dispatch, + # so human-filed issues never spend tokens. + if: > + github.event_name == 'workflow_dispatch' || + (github.event.action == 'opened' && + contains(join(github.event.issue.labels.*.name, ','), 'crashlytics-auto')) || + (github.event.action == 'labeled' && github.event.label.name == 'crashlytics-auto') + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} # for the `gh` CLI used by Claude + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # Lets the Firebase MCP server read Crashlytics. google-github-actions/auth + # exports GOOGLE_APPLICATION_CREDENTIALS, which firebase-tools picks up as + # Application Default Credentials. + # + # Phase 0 (simple): a service-account key JSON stored as the secret GCP_SA_KEY. + # More secure later: swap credentials_json for keyless Workload Identity Federation: + # with: + # workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + # service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Triage the crash with Claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + You are triaging an automated Crashlytics crash report filed as GitHub issue + #${{ github.event.issue.number || github.event.inputs.issue_number }} in stozo04/OpenLoop. + + Follow this repo's CLAUDE.md and docs/DEFINITION_OF_DONE.md. + + Steps: + 1. Read the issue: `gh issue view ${{ github.event.issue.number || github.event.inputs.issue_number }}`. + Extract the `Crashlytics-Issue-ID:` value from the body. + 2. Use the Firebase MCP Crashlytics tools (crashlytics_get_issue, crashlytics_list_events, + crashlytics_batch_get_events) to fetch the stacktrace and a few sample events for that + issue ID. If an MCP call fails (the tools are Experimental), proceed with whatever + metadata is in the issue body and say so. + 3. Read the relevant source and docs/lessons_learned/ (several past lessons are + Crashlytics-sourced) to locate the likely cause. + 4. ALWAYS post a root-cause comment on the issue with: the failing stack frame, the most + likely cause, and the file(s) involved. Use `gh issue comment`. + 5. Decision: + - HIGH confidence (you can point to a specific cause in the code): create a branch + `feature/crashlytics-`, make the MINIMAL fix, push, and open a **DRAFT** PR + with `gh pr create --draft`. The PR body must say "Fixes #", list which + DEFINITION_OF_DONE steps were NOT run on CI (release build, emulator run + screenshot, + instrumented tests), and include the manual QA checklist. NEVER mark it ready; NEVER merge. + - LOW confidence (OEM-specific, OOM, third-party SDK, or unclear): do NOT write code. + Add the `needs-human` label (`gh issue edit ... --add-label needs-human`) and explain why. + 6. Be honest about uncertainty. A wrong patch that looks authoritative is worse than none. + claude_args: | + --mcp-config .github/firebase-mcp.json + --allowedTools "mcp__firebase__crashlytics_get_issue,mcp__firebase__crashlytics_list_events,mcp__firebase__crashlytics_batch_get_events,mcp__firebase__crashlytics_get_report,mcp__firebase__crashlytics_list_notes,Bash,Read,Edit,Write,Glob,Grep" + --max-turns 25 + --model claude-sonnet-4-6 diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..e9b91f5 --- /dev/null +++ b/firebase.json @@ -0,0 +1,9 @@ +{ + "functions": [ + { + "source": "functions", + "codebase": "crashlytics-autotriage", + "ignore": ["node_modules", ".git", "firebase-debug.log", "*.local"] + } + ] +} diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 0000000..c6b7d23 --- /dev/null +++ b/functions/index.js @@ -0,0 +1,101 @@ +"use strict"; + +/** + * DRAFT — pending sign-off. See docs/PRD-crashlytics-autotriage.md. + * + * On a new FATAL Crashlytics issue, create (exactly once) a GitHub issue labeled + * `crashlytics-auto`. A GitHub Action (.github/workflows/crashlytics-autotriage.yml) + * then has Claude triage it and, when confident, open a draft fix PR. + * + * Runtime: Node 22, firebase-functions v6 (2nd gen). Uses the global `fetch` (Node 18+), + * so there are no extra HTTP dependencies. + */ + +const {onNewFatalIssuePublished} = require("firebase-functions/v2/alerts/crashlytics"); +const {defineSecret} = require("firebase-functions/params"); +const logger = require("firebase-functions/logger"); + +// Fine-grained PAT (Issues: read & write on stozo04/OpenLoop) or a GitHub App token. +const GITHUB_TOKEN = defineSecret("GITHUB_TOKEN"); + +const REPO = "stozo04/OpenLoop"; +const FIREBASE_PROJECT = "openloop-8c266"; +const APP_ID = "android:io.github.stozo04.openloop"; +const LABEL = "crashlytics-auto"; + +/** Standard GitHub REST headers. */ +function ghHeaders(token) { + return { + "Authorization": `Bearer ${token}`, + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "openloop-crashlytics-autotriage", + }; +} + +/** Has an issue already been filed for this Crashlytics issue ID? (idempotency) */ +async function issueAlreadyFiled(token, crashId) { + const q = `repo:${REPO} label:${LABEL} "Crashlytics-Issue-ID: ${crashId}"`; + const url = `https://api.github.com/search/issues?q=${encodeURIComponent(q)}`; + const res = await fetch(url, {headers: ghHeaders(token)}); + if (!res.ok) { + logger.warn(`Dedupe search failed (${res.status}); proceeding to create.`); + return false; + } + const body = await res.json(); + return (body.total_count || 0) > 0; +} + +exports.crashlyticsToGithub = onNewFatalIssuePublished( + {secrets: [GITHUB_TOKEN], region: "us-central1"}, + async (event) => { + const issue = (event.data && event.data.payload && event.data.payload.issue) || {}; + const crashId = issue.id || "unknown"; + const title = issue.title || "Unknown crash"; + const subtitle = issue.subtitle || ""; + const appVersion = issue.appVersion || "unknown"; + const token = GITHUB_TOKEN.value(); + + if (await issueAlreadyFiled(token, crashId)) { + logger.info(`Issue for Crashlytics ${crashId} already exists; skipping.`); + return; + } + + const consoleUrl = + `https://console.firebase.google.com/project/${FIREBASE_PROJECT}` + + `/crashlytics/app/${APP_ID}/issues/${crashId}`; + + const body = [ + "**Automated triage** — a new fatal crash was reported by Firebase Crashlytics.", + "", + `- **Crashlytics-Issue-ID:** ${crashId}`, + `- **Title:** ${title}`, + `- **Subtitle:** ${subtitle}`, + `- **First seen app version:** ${appVersion}`, + `- **Console:** ${consoleUrl}`, + "", + "---", + "", + `@claude please triage this crash. Use the Firebase MCP Crashlytics tools to fetch the`, + `stacktrace and sample events for issue \`${crashId}\`, consult \`docs/lessons_learned/\`,`, + "and post a root-cause comment. If you can point to a specific cause in the code, open a", + "**draft** PR with a minimal fix per `docs/DEFINITION_OF_DONE.md` (note CI could not run", + "the emulator). Otherwise label `needs-human` and explain. Never auto-merge.", + ].join("\n"); + + const res = await fetch(`https://api.github.com/repos/${REPO}/issues`, { + method: "POST", + headers: {...ghHeaders(token), "Content-Type": "application/json"}, + body: JSON.stringify({title: `[Crashlytics] ${title}`, body, labels: [LABEL, "bug"]}), + }); + + if (!res.ok) { + const text = await res.text(); + logger.error(`GitHub issue creation failed: ${res.status} ${text}`); + throw new Error(`GitHub API ${res.status}`); + } + + const created = await res.json(); + logger.info(`Created GitHub issue #${created.number} for Crashlytics ${crashId}.`); + }, +); diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 0000000..aa9b483 --- /dev/null +++ b/functions/package.json @@ -0,0 +1,12 @@ +{ + "name": "openloop-crashlytics-autotriage", + "description": "DRAFT Cloud Function: new fatal Crashlytics issue -> GitHub issue. See docs/PRD-crashlytics-autotriage.md", + "private": true, + "main": "index.js", + "engines": { + "node": "22" + }, + "dependencies": { + "firebase-functions": "^6.0.0" + } +} From 1b4fd3502325dbdb5271fad36eea8a56a96dc611 Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 23 Jun 2026 14:10:08 -0500 Subject: [PATCH 2/6] docs: add Crashlytics auto-triage PRD and Firebase runbook - docs/PRD-crashlytics-autotriage.md: problem, architecture, rollout, risks - docs/FIREBASE.md: operational runbook; GITHUB_TOKEN 1-year expiry is the first thing to check if auto-triage goes quiet (~June 2027) --- docs/FIREBASE.md | 82 +++++++++++++++++ docs/PRD-crashlytics-autotriage.md | 137 +++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 docs/FIREBASE.md create mode 100644 docs/PRD-crashlytics-autotriage.md diff --git a/docs/FIREBASE.md b/docs/FIREBASE.md new file mode 100644 index 0000000..fde2f23 --- /dev/null +++ b/docs/FIREBASE.md @@ -0,0 +1,82 @@ +# Firebase — Crashlytics auto-triage runbook + +Operational notes for the Crashlytics → GitHub → Claude auto-triage system. Design and rationale live in [`PRD-crashlytics-autotriage.md`](PRD-crashlytics-autotriage.md); this file is the day-to-day "what is it and how do I fix it" reference. + +**Firebase project:** `openloop-8c266` · **Repo:** `stozo04/OpenLoop` · **Plan:** Blaze (required for the alert function) + +--- + +## ⚠️ FIRST THING TO CHECK IF IT STOPS WORKING: the GitHub token expires + +The `GITHUB_TOKEN` secret the Cloud Function uses to file GitHub issues is a **fine-grained Personal Access Token created with a 1-year expiration** (created June 2026 → **expires around June 2027** — confirm the exact date at ). + +**When that token expires, crashes will silently stop becoming GitHub issues.** No crash, no error popup — the function just starts getting `401 Unauthorized` from GitHub and logs it. So if auto-triage goes quiet, **check the token first.** + +### How to renew the token (2 minutes) + +1. → create a new fine-grained token. + - Resource owner `stozo04`, repository **OpenLoop** only, permission **Issues: Read and write**. +2. Update the secret (paste the new token at the hidden prompt — the argument is the *name*, not the value): + + ``` + firebase functions:secrets:set GITHUB_TOKEN + ``` + +3. Re-deploy so the function binds the new secret version: + + ``` + firebase deploy --only functions:crashlytics-autotriage + ``` + +> Never paste a token into a terminal command, a file, or a chat. Only into the hidden `Enter a value for GITHUB_TOKEN:` prompt. If a token is ever exposed, revoke it immediately and issue a new one. + +--- + +## What this system is + +1. A new **fatal** Crashlytics issue fires a Firebase Alert. +2. The Cloud Function `crashlyticsToGithub` (`functions/index.js`, trigger `onNewFatalIssuePublished`) creates one GitHub issue labeled `crashlytics-auto`. +3. The GitHub Action `.github/workflows/crashlytics-autotriage.yml` wakes Claude, which reads the crash via the Firebase MCP server, comments a root cause, and opens a **draft** PR when confident. Nothing is auto-merged. + +## Files + +| File | Role | +|------|------| +| `functions/index.js` | The alert listener that files the GitHub issue | +| `functions/package.json` | Node 22 runtime + `firebase-functions` v6 | +| `firebase.json` | Declares the function under codebase `crashlytics-autotriage` | +| `.github/workflows/crashlytics-autotriage.yml` | The triage workflow (runs on GitHub) | +| `.github/firebase-mcp.json` | Firebase MCP server config for the workflow | + +## Secrets & access (where each one lives) + +| Name | Lives in | Purpose | Expires? | +|------|----------|---------|----------| +| `GITHUB_TOKEN` | Firebase (Secret Manager) | Function files GitHub issues | **Yes — ~June 2027** (see above) | +| `ANTHROPIC_API_KEY` | GitHub repo Actions secrets | Runs Claude | No (unless rotated) | +| `GCP_SA_KEY` | GitHub repo Actions secrets | Lets the workflow read Crashlytics | No expiry, but key can be rotated | +| Service account `gh-crashlytics-reader` | Google Cloud IAM | Holds role `Firebase Crashlytics Viewer` | — | + +## Common commands + +``` +firebase functions:list # is the function deployed? +firebase deploy --only functions:crashlytics-autotriage # (re)deploy +firebase functions:secrets:set GITHUB_TOKEN # set/rotate the token (paste at hidden prompt) +firebase functions:secrets:access GITHUB_TOKEN # confirm a value exists (does not print it) +``` + +To read function logs (e.g. to see a `401` from an expired token): Firebase Console → Functions → `crashlyticsToGithub` → Logs, or Google Cloud Console → Logging. + +## Troubleshooting — in priority order + +1. **No new GitHub issues from crashes?** → **Token expired** (most likely). Renew it (top of this doc). Then check the function logs for `401`. +2. **Function not firing at all?** → Confirm it's deployed (`firebase functions:list`) and the project is still on the **Blaze** plan. +3. **Issue created but no Claude comment / draft PR?** → Check the **Actions** tab. Likely a missing/expired `ANTHROPIC_API_KEY`, the Claude GitHub App was removed, or `GCP_SA_KEY` lost its Crashlytics access. +4. **Claude can't read the crash details?** → The Crashlytics MCP tools are **Experimental** and can change; also verify the service account still has `Firebase Crashlytics Viewer`. +5. **Duplicate issues for one crash?** → The function dedupes on the `Crashlytics-Issue-ID:` marker; if the issue body format changed, the dedupe search can miss. + +## Notes + +- Crashlytics MCP tooling is **Experimental** at Google (no SLA). The function is built so a crash still becomes a GitHub issue even if the MCP/detective half breaks. +- The bot only ever opens **draft** PRs and never merges — a human always reviews. diff --git a/docs/PRD-crashlytics-autotriage.md b/docs/PRD-crashlytics-autotriage.md new file mode 100644 index 0000000..2464912 --- /dev/null +++ b/docs/PRD-crashlytics-autotriage.md @@ -0,0 +1,137 @@ +# PRD — Crashlytics auto-triage → GitHub issue → Claude draft PR + +**Status:** Draft, awaiting owner sign-off. No code in this PRD is deployed or wired up yet. +**Owner:** Steven Gates · **Firebase project:** `openloop-8c266` · **Repo:** `stozo04/OpenLoop` +**Related:** [`DEFINITION_OF_DONE.md`](DEFINITION_OF_DONE.md), [`lessons_learned/024-fgs-type-constant-api-gating.md`](lessons_learned/024-fgs-type-constant-api-gating.md) (a Crashlytics-sourced fix — the workflow this PRD describes would have filed it automatically). + +> **Experimental dependency.** The Crashlytics MCP tools (via the Firebase MCP server) are flagged **Experimental** by Google — no SLA, may change in backward-incompatible ways. This design treats them as best-effort and degrades gracefully if a tool call fails. + +--- + +## 1. Problem statement + +OpenLoop is live in Production and reachable by a very large user base. New fatal crashes surface in Firebase Crashlytics, but turning a crash into a tracked, diagnosed, and fixed issue is entirely manual today: someone has to notice the crash, open the console, read the stacktrace, file a GitHub issue, find the offending code, and write a fix. That latency and manual toil means real crashes sit unaddressed. + +We want a crash to **automatically** become a tracked GitHub issue with a Claude-authored root-cause diagnosis, and — when Claude is confident — a **draft** pull request with a first-pass fix, so the owner starts from a diagnosis and a candidate patch instead of a blank page. + +## 2. Goals & non-goals + +**Goals (first milestone — "Triage + draft PR"):** + +- A new *fatal* Crashlytics issue auto-creates exactly one GitHub issue, labeled `crashlytics-auto`, containing crash metadata and a console deep link. +- Claude (via the GitHub Action) pulls the full stacktrace and sample events through the Firebase MCP server, reads the codebase and `docs/lessons_learned/`, and posts a root-cause comment. +- When confidence is high, Claude opens a **draft** PR with a minimal fix on a `feature/crashlytics-` branch, linking the issue. When not, it labels the issue `needs-human` and explains why. +- Nothing is ever auto-merged. A human always reviews. + +**Non-goals (explicitly out of scope for this milestone):** + +- Auto-merging fixes, or treating the draft PR as "Ready for PR" per [`DEFINITION_OF_DONE.md`](DEFINITION_OF_DONE.md). See §7. +- Acting on non-fatal issues, ANRs, or velocity alerts (deferred to a later milestone). +- Running the full DoD gate (emulator boot + screenshot + release build) on the CI runner — the runner cannot do this cheaply or reliably for Android. The human completes the gate. + +## 3. Success criteria + +- **Coverage:** ≥ 95% of new fatal issues produce a GitHub issue within ~5 minutes, with no duplicates. +- **Diagnosis quality:** On a hand-labeled sample of the first 15 issues, the owner rates Claude's root-cause comment "useful" (correct or a reasonable lead) ≥ 70% of the time before draft-PR generation is trusted. +- **Safety:** Zero auto-merges; zero non-draft PRs created by the bot; zero issues created for the same Crashlytics issue twice. +- **Cost:** Bounded by `--max-turns` and label-gated triggering; no runaway Action minutes or token spend. + +## 4. Architecture + +``` +Crashlytics (new fatal issue) + │ Firebase Alerts: crashlytics.newFatalIssue + ▼ +Cloud Function v2 onNewFatalIssuePublished (functions/index.js) + │ dedupe → POST /repos/stozo04/OpenLoop/issues (label: crashlytics-auto) + ▼ +GitHub Issue (#N) — crash metadata + console deep link + @claude instructions + │ on: issues [opened], if label == crashlytics-auto + ▼ +GitHub Action crashlytics-autotriage.yml + │ - checkout repo + │ - auth to Google → Firebase MCP can read Crashlytics + │ - anthropics/claude-code-action@v1 with Firebase MCP config + prompt + ▼ +Claude: crashlytics_get_issue / list_events (stacktrace) → read code + lessons_learned + ├─ always: post root-cause comment on the issue + ├─ high confidence: open DRAFT PR (feature/crashlytics-) with minimal fix + └─ low confidence: label needs-human + explain + ▼ +Human (Steven): run the full DoD gate, then mark ready / merge — or reject +``` + +### 4.1 Components + +| Component | File | Responsibility | +|-----------|------|----------------| +| Crash trigger | `functions/index.js` | `onNewFatalIssuePublished` handler: dedupe, format, create GitHub issue | +| Functions manifest | `functions/package.json` | Node 22 runtime, `firebase-functions` v6 | +| Firebase deploy config | `firebase.json` | New file — declares the functions codebase (`crashlytics-autotriage`) | +| Triage workflow | `.github/workflows/crashlytics-autotriage.yml` | Trigger on labeled issue, auth, run Claude action | +| MCP config | `.github/firebase-mcp.json` | Declares the Firebase MCP server for the action | + +### 4.2 Auth & secrets + +| Secret / config | Where | Purpose | +|-----------------|-------|---------| +| `GITHUB_TOKEN` (fine-grained PAT or GitHub App token) | Cloud Function secret (Secret Manager) | Function creates issues in `stozo04/OpenLoop` (Issues: read & write) | +| `ANTHROPIC_API_KEY` | Repo Actions secret | Claude Code Action | +| `GCP_SA_KEY` (service-account key JSON) or Workload Identity Federation | Repo Actions secrets | Lets the runner's Firebase MCP read Crashlytics | +| Crashlytics read role | Service account IAM | `roles/firebasecrashlytics.viewer` (add `...writer` only if we later let Claude post Crashlytics notes / close issues) | + +For Phase 0 the simple path is a service-account key JSON stored as `GCP_SA_KEY`. The more secure long-term option is **Workload Identity Federation** (no downloadable key); the workflow keeps that as a documented swap. + +## 5. Behavior details + +- **Dedupe:** before creating an issue, the function searches existing `crashlytics-auto` issues for the Crashlytics issue ID marker (`Crashlytics-Issue-ID: ` in the body). If found, it skips instead of creating a duplicate. +- **Trigger scope:** the workflow runs only when an opened issue carries the `crashlytics-auto` label, so human-filed issues never spend tokens. +- **Tool allowlist:** Claude is given the Crashlytics **read** MCP tools plus normal file/git tools. Crashlytics write tools (`crashlytics_update_issue`, `crashlytics_create_note`) are withheld in this milestone. +- **Confidence gate:** the prompt instructs Claude to open a draft PR only when it can point to a specific cause in the code; otherwise comment + `needs-human`. +- **No loops:** the bot never opens issues from the Action, only PRs — so it can't retrigger itself. + +## 6. Rollout plan + +1. **Phase 0 — issue-only (recommended first):** deploy the function; let it create issues. Manually run the Claude action on a couple of them via `workflow_dispatch` to sanity-check diagnosis quality. No auto draft-PRs yet. +2. **Phase 1 — auto draft-PR:** enable the `issues: [opened]` trigger and draft-PR generation once §3 diagnosis-quality bar is met. +3. **Phase 2 (future):** consider velocity alerts, ANRs, and (separately decided) letting Claude post a Crashlytics note linking the PR. + +## 7. The Definition-of-Done tension (decision required) + +[`DEFINITION_OF_DONE.md`](DEFINITION_OF_DONE.md) states the **Production zero-error rule**: a PR is opened only from a fully green state (clean debug + release build, 0 test failures, 0 new lint errors, app run on an emulator with a screenshot). **A CI runner cannot satisfy that gate for an Android app** — no emulator boot + screenshot per crash, cheaply or reliably. + +This design resolves the tension by making the bot's output a **draft triage PR**, explicitly *not* "Ready for PR": + +- The PR is opened as a **draft**, labeled `crashlytics-auto`, with a body that states plainly which DoD steps were **not** run on CI and includes the manual QA checklist. +- It is a *starting point*. Steven runs the full DoD gate locally and only then marks it ready / merges. + +**Decision needed from the owner:** do we formally amend `DEFINITION_OF_DONE.md` to recognize "automated triage draft" as a distinct, non-mergeable state (so the bot isn't seen as violating the zero-error rule)? Recommended: yes — a short subsection that says triage drafts are exempt *because* they can never be merged without a human clearing the full gate. + +## 8. Risks & mitigations + +| Risk | Mitigation | +|------|-----------| +| Crashlytics MCP is Experimental and may break | Degrade gracefully; function-created issue still lands even if MCP calls fail | +| Issue spam / token burn on crash storms | Gate on `newFatalIssue` only; dedupe; `--max-turns` cap; label-scoped trigger | +| Unfixable crashes (OEM, OOM, third-party SDK) | Confidence gate → comment + `needs-human` instead of a bad patch | +| Bot opens a low-quality patch that looks authoritative | Always **draft**; never auto-merge; human owns the DoD gate | +| New `firebase.json` changes `firebase deploy` behavior | Function isolated in its own codebase name; documented in setup checklist | +| Secret leakage | Least-privilege IAM; all tokens in Secret Manager / Actions secrets; WIF available as a keyless upgrade | + +## 9. Open questions + +1. Amend `DEFINITION_OF_DONE.md` for the triage-draft state (§7)? Recommend yes. +2. GitHub auth for the function: fine-grained PAT (simple) vs. a GitHub App (cleaner attribution, more setup). Recommend PAT for v1. +3. Should Claude post a Crashlytics note linking the PR (needs the writer role + write tools)? Default: no, in v1. +4. Region for the function (`us-central1` default) and whether to also handle `onNewNonfatalIssuePublished` later. +5. Confidence threshold wording — tune the prompt after the first batch of real issues. + +## 10. Setup checklist (owner actions, after sign-off) + +- [ ] Decide §7 and §9 open questions. +- [ ] Create the GitHub label `crashlytics-auto` (and `needs-human`). +- [ ] Create a fine-grained PAT (Issues: R/W on `stozo04/OpenLoop`) → store as Cloud Function secret `GITHUB_TOKEN`. +- [ ] Add `ANTHROPIC_API_KEY` to repo Actions secrets. +- [ ] Create a service account with `roles/firebasecrashlytics.viewer`; store its key as the repo secret `GCP_SA_KEY` (or configure Workload Identity Federation). +- [ ] Review the new `firebase.json` and `functions/`; `firebase deploy --only functions:crashlytics-autotriage`. +- [ ] Run the Claude workflow via `workflow_dispatch` on a test issue (Phase 0) before enabling the auto trigger. From 9ea82b5be61225c17b8181ea642165e45817f5c9 Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 23 Jun 2026 14:13:55 -0500 Subject: [PATCH 3/6] docs: add Firebase project console link to FIREBASE.md --- docs/FIREBASE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/FIREBASE.md b/docs/FIREBASE.md index fde2f23..bfa1678 100644 --- a/docs/FIREBASE.md +++ b/docs/FIREBASE.md @@ -3,6 +3,7 @@ Operational notes for the Crashlytics → GitHub → Claude auto-triage system. Design and rationale live in [`PRD-crashlytics-autotriage.md`](PRD-crashlytics-autotriage.md); this file is the day-to-day "what is it and how do I fix it" reference. **Firebase project:** `openloop-8c266` · **Repo:** `stozo04/OpenLoop` · **Plan:** Blaze (required for the alert function) +**Project Console:** --- From 3d509d36fc6c91b464d5997a36ab20ccc2aa48e1 Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 23 Jun 2026 14:15:55 -0500 Subject: [PATCH 4/6] chore: gitignore Cloud Functions node_modules and firebase debug logs npm install in functions/ pulls thousands of files; they're rebuilt on demand and excluded from deploy via firebase.json, so they should never be tracked. --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 2d01cd5..6106ca6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,10 @@ docs/local/ # E2E loop source video — local test asset, never commit (see .claude/commands/e2e-fold-loop.md) google-pro-fold-video.mp4 + +# Cloud Functions (Firebase) — dependencies are regenerated by `npm install` and +# excluded from deploy via firebase.json; never commit them. +functions/node_modules/ +.firebase/ +firebase-debug.log +functions/firebase-debug.log From 341f656eb6024d02016150b1eb513c7bf2329b40 Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 23 Jun 2026 14:21:54 -0500 Subject: [PATCH 5/6] chore: gitignore the Google service-account key file Never commit the SA key JSON. Adds the specific filename plus defensive patterns (*service-account*.json, *-DO-NOT-COMMIT.*). --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 6106ca6..b422ea4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,8 @@ functions/node_modules/ .firebase/ firebase-debug.log functions/firebase-debug.log + +# Google service-account key (private credential) — NEVER commit +openloop-google-service-account-key-DO-NOT-COMMIT.json +*service-account*.json +*-DO-NOT-COMMIT.* From 8e4de0f4b7496e248f2423b048f37985b612aaf6 Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 23 Jun 2026 14:22:41 -0500 Subject: [PATCH 6/6] add gitignore changes --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 6106ca6..b422ea4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,8 @@ functions/node_modules/ .firebase/ firebase-debug.log functions/firebase-debug.log + +# Google service-account key (private credential) — NEVER commit +openloop-google-service-account-key-DO-NOT-COMMIT.json +*service-account*.json +*-DO-NOT-COMMIT.*