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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/first-tree-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
49 changes: 25 additions & 24 deletions skills/first-tree/references/workflow-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CODEBASE_OWNER>/<CODEBASE_NAME> \
--body -
gh secret set CLAUDE_CODE_OAUTH_TOKEN \
--repo <CODEBASE_OWNER>/<CODEBASE_NAME>
```

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 <CODEBASE_OWNER>/<CODEBASE_NAME> \
--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

Expand Down Expand Up @@ -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.
Expand Down
36 changes: 20 additions & 16 deletions skills/gardener/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tree>` 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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -564,13 +566,15 @@ The `modules.<name>.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`. |
Expand Down
50 changes: 31 additions & 19 deletions src/products/gardener/engine/install-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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} \\
Expand Down Expand Up @@ -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 <codebase-owner>/<repo> --body -`,
);
write(
` printf '%s' \"$ANTHROPIC_API_KEY\" | gh secret set ANTHROPIC_API_KEY --repo <codebase-owner>/<repo> --body -`,
` gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo <codebase-owner>/<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 <codebase-owner>/<repo>`,
Expand Down
9 changes: 8 additions & 1 deletion tests/gardener/gardener-install-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading