diff --git a/.github/workflows/first-tree-sync.yml b/.github/workflows/first-tree-sync.yml index 15418c5..39c85a9 100644 --- a/.github/workflows/first-tree-sync.yml +++ b/.github/workflows/first-tree-sync.yml @@ -83,6 +83,8 @@ jobs: - name: Run first-tree gardener env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GARDENER_CLASSIFIER_MODEL: ${{ secrets.GARDENER_CLASSIFIER_MODEL }} # Bypass gardener's `gh api user` self-identification call — # GITHUB_TOKEN can't resolve /user reliably. Hardcoding the # bot identity is enough for the self-loop guard. diff --git a/skills/first-tree/references/workflow-mode.md b/skills/first-tree/references/workflow-mode.md index f2ed311..8ffa16c 100644 --- a/skills/first-tree/references/workflow-mode.md +++ b/skills/first-tree/references/workflow-mode.md @@ -118,38 +118,39 @@ broad, user prefers a dedicated token), create a new fine-grained PAT: unset TOKEN ``` -## Step 3 — set the `ANTHROPIC_API_KEY` secret +## Step 3 — set the `CLAUDE_CODE_OAUTH_TOKEN` secret -`gardener comment` needs a classifier to produce a verdict. When -`ANTHROPIC_API_KEY` is unset, the CLI **refuses to post** (see PR #255) -rather than silently degrading to a hard-coded template. This is the -intended fail-closed behaviour for push mode. +The generated push-mode workflow installs the `claude` CLI and lets +`gardener comment` authenticate through `CLAUDE_CODE_OAUTH_TOKEN`. +Generate a long-lived token locally with: -If your shell already has `ANTHROPIC_API_KEY` exported, pipe it directly -into the repo secret without echoing it or pasting it into chat: +```bash +claude setup-token +``` + +Then save it on the codebase repo without echoing it into chat history: ```bash -printf '%s' "$ANTHROPIC_API_KEY" | gh secret set ANTHROPIC_API_KEY \ - --repo / \ - --body - +gh secret set CLAUDE_CODE_OAUTH_TOKEN \ + --repo / ``` -If the variable is not already exported locally, ask the user to paste -it privately into your terminal, then run: +Paste the token into the terminal prompt when `gh` asks for it. + +## Step 3b — optional `ANTHROPIC_API_KEY` fallback + +If you want CI to fall back to the API-key classifier path when Claude +Code auth is unavailable, also set `ANTHROPIC_API_KEY`: ```bash -printf '%s' "$TOKEN" | gh secret set ANTHROPIC_API_KEY \ +printf '%s' "$ANTHROPIC_API_KEY" | gh secret set ANTHROPIC_API_KEY \ --repo / \ --body - -unset TOKEN ``` -Never print the key, never write it to a file, and never paste it into -chat. - -The generated workflow also reads an optional `GARDENER_CLASSIFIER_MODEL` -secret if you need to pin a specific Anthropic model; omit it to use the -built-in default. +The generated workflow also reads an optional +`GARDENER_CLASSIFIER_MODEL` secret if you need to pin a specific model; +omit it to use the built-in default. ## Step 4 — commit and open a PR @@ -184,10 +185,10 @@ After the workflow PR merges: - **`TREE_REPO_TOKEN unset`** — secret not installed or scoped to a different repo. Re-run Step 2 with the correct `--repo`. -- **Low-signal `INSUFFICIENT_CONTEXT` review** — `ANTHROPIC_API_KEY` is - not available to the workflow job, so gardener fell back to the - default no-classifier path. Re-run Step 3 with the correct `--repo`, - or add the secret at the org/environment level used by this repo. +- **Low-signal `INSUFFICIENT_CONTEXT` review** — the workflow could not + authenticate a real classifier. Re-run Step 3 with the correct + `--repo`, or add the optional `ANTHROPIC_API_KEY` fallback from + Step 3b at the repo/org/environment level used by this repo. - **`tree-repo auth/access error (401/403/404)`** — PAT lacks `issues:write` or `contents:read` on the tree repo, or points at the wrong repo. Regenerate with the scopes in Step 2. diff --git a/skills/gardener/SKILL.md b/skills/gardener/SKILL.md index c708456..00610c2 100644 --- a/skills/gardener/SKILL.md +++ b/skills/gardener/SKILL.md @@ -111,13 +111,15 @@ Detected when the user hasn't given a tree slug, or the slug 404s. Step 0 returned ADMIN / MAINTAIN / WRITE for the source repo. -1. Confirm `ANTHROPIC_API_KEY` is available (ask user to paste into their - shell env if not already exported). +1. Confirm the user can generate `CLAUDE_CODE_OAUTH_TOKEN` locally via + `claude setup-token`. If they prefer the API-key path, confirm + `ANTHROPIC_API_KEY` is available instead. 2. `first-tree gardener install-workflow --tree-repo ` inside the codebase repo. 3. Walk through `skills/first-tree/references/workflow-mode.md` for - `TREE_REPO_TOKEN` + `ANTHROPIC_API_KEY` secret setup (audit-log - caveats must be surfaced before `gh secret set`). + `TREE_REPO_TOKEN` + `CLAUDE_CODE_OAUTH_TOKEN` secret setup + (`ANTHROPIC_API_KEY` stays an optional fallback; audit-log caveats + must be surfaced before `gh secret set`). 4. Open the workflow PR for human review. ### Scenario C — user does not own the codebase (pull mode) @@ -494,14 +496,14 @@ npx -p first-tree first-tree gardener install-workflow \ Set the `TREE_REPO_TOKEN` secret (see the workflow-mode reference for the quick `gh auth token` path and its caveats, or the scoped-PAT -fallback) **and** the `ANTHROPIC_API_KEY` secret on the codebase repo — -the generated workflow references both via -`secrets.ANTHROPIC_API_KEY` / `secrets.TREE_REPO_TOKEN` and falls back -to a skip if the classifier key is missing. `GARDENER_CLASSIFIER_MODEL` -is wired through as an optional secret; leave it unset to use the -default model. Commit the generated workflow file and open a PR. On -every PR merge thereafter the workflow files a tree-repo issue assigned -to the NODE owners. +fallback) **and** the `CLAUDE_CODE_OAUTH_TOKEN` secret on the codebase +repo. The generated workflow installs `claude`, authenticates it via +`CLAUDE_CODE_OAUTH_TOKEN`, and runs `first-tree gardener comment` on +every PR. `ANTHROPIC_API_KEY` / `GARDENER_CLASSIFIER_MODEL` remain +optional secrets if you want an API-key fallback or model override. +Commit the generated workflow file and open a PR. On every PR merge +thereafter the workflow files a tree-repo issue assigned to the NODE +owners. ### Respond to feedback on a sync PR @@ -564,13 +566,15 @@ The `modules..enabled: false` knob is the opt-out: gardener exits ## Environment -Gardener reads a small set of env vars. `ANTHROPIC_API_KEY` is required -for every `gardener comment` invocation; `TREE_REPO_TOKEN` is required -only for the merge→issue branch. +Gardener reads a small set of env vars. `CLAUDE_CODE_OAUTH_TOKEN` +authenticates the installed `claude` CLI in CI; `ANTHROPIC_API_KEY` +is the fallback classifier path when `claude` auth is unavailable; +`TREE_REPO_TOKEN` is required only for the merge→issue branch. | Variable | Purpose | |---|---| -| `ANTHROPIC_API_KEY` | **Required** for `gardener comment`. The stock CLI instantiates the built-in Anthropic classifier from this key; if it is unset, `runComment` fails closed and emits `BREEZE_RESULT: status=skipped summary=no classifier injected` without touching `gh`. Forwarded into the launchd plist by `gardener start`; referenced as `secrets.ANTHROPIC_API_KEY` by the push-mode workflow generated by `install-workflow`. | +| `CLAUDE_CODE_OAUTH_TOKEN` | OAuth token for the installed `claude` CLI in CI. The push-mode workflow generated by `install-workflow` forwards this into the gardener step so `selectClassifier()` can use the `claude-cli` path without a separate API key. | +| `ANTHROPIC_API_KEY` | Fallback classifier credential for `gardener comment` when `claude` is unavailable or unauthenticated. Forwarded into the launchd plist by `gardener start`; also accepted by the push-mode workflow as an optional fallback secret. If neither `claude` auth nor `ANTHROPIC_API_KEY` is available, `runComment` fails closed and emits `BREEZE_RESULT: status=skipped summary=no classifier injected` without touching `gh`. | | `GARDENER_CLASSIFIER_MODEL` | Optional override for the classifier model (default `claude-haiku-4-5`). Blank/unset is normalized to the default, so it's safe to leave the GitHub Actions secret empty. | | `BREEZE_SNAPSHOT_DIR` | Directory with pre-fetched `pr-view.json`, `pr.diff`, `issue-view.json`, `issue-comments.json`, `pr-reviews.json`, `subject.json`. Set by breeze-runner so gardener doesn't re-fetch. Also enables snapshot-mode idempotency checks in `respond` when `pr-commits.json` is present. | | `TREE_REPO_TOKEN` | PAT with `repo` scope on the tree repo. Consumed **only** by `comment`'s merge→issue branch, for `gh issue create` and the follow-up marker PATCH. No fallback to `GH_TOKEN`/`GITHUB_TOKEN` — if unset, the merge→issue path silently skips and logs `skipped: token_absent`. | diff --git a/src/products/gardener/engine/install-workflow.ts b/src/products/gardener/engine/install-workflow.ts index 723d2e7..001d59c 100644 --- a/src/products/gardener/engine/install-workflow.ts +++ b/src/products/gardener/engine/install-workflow.ts @@ -46,12 +46,12 @@ Options: --help, -h Show this help message. Next steps after install: - 1. Set the TREE_REPO_TOKEN and ANTHROPIC_API_KEY secrets on this + 1. Set the TREE_REPO_TOKEN and CLAUDE_CODE_OAUTH_TOKEN secrets on this repo (see skills/first-tree/references/workflow-mode.md for the gh-auth-based quick path and the caveats). - 2. Set the ANTHROPIC_API_KEY secret on this repo. Without it, - gardener comment refuses to post (PR #255) — this is the - intended fail-closed behaviour when no classifier is wired. + 2. Optional: set ANTHROPIC_API_KEY and GARDENER_CLASSIFIER_MODEL + if you want the CI workflow to fall back to the API-key path + when Claude Code auth is unavailable. 3. Commit and open a PR for the new workflow file. 4. Verify the workflow runs once the PR is merged. `; @@ -165,8 +165,8 @@ on: jobs: tree-sync: # Skip fork PRs: GitHub withholds secrets (TREE_REPO_TOKEN, - # ANTHROPIC_API_KEY) from fork workflows, so the job can't do its - # work. A skipped check is less misleading than a failed one. + # CLAUDE_CODE_OAUTH_TOKEN) from fork workflows, so the job can't do + # its work. A skipped check is less misleading than a failed one. # Also skip first-tree's own sync PRs so gardener never reviews itself. if: \${{ github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'first-tree:sync') }} runs-on: ubuntu-latest @@ -176,13 +176,7 @@ jobs: pull-requests: write env: TREE_REPO_TOKEN: \${{ secrets.TREE_REPO_TOKEN }} - ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} GH_TOKEN: \${{ github.token }} - # Optional: when set, gardener comment posts an AI-classified verdict. - # When unset, gardener comment refuses to post (see PR #255). Set - # ANTHROPIC_API_KEY as a repo secret to enable posting. - ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} - GARDENER_CLASSIFIER_MODEL: \${{ secrets.GARDENER_CLASSIFIER_MODEL }} steps: - name: Checkout source repo uses: actions/checkout@v4 @@ -227,9 +221,27 @@ jobs: with: node-version: "${nodeVersion}" + - name: Install CLIs (first-tree + claude) + # npx -p first-tree first-tree ... (the older + # install-workflow template) is flaky on Node 22 / newer npm. + # Install both CLIs globally instead. + # + # Gardener's verdict classifier shells out to the claude + # CLI. Pre-install @anthropic-ai/claude-code so the workflow + # can authenticate via CLAUDE_CODE_OAUTH_TOKEN. + run: npm install -g first-tree @anthropic-ai/claude-code + - name: Run first-tree gardener + env: + CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} + GARDENER_CLASSIFIER_MODEL: \${{ secrets.GARDENER_CLASSIFIER_MODEL }} + # Bypass gardener's \`gh api user\` self-identification call — + # GITHUB_TOKEN can't resolve /user reliably. Hardcoding the + # bot identity is enough for the self-loop guard. + GARDENER_USER: github-actions[bot] run: | - npx -p first-tree first-tree gardener comment \\ + first-tree gardener comment \\ --pr \${{ github.event.pull_request.number }} \\ --repo \${{ github.repository }} \\ --tree-path ${treePath} \\ @@ -317,28 +329,28 @@ export async function runInstallWorkflow( write(""); write("Next steps:"); write( - " 1. Set the TREE_REPO_TOKEN and ANTHROPIC_API_KEY secrets on this", + " 1. Set the TREE_REPO_TOKEN and CLAUDE_CODE_OAUTH_TOKEN secrets on", ); write( - " repo. Quick path via your local gh login (review the caveats in", + " this repo. Quick paths via your local gh / claude login", ); write( - " skills/first-tree/references/workflow-mode.md first):", + " (review the caveats in skills/first-tree/references/workflow-mode.md first):", ); write( ` gh auth token | gh secret set TREE_REPO_TOKEN --repo / --body -`, ); write( - ` printf '%s' \"$ANTHROPIC_API_KEY\" | gh secret set ANTHROPIC_API_KEY --repo / --body -`, + ` gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo /`, ); write( ` The token needs \`issues:write\` and \`contents:read\` on ${flags.treeRepo}.`, ); write( - " 2. Set ANTHROPIC_API_KEY on this repo. Without it, gardener comment", + " 2. Optional: set ANTHROPIC_API_KEY / GARDENER_CLASSIFIER_MODEL if", ); write( - " refuses to post (PR #255 fail-closed). Quick path:", + " you want an API-key fallback when Claude Code auth is unavailable.", ); write( ` gh secret set ANTHROPIC_API_KEY --repo /`, diff --git a/tests/gardener/gardener-install-workflow.test.ts b/tests/gardener/gardener-install-workflow.test.ts index 76ec771..2086954 100644 --- a/tests/gardener/gardener-install-workflow.test.ts +++ b/tests/gardener/gardener-install-workflow.test.ts @@ -104,12 +104,19 @@ describe("gardener install-workflow — yaml builder", () => { "github.event.pull_request.head.repo.full_name == github.repository", ); expect(yaml).toContain("TREE_REPO_TOKEN: ${{ secrets.TREE_REPO_TOKEN }}"); + expect(yaml).toContain( + "CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}", + ); expect(yaml).toContain( "ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}", ); expect(yaml).toContain( "GARDENER_CLASSIFIER_MODEL: ${{ secrets.GARDENER_CLASSIFIER_MODEL }}", ); + expect(yaml).toContain( + "npm install -g first-tree @anthropic-ai/claude-code", + ); + expect(yaml).toContain("GARDENER_USER: github-actions[bot]"); }); it("honors a custom tree-path override", () => { @@ -142,7 +149,7 @@ describe("gardener install-workflow — runInstallWorkflow", () => { expect(body).toContain('askpass_script="$RUNNER_TEMP/first-tree-git-askpass.sh"'); expect(lines.some((l) => l.includes("wrote"))).toBe(true); expect(lines.some((l) => l.includes("TREE_REPO_TOKEN"))).toBe(true); - expect(lines.some((l) => l.includes("ANTHROPIC_API_KEY"))).toBe(true); + expect(lines.some((l) => l.includes("CLAUDE_CODE_OAUTH_TOKEN"))).toBe(true); }); it("refuses to overwrite without --force", async () => {