From aed334bfea342d1b0cd7d52aa5bc9f32a2c8735d Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:08:41 +0000 Subject: [PATCH 01/26] docs: Rewrite README around install script The manual setup instructions (inline YAML, PAT steps, env var tables) are now handled by bin/install. The README becomes a high-level guide: what agentic does, how to set it up, and how the PR loop works. - Move Repo Setup to the top as the first section - Replace inline workflow YAML with template reference table - Trim Bot Identity from 38 lines to 4 - Remove duplicate env var inheritance section - Add diagnostics and custom env var sections - Rewrite opening paragraph in clear-speak - Remove completed TODO item --- README.md | 311 ++++++++++++------------------------------------------ 1 file changed, 69 insertions(+), 242 deletions(-) diff --git a/README.md b/README.md index 06ac8f2..195f856 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,72 @@ # Agentic -Reusable Claude Code skills and GitHub Actions composite actions for automated PR lifecycle management. +**Ship features by talking to GitHub.** + +Run Claude Code on GitHub, not just your laptop. Work on multiple features at the same time. Every change goes through a pull request, so the whole team can see and review what the AI builds. Comment `@claude` on an issue — it writes the code, opens a PR, fixes review feedback, and merges. Everything tracked in git. + +## Repo Setup + +Run the install script to set up a client repo: + +```bash +bin/install +``` + +This interactively: +1. Checks prerequisites (bot account + PAT, Claude OAuth token) +2. Creates or connects to a repo +3. Configures repo settings (squash merge, delete branch on merge, no auto-merge) +4. Copies 4 thin workflow files from `templates/workflows/` into the client repo's `.github/workflows/` +5. Optionally sets secrets and variables +6. Adds the bot as a collaborator +7. Creates a CLAUDE.md template +8. Commits and pushes + +The workflow files are thin callers that reference this repo's composite actions via `uses:`. When the agentic repo's actions are updated, all client repos pick up the changes automatically on the next workflow run. + +### Workflow templates + +The templates live in `templates/workflows/` and use a `__AGENTIC_REPO__` placeholder that the install script replaces with this repo's `owner/repo` path: + +| Template | Trigger | What it does | +|----------|---------|-------------| +| `gh-commented.yml` | Issue comment or new issue starting with `@claude` | Responds to questions | +| `gh-start-work.yml` | Issue comment with `@claude /pr-start` | Implements issue, opens PR | +| `pr-changes-requested.yml` | PR review with changes requested starting with `@claude` | Fixes code, pushes | +| `pr-approved.yml` | PR review approved starting with `@claude` | Squash merges | + +### Required secrets and variables + +**Secrets** (Settings → Secrets and variables → Actions): +- `CLAUDE_CODE_OAUTH_TOKEN`: OAuth token from `claude setup-token` +- `AGENTIC_BOT_TOKEN`: The bot account's fine-grained PAT + +**Variables** (Settings → Secrets and variables → Actions → Variables): +- `AGENTIC_BOT_NAME`: The bot's GitHub username +- `AGENTIC_BOT_EMAIL`: The bot's noreply email + +### Diagnostics + +To verify a client repo is set up correctly: + +```bash +bin/diagnostics owner/repo +``` + +### Custom environment variables + +To pass project-specific env vars (e.g. for a dev server proxy), add an `env` block at the job level in the client workflow. Composite actions inherit job-level env vars automatically: + +```yaml +jobs: + pr-fix: + env: + VITE_PROXY_TARGET: ${{ secrets.VITE_PROXY_TARGET }} + VITE_HOST: localhost + steps: + - uses: //.github/actions/run-claude-skill@main + # VITE_PROXY_TARGET and VITE_HOST are automatically available +``` ## Skills @@ -109,42 +175,9 @@ This only happens when the plan involves UI work. The GitHub Actions runner has ## Bot Identity -In GitHub Actions, the workflow sets git identity directly using `AGENTIC_BOT_NAME` and `AGENTIC_BOT_EMAIL` variables, and authenticates with `AGENTIC_BOT_TOKEN`. Claude never runs git or gh commands — the workflow handles all git and GitHub operations. - -For local use, `bin/git-bot` is a wrapper script that sets the bot's identity via environment variables. This ensures commits, PRs, and comments are attributed to your machine account, not your personal GitHub account. - -### Setting up a GitHub machine account - -1. Create a new GitHub account for your bot (e.g. `yourname-bot`) -2. Add the bot account as a collaborator on your repos (with write access) -3. On the bot account, generate a fine-grained Personal Access Token (PAT): - - Settings → Developer settings → Personal access tokens → Fine-grained tokens - - Repository access: **All repositories** (covers any repo the bot is added to in the future) - - Permissions: - - **Contents**: Read and write (for git push) - - **Pull requests**: Read and write (for creating/commenting/merging PRs) - - **Metadata**: Read (auto-selected) -4. Note the bot's noreply email from https://github.com/settings/emails — it looks like `ID+username@users.noreply.github.com` - -### Environment variables - -`git-bot` requires three env vars: - -| Variable | Value | -|---|---| -| `AGENTIC_BOT_NAME` | The bot's GitHub username (e.g. `yourname-bot`) | -| `AGENTIC_BOT_EMAIL` | The bot's noreply email (e.g. `ID+yourname-bot@users.noreply.github.com`) | -| `GH_TOKEN` | The bot's PAT | +A GitHub machine account (e.g. `yourname-bot`) is used for all git and GitHub operations. Commits, PRs, and comments are attributed to the bot, not your personal account. Claude never runs git or gh commands directly — the workflows handle that. -**Locally**, set these in your shell profile or a `.env` file: - -```bash -export AGENTIC_BOT_NAME="yourname-bot" -export AGENTIC_BOT_EMAIL="123456+yourname-bot@users.noreply.github.com" -export GH_TOKEN="ghp_..." -``` - -**In GitHub Actions**, add `AGENTIC_BOT_TOKEN` as a repo/org secret, and `AGENTIC_BOT_NAME` / `AGENTIC_BOT_EMAIL` as repo/org variables (Settings → Secrets and variables → Actions). +`bin/install` walks you through setting up the bot account and its PAT. For local use, `bin/git-bot` is a wrapper that sets the bot's identity via `AGENTIC_BOT_NAME`, `AGENTIC_BOT_EMAIL`, and `GH_TOKEN` environment variables. ## Composite Actions @@ -157,212 +190,6 @@ This repo provides 6 composite actions that can be composed into workflows: - **create-pr** — Creates a PR with optional issue linking from `/tmp/commit_msg.txt` and `/tmp/comment.txt` - **squash-merge** — Squash merges a PR with custom commit message from `/tmp/commit_msg.txt` -### Environment Variable Inheritance - -Composite actions automatically inherit environment variables from the job. Set env vars at the job level and they'll be available to all steps, including those in composite actions: - -```yaml -jobs: - pr-fix: - env: - VITE_PROXY_TARGET: ${{ secrets.VITE_PROXY_TARGET }} - VITE_HOST: localhost - steps: - - uses: chriswickett/agentic/.github/actions/run-claude-skill@main - # VITE_PROXY_TARGET and VITE_HOST are automatically available -``` - -Secrets and tokens must still be passed explicitly as inputs. - -## Repo Setup - -Before using these workflows with a client repo: - -1. **Disable auto-merge**: Settings → General → Pull Requests. Make sure "Allow auto-merge" is OFF. GClaude controls when merges happen. - -2. **Allow squash merging**: Same section, make sure "Allow squash merging" is ON. - -3. **Add workflow files**: Create four workflow files in `.github/workflows/`. All workflows require `@claude` at the start of the triggering comment/review for security. - - `gh-commented.yml` — Responds to issue comments and new issues: - ```yaml - on: - issue_comment: - types: [created] - issues: - types: [opened] - - jobs: - gh-respond: - if: | - (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '@claude') && !startsWith(github.event.comment.body, '@claude /pr-start')) || - (github.event_name == 'issues' && startsWith(github.event.issue.body, '@claude')) - runs-on: ubuntu-latest - timeout-minutes: 5 - env: - VITE_PROXY_TARGET: ${{ secrets.VITE_PROXY_TARGET }} - VITE_HOST: localhost - steps: - - uses: actions/checkout@v4 - - - uses: chriswickett/agentic/.github/actions/setup-claude-agent@main - - - uses: chriswickett/agentic/.github/actions/run-claude-skill@main - with: - skill-name: gh-respond - context-type: issue - context-number: ${{ github.event.issue.number }} - repository: ${{ github.repository }} - claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - - - uses: chriswickett/agentic/.github/actions/post-comment@main - with: - comment-file: /tmp/comment.txt - issue-or-pr-number: ${{ github.event.issue.number }} - comment-type: issue - repository: ${{ github.repository }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - ``` - - `gh-start-work.yml` — Implements an issue and opens a PR: - ```yaml - on: - issue_comment: - types: [created] - - jobs: - pr-start: - if: "!github.event.issue.pull_request && startsWith(github.event.comment.body, '@claude /pr-start')" - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - VITE_PROXY_TARGET: ${{ secrets.VITE_PROXY_TARGET }} - VITE_HOST: localhost - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: chriswickett/agentic/.github/actions/setup-claude-agent@main - - - uses: chriswickett/agentic/.github/actions/run-claude-skill@main - with: - skill-name: pr-start - context-type: issue - context-number: ${{ github.event.issue.number }} - repository: ${{ github.repository }} - claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - - - uses: chriswickett/agentic/.github/actions/git-commit-push@main - with: - branch-name-file: /tmp/pr_branch_name.txt - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - bot-name: ${{ vars.AGENTIC_BOT_NAME }} - bot-email: ${{ vars.AGENTIC_BOT_EMAIL }} - - - uses: chriswickett/agentic/.github/actions/create-pr@main - with: - issue-number: ${{ github.event.issue.number }} - repository: ${{ github.repository }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - ``` - - `pr-changes-requested.yml` — Fixes code when reviewer requests changes: - ```yaml - on: - pull_request_review: - types: [submitted] - - jobs: - pr-fix: - if: github.event.review.state == 'changes_requested' && startsWith(github.event.review.body, '@claude') - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - VITE_PROXY_TARGET: ${{ secrets.VITE_PROXY_TARGET }} - VITE_HOST: localhost - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - - - uses: chriswickett/agentic/.github/actions/setup-claude-agent@main - - - uses: chriswickett/agentic/.github/actions/run-claude-skill@main - with: - skill-name: pr-fix - context-type: pr - context-number: ${{ github.event.pull_request.number }} - repository: ${{ github.repository }} - claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - - - uses: chriswickett/agentic/.github/actions/git-commit-push@main - with: - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - bot-name: ${{ vars.AGENTIC_BOT_NAME }} - bot-email: ${{ vars.AGENTIC_BOT_EMAIL }} - - - uses: chriswickett/agentic/.github/actions/post-comment@main - with: - issue-or-pr-number: ${{ github.event.pull_request.number }} - comment-type: pr - repository: ${{ github.repository }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - ``` - - `pr-approved.yml` — Squash merges when reviewer approves: - ```yaml - on: - pull_request_review: - types: [submitted] - - jobs: - pr-merge: - if: github.event.review.state == 'approved' && startsWith(github.event.review.body, '@claude') - runs-on: ubuntu-latest - timeout-minutes: 5 - env: - VITE_PROXY_TARGET: ${{ secrets.VITE_PROXY_TARGET }} - VITE_HOST: localhost - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - - - uses: chriswickett/agentic/.github/actions/setup-claude-agent@main - - - uses: chriswickett/agentic/.github/actions/run-claude-skill@main - with: - skill-name: pr-merge - context-type: pr - context-number: ${{ github.event.pull_request.number }} - repository: ${{ github.repository }} - claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - - - uses: chriswickett/agentic/.github/actions/squash-merge@main - with: - pr-number: ${{ github.event.pull_request.number }} - repository: ${{ github.repository }} - github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} - ``` - -4. **Add secrets**: At repo level (Settings → Secrets and variables → Actions) or org level: - - `CLAUDE_CODE_OAUTH_TOKEN`: OAuth token from `claude setup-token` (uses your Claude Pro/Max subscription) - - `AGENTIC_BOT_TOKEN`: The bot account's PAT - - `VITE_PROXY_TARGET` (optional): For frontend repos that need a proxy to a backend - -5. **Add variables**: At repo level (Settings → Secrets and variables → Actions → Variables) or org level: - - `AGENTIC_BOT_NAME`: The bot's GitHub username - - `AGENTIC_BOT_EMAIL`: The bot's noreply email - ## TODO - Harden and expand the allowed commands list in `bin/bg` -- Setup script to generate client repo workflow files from agentic - From ac5d502f623f61bc9a816a465458c4a1e6e05803 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:10:58 +0000 Subject: [PATCH 02/26] feat: Add install script and client repo tooling Interactive setup script that automates the full client repo onboarding: repo creation, settings, workflow files, secrets, bot collaborator access, and CLAUDE.md template. - bin/install: main script with interactive prompts - bin/prerequisites: gate that explains bot account + PAT setup - bin/diagnostics: verifies a client repo is wired up correctly - templates/workflows/: 4 thin caller workflow templates using __AGENTIC_REPO__ placeholder for composite action references --- bin/diagnostics | 147 +++++++++ bin/install | 323 +++++++++++++++++++ bin/prerequisites | 40 +++ templates/workflows/gh-commented.yml | 36 +++ templates/workflows/gh-start-work.yml | 39 +++ templates/workflows/pr-approved.yml | 33 ++ templates/workflows/pr-changes-requested.yml | 40 +++ 7 files changed, 658 insertions(+) create mode 100755 bin/diagnostics create mode 100755 bin/install create mode 100755 bin/prerequisites create mode 100644 templates/workflows/gh-commented.yml create mode 100644 templates/workflows/gh-start-work.yml create mode 100644 templates/workflows/pr-approved.yml create mode 100644 templates/workflows/pr-changes-requested.yml diff --git a/bin/diagnostics b/bin/diagnostics new file mode 100755 index 0000000..876d504 --- /dev/null +++ b/bin/diagnostics @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +BOLD=$'\033[1m' +DIM=$'\033[2m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[0;33m' +RED=$'\033[0;31m' +CYAN=$'\033[0;36m' +RESET=$'\033[0m' + +ok() { echo -e " ${GREEN}✓${RESET} $*"; } +fail() { echo -e " ${RED}✗${RESET} $*"; } +warn() { echo -e " ${YELLOW}!${RESET} $*"; } + +# --- Get repo --- + +if [[ -n "${1:-}" ]]; then + REPO="$1" +else + read -rp "Repo to check (owner/repo): " REPO +fi + +echo "" +echo -e "${BOLD}Diagnostics for $REPO${RESET}" +echo "" + +# --- Check gh auth --- + +echo -e "${BOLD}GitHub CLI${RESET}" +if gh auth status &>/dev/null 2>&1; then + ok "gh authenticated" +else + fail "gh not authenticated — run 'gh auth login'" +fi +echo "" + +# --- Check repo exists and is accessible --- + +echo -e "${BOLD}Repo access${RESET}" +if gh api "/repos/$REPO" &>/dev/null 2>&1; then + ok "Repo $REPO is accessible" +else + fail "Cannot access $REPO — check the repo exists and you have access" +fi +echo "" + +# --- Check repo settings --- + +echo -e "${BOLD}Repo settings${RESET}" +SETTINGS=$(gh api "/repos/$REPO" --jq '{auto_merge: .allow_auto_merge, squash: .allow_squash_merge, delete_branch: .delete_branch_on_merge}' 2>/dev/null || echo "{}") + +if echo "$SETTINGS" | jq -e '.auto_merge == false' &>/dev/null; then + ok "Auto-merge disabled" +else + fail "Auto-merge should be disabled" +fi + +if echo "$SETTINGS" | jq -e '.squash == true' &>/dev/null; then + ok "Squash merge enabled" +else + fail "Squash merge should be enabled" +fi + +if echo "$SETTINGS" | jq -e '.delete_branch == true' &>/dev/null; then + ok "Delete branch on merge enabled" +else + fail "Delete branch on merge should be enabled" +fi +echo "" + +# --- Check workflow files --- + +echo -e "${BOLD}Workflow files${RESET}" +for wf in gh-commented gh-start-work pr-changes-requested pr-approved; do + if gh api "/repos/$REPO/contents/.github/workflows/${wf}.yml" &>/dev/null 2>&1; then + ok "${wf}.yml" + else + fail "${wf}.yml not found" + fi +done +echo "" + +# --- Check secrets --- + +echo -e "${BOLD}Secrets${RESET}" +SECRETS=$(gh secret list --repo "$REPO" 2>/dev/null || echo "") + +if echo "$SECRETS" | grep -q "CLAUDE_CODE_OAUTH_TOKEN"; then + ok "CLAUDE_CODE_OAUTH_TOKEN is set" +else + fail "CLAUDE_CODE_OAUTH_TOKEN not set" +fi + +if echo "$SECRETS" | grep -q "AGENTIC_BOT_TOKEN"; then + ok "AGENTIC_BOT_TOKEN is set" +else + fail "AGENTIC_BOT_TOKEN not set" +fi +echo "" + +# --- Check variables --- + +echo -e "${BOLD}Variables${RESET}" +VARS=$(gh variable list --repo "$REPO" 2>/dev/null || echo "") + +if echo "$VARS" | grep -q "AGENTIC_BOT_NAME"; then + BOT_NAME=$(echo "$VARS" | grep "AGENTIC_BOT_NAME" | awk '{print $2}') + ok "AGENTIC_BOT_NAME = $BOT_NAME" +else + fail "AGENTIC_BOT_NAME not set" + BOT_NAME="" +fi + +if echo "$VARS" | grep -q "AGENTIC_BOT_EMAIL"; then + ok "AGENTIC_BOT_EMAIL is set" +else + fail "AGENTIC_BOT_EMAIL not set" +fi +echo "" + +# --- Test bot token and access --- + +echo -e "${BOLD}Bot repo access${RESET}" +echo -n "Paste AGENTIC_BOT_TOKEN to test (or press Enter to skip): " +read -rs TEST_TOKEN +echo "" + +if [[ -n "$TEST_TOKEN" ]]; then + if GH_TOKEN="$TEST_TOKEN" gh api "/repos/$REPO" &>/dev/null 2>&1; then + ok "Bot token can access $REPO" + else + fail "Bot token cannot access $REPO" + cat < Personal access tokens > Pending requests)${RESET} + ${DIM} - Bot is a collaborator or member of a team with repo access${RESET} + ${DIM} - Repository access: All repositories${RESET} + ${DIM} - Permissions: Contents (read/write), Pull requests (read/write), Issues (read/write), Metadata (read)${RESET} +EOF + fi +else + warn "Skipped — paste the bot token to verify access" +fi +echo "" diff --git a/bin/install b/bin/install new file mode 100755 index 0000000..b6abe61 --- /dev/null +++ b/bin/install @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- Colors --- +BOLD='\033[1m' +DIM='\033[2m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +RESET='\033[0m' + +info() { echo -e "${CYAN}$*${RESET}"; } +ok() { echo -e "${GREEN}✓ $*${RESET}"; } +warn() { echo -e "${YELLOW}$*${RESET}"; } +err() { echo -e "${RED}Error: $*${RESET}" >&2; exit 1; } + +# --- Preamble --- + +for cmd in gh git jq; do + command -v "$cmd" &>/dev/null || err "$cmd is not installed" +done + +gh auth status &>/dev/null 2>&1 || err "Not authenticated with GitHub CLI. Run 'gh auth login' first." + +# Detect agentic repo from this script's location +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +AGENTIC_REMOTE=$(git -C "$AGENTIC_ROOT" remote get-url origin 2>/dev/null) \ + || err "Could not detect agentic repo remote" + +# Extract owner/repo from remote URL (handles https and ssh) +AGENTIC_REPO=$(echo "$AGENTIC_REMOTE" | sed -E 's|.*github\.com[:/]||; s|\.git$||') + +echo "" +echo -e "${BOLD}Agentic Client Repo Setup${RESET}" +echo -e "${DIM}Agentic repo: $AGENTIC_REPO${RESET}" + +# --- Prerequisites --- + +"$SCRIPT_DIR/prerequisites" + +echo "" + +# --- Interactive prompts --- + +echo "1) Create a new repo" +echo "2) Use an existing repo" +read -rp "Choose [1/2]: " REPO_CHOICE +echo "" + +if [[ "$REPO_CHOICE" == "1" ]]; then + # --- New repo --- + read -rp "Repo name: " REPO_NAME + [[ -z "$REPO_NAME" ]] && err "Repo name cannot be empty" + + echo "" + echo "1) Personal account" + echo "2) Organization" + read -rp "Choose [1/2]: " OWNER_TYPE + + if [[ "$OWNER_TYPE" == "2" ]]; then + read -rp "Organization name: " ORG_NAME + [[ -z "$ORG_NAME" ]] && err "Organization name cannot be empty" + FULL_REPO="$ORG_NAME/$REPO_NAME" + else + OWNER=$(gh api user -q .login) + FULL_REPO="$OWNER/$REPO_NAME" + fi + + echo "" + echo "1) Private" + echo "2) Public" + read -rp "Visibility [1/2]: " VIS_CHOICE + + VISIBILITY="private" + [[ "$VIS_CHOICE" == "2" ]] && VISIBILITY="public" + + echo "" + info "Creating $FULL_REPO ($VISIBILITY)..." + + if [[ "$OWNER_TYPE" == "2" ]]; then + gh repo create "$FULL_REPO" --"$VISIBILITY" --clone + else + gh repo create "$REPO_NAME" --"$VISIBILITY" --clone + fi + + CLIENT_DIR="$(pwd)/$REPO_NAME" + ok "Repo created and cloned to $CLIENT_DIR" + +elif [[ "$REPO_CHOICE" == "2" ]]; then + # --- Existing repo --- + echo "1) Local clone (enter path)" + echo "2) GitHub repo (enter owner/repo to clone)" + read -rp "Choose [1/2]: " EXISTING_CHOICE + echo "" + + if [[ "$EXISTING_CHOICE" == "1" ]]; then + read -rp "Path to local repo: " CLIENT_DIR_INPUT + CLIENT_DIR=$(cd "$CLIENT_DIR_INPUT" && pwd) || err "Directory not found: $CLIENT_DIR_INPUT" + FULL_REPO=$(git -C "$CLIENT_DIR" remote get-url origin 2>/dev/null \ + | sed -E 's|.*github\.com[:/]||; s|\.git$||') \ + || err "Could not detect remote for $CLIENT_DIR" + else + read -rp "owner/repo: " FULL_REPO + [[ -z "$FULL_REPO" ]] && err "owner/repo cannot be empty" + info "Cloning $FULL_REPO..." + gh repo clone "$FULL_REPO" + REPO_NAME=$(echo "$FULL_REPO" | cut -d/ -f2) + CLIENT_DIR="$(pwd)/$REPO_NAME" + fi + + ok "Using repo: $FULL_REPO ($CLIENT_DIR)" +else + err "Invalid choice" +fi + +# --- Configure repo settings --- + +echo "" +info "Configuring repo settings..." + +gh api -X PATCH "/repos/$FULL_REPO" \ + -F allow_auto_merge=false \ + -F allow_squash_merge=true \ + -F delete_branch_on_merge=true \ + --silent + +ok "Auto-merge disabled, squash merge enabled, delete branch on merge enabled" + +# --- Write workflow files --- + +echo "" +info "Writing workflow files..." + +WORKFLOW_DIR="$CLIENT_DIR/.github/workflows" +TEMPLATE_DIR="$AGENTIC_ROOT/templates/workflows" +mkdir -p "$WORKFLOW_DIR" + +for template in "$TEMPLATE_DIR"/*.yml; do + filename=$(basename "$template") + sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" + echo -e " ${DIM}$filename${RESET}" +done + +ok "Created workflow files in $WORKFLOW_DIR/" + +# --- Secrets and variables (optional) --- + +echo "" +read -rp "Configure secrets and variables now? [y/N]: " SETUP_SECRETS + +if [[ "${SETUP_SECRETS,,}" == "y" ]]; then + echo "" + echo -e "${BOLD}Secrets${RESET}" + echo -e "${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" + echo -e "${DIM}It authenticates with your Claude Pro/Max subscription and outputs an OAuth token.${RESET}" + echo "" + + echo -n "CLAUDE_CODE_OAUTH_TOKEN: " + read -rs CLAUDE_TOKEN + echo "" + [[ -n "$CLAUDE_TOKEN" ]] && echo "$CLAUDE_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$FULL_REPO" + + echo -n "AGENTIC_BOT_TOKEN: " + read -rs BOT_TOKEN + echo "" + [[ -n "$BOT_TOKEN" ]] && echo "$BOT_TOKEN" | gh secret set AGENTIC_BOT_TOKEN --repo "$FULL_REPO" + + echo "" + echo -e "${BOLD}Variables${RESET}" + + read -rp "AGENTIC_BOT_NAME: " BOT_NAME + [[ -n "$BOT_NAME" ]] && gh variable set AGENTIC_BOT_NAME --repo "$FULL_REPO" --body "$BOT_NAME" + + read -rp "AGENTIC_BOT_EMAIL: " BOT_EMAIL + [[ -n "$BOT_EMAIL" ]] && gh variable set AGENTIC_BOT_EMAIL --repo "$FULL_REPO" --body "$BOT_EMAIL" + + ok "Secrets and variables configured" +else + echo "" + warn "Remember to set these before using the workflows:" + echo -e " ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" + echo "" + echo " Secrets:" + echo " gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO" + echo " gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO" + echo "" + echo " Variables:" + echo " gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body \"bot-username\"" + echo " gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body \"123+bot@users.noreply.github.com\"" +fi + +# --- Add bot as collaborator --- + +echo "" +echo -e "${BOLD}Bot repo access${RESET}" +echo "" +echo "The bot needs write access to this repo. Two approaches:" +echo "" +echo "1) Add bot directly as collaborator (simple, per-repo)" +echo "2) Skip — I've added the bot to an org team that has repo access" +echo "" +echo -e "${DIM}Tip: If you're using a GitHub org, add the bot to a team instead.${RESET}" +echo -e "${DIM}Any repo the team can access, the bot automatically can too —${RESET}" +echo -e "${DIM}no need to add it individually each time you run this script.${RESET}" +echo "" +read -rp "Choose [1/2]: " COLLAB_CHOICE + +if [[ "$COLLAB_CHOICE" == "1" ]]; then + # Use BOT_NAME if already set from secrets step, otherwise ask + if [[ -z "${BOT_NAME:-}" ]]; then + read -rp "Bot GitHub username: " BOT_NAME + fi + + if [[ -n "$BOT_NAME" ]]; then + info "Adding $BOT_NAME as collaborator..." + gh api -X PUT "/repos/$FULL_REPO/collaborators/$BOT_NAME" -f permission=write --silent + ok "$BOT_NAME added as collaborator (write access)" + + # Accept the invitation using the bot's token + if [[ -n "${BOT_TOKEN:-}" ]]; then + info "Accepting invitation as $BOT_NAME..." + INVITE_ID=$(GH_TOKEN="$BOT_TOKEN" gh api "/user/repository_invitations" --jq ".[] | select(.repository.full_name==\"$FULL_REPO\") | .id" 2>/dev/null || true) + if [[ -n "$INVITE_ID" ]]; then + GH_TOKEN="$BOT_TOKEN" gh api -X PATCH "/user/repository_invitations/$INVITE_ID" --silent + ok "Invitation accepted" + else + warn "Could not find invitation — the bot may need to accept manually at https://github.com/$FULL_REPO/invitations" + fi + else + warn "Bot token not available — the bot will need to accept the invitation at https://github.com/$FULL_REPO/invitations" + fi + fi +else + echo "" + warn "Remember to add the bot to the repo via your team/org settings." +fi + +# --- CLAUDE.md --- + +echo "" +echo -e "${BOLD}CLAUDE.md${RESET}" +echo "" + +if [[ -f "$CLIENT_DIR/CLAUDE.md" ]]; then + warn "CLAUDE.md already exists — skipping" +else + read -rp "Create a CLAUDE.md template? [Y/n]: " CREATE_CLAUDE_MD + if [[ "${CREATE_CLAUDE_MD,,}" != "n" ]]; then + read -rp "Project name: " PROJECT_NAME + read -rp "Short description: " PROJECT_DESC + read -rp "Tech stack (e.g. Next.js, TypeScript, Prisma): " TECH_STACK + + cat > "$CLIENT_DIR/CLAUDE.md" << MD +# ${PROJECT_NAME:-Project} + +${PROJECT_DESC:-A brief description of what this project does.} + +## Tech stack + +${TECH_STACK:-Describe the technologies used here.} + +## Project structure + +Describe the key directories and files here. + +## Development + +### Setup + +\`\`\`bash +# How to install dependencies and get running locally +\`\`\` + +### Common commands + +\`\`\`bash +# dev server, tests, linting, etc. +\`\`\` + +## Conventions + +- Describe coding conventions, naming patterns, etc. +MD + + ok "Created CLAUDE.md template — edit it to add more project context" + fi +fi + +# --- Commit and push --- + +echo "" +info "Committing and pushing..." + +cd "$CLIENT_DIR" +git add .github/workflows/ +[[ -f "CLAUDE.md" ]] && git add CLAUDE.md + +if git rev-parse HEAD &>/dev/null 2>&1; then + git commit -m "Add agentic workflow files and CLAUDE.md" + git push +else + # Empty repo — create initial commit + git checkout -b main + git commit -m "Initial commit: add agentic workflow files and CLAUDE.md" + git push -u origin main +fi + +ok "Changes committed and pushed" + +# --- Summary --- + +echo "" +echo -e "${BOLD}${GREEN}Setup complete!${RESET}" +echo "" +echo " Repo: https://github.com/$FULL_REPO" +echo " Workflows: 4 files in .github/workflows/" +echo "" +echo "Flesh out CLAUDE.md with detailed project context, then verify the setup:" +echo "" +echo " gh issue create --repo $FULL_REPO --title 'Test agentic setup' --body '@claude Are you there?'" diff --git a/bin/prerequisites b/bin/prerequisites new file mode 100755 index 0000000..feaa4a9 --- /dev/null +++ b/bin/prerequisites @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +BOLD=$'\033[1m' +CYAN=$'\033[0;36m' +RESET=$'\033[0m' + +cat < Developer settings > Personal access tokens > Fine-grained tokens > Generate new token + - Token name: anything you like (e.g. agentic-bot) — just a label for your reference + - Resource owner: set this to your ORG, not the bot's personal account + (a token scoped to the bot user can't access org repos, even if the bot is a member) + One PAT covers all repos in that org. You'd need a separate PAT for a + different org or for personal repos. + - After creating the token, an org admin must approve it: + Org settings > Personal access tokens > Pending requests + - Repository access: All repositories + - Permissions: Contents (read/write), Pull requests (read/write), Issues (read/write), Metadata (read) + Also note the bot's noreply email from https://github.com/settings/emails + - It looks like: ID+username@users.noreply.github.com + + ${BOLD}2. Claude OAuth token${RESET} + Run 'claude setup-token' in your terminal. It authenticates with your + Claude Pro/Max subscription and outputs an OAuth token to copy. + +EOF + +read -rp "Do you have these ready? [y/N]: " PREREQS_READY +[[ "${PREREQS_READY,,}" == "y" ]] || { echo ""; echo -e "${CYAN}Set those up first, then re-run this script.${RESET}"; exit 1; } diff --git a/templates/workflows/gh-commented.yml b/templates/workflows/gh-commented.yml new file mode 100644 index 0000000..ac59793 --- /dev/null +++ b/templates/workflows/gh-commented.yml @@ -0,0 +1,36 @@ +name: GH Commented + +on: + issue_comment: + types: [created] + issues: + types: [opened] + +jobs: + gh-respond: + if: | + (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '@claude') && !startsWith(github.event.comment.body, '@claude /pr-start')) || + (github.event_name == 'issues' && startsWith(github.event.issue.body, '@claude')) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - uses: __AGENTIC_REPO__/.github/actions/setup-claude-agent@main + + - uses: __AGENTIC_REPO__/.github/actions/run-claude-skill@main + with: + skill-name: gh-respond + context-type: issue + context-number: ${{ github.event.issue.number }} + repository: ${{ github.repository }} + claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} + + - uses: __AGENTIC_REPO__/.github/actions/post-comment@main + with: + comment-file: /tmp/comment.txt + issue-or-pr-number: ${{ github.event.issue.number }} + comment-type: issue + repository: ${{ github.repository }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} diff --git a/templates/workflows/gh-start-work.yml b/templates/workflows/gh-start-work.yml new file mode 100644 index 0000000..091cf13 --- /dev/null +++ b/templates/workflows/gh-start-work.yml @@ -0,0 +1,39 @@ +name: GH Start Work + +on: + issue_comment: + types: [created] + +jobs: + pr-start: + if: "!github.event.issue.pull_request && startsWith(github.event.comment.body, '@claude /pr-start')" + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: __AGENTIC_REPO__/.github/actions/setup-claude-agent@main + + - uses: __AGENTIC_REPO__/.github/actions/run-claude-skill@main + with: + skill-name: pr-start + context-type: issue + context-number: ${{ github.event.issue.number }} + repository: ${{ github.repository }} + claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} + + - uses: __AGENTIC_REPO__/.github/actions/git-commit-push@main + with: + branch-name-file: /tmp/pr_branch_name.txt + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} + bot-name: ${{ vars.AGENTIC_BOT_NAME }} + bot-email: ${{ vars.AGENTIC_BOT_EMAIL }} + + - uses: __AGENTIC_REPO__/.github/actions/create-pr@main + with: + issue-number: ${{ github.event.issue.number }} + repository: ${{ github.repository }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} diff --git a/templates/workflows/pr-approved.yml b/templates/workflows/pr-approved.yml new file mode 100644 index 0000000..9eeb17c --- /dev/null +++ b/templates/workflows/pr-approved.yml @@ -0,0 +1,33 @@ +name: PR Approved + +on: + pull_request_review: + types: [submitted] + +jobs: + pr-merge: + if: github.event.review.state == 'approved' && startsWith(github.event.review.body, '@claude') + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + - uses: __AGENTIC_REPO__/.github/actions/setup-claude-agent@main + + - uses: __AGENTIC_REPO__/.github/actions/run-claude-skill@main + with: + skill-name: pr-merge + context-type: pr + context-number: ${{ github.event.pull_request.number }} + repository: ${{ github.repository }} + claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} + + - uses: __AGENTIC_REPO__/.github/actions/squash-merge@main + with: + pr-number: ${{ github.event.pull_request.number }} + repository: ${{ github.repository }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} diff --git a/templates/workflows/pr-changes-requested.yml b/templates/workflows/pr-changes-requested.yml new file mode 100644 index 0000000..9ea6d19 --- /dev/null +++ b/templates/workflows/pr-changes-requested.yml @@ -0,0 +1,40 @@ +name: PR Changes Requested + +on: + pull_request_review: + types: [submitted] + +jobs: + pr-fix: + if: github.event.review.state == 'changes_requested' && startsWith(github.event.review.body, '@claude') + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + - uses: __AGENTIC_REPO__/.github/actions/setup-claude-agent@main + + - uses: __AGENTIC_REPO__/.github/actions/run-claude-skill@main + with: + skill-name: pr-fix + context-type: pr + context-number: ${{ github.event.pull_request.number }} + repository: ${{ github.repository }} + claude-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} + + - uses: __AGENTIC_REPO__/.github/actions/git-commit-push@main + with: + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} + bot-name: ${{ vars.AGENTIC_BOT_NAME }} + bot-email: ${{ vars.AGENTIC_BOT_EMAIL }} + + - uses: __AGENTIC_REPO__/.github/actions/post-comment@main + with: + issue-or-pr-number: ${{ github.event.pull_request.number }} + comment-type: pr + repository: ${{ github.repository }} + github-token: ${{ secrets.AGENTIC_BOT_TOKEN }} From 9d333dc374667f313bc8d0cfda9822529bf7dd1a Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:22:57 +0000 Subject: [PATCH 03/26] refactor: Extract shared colors into bin/lib/common.sh Colors and helpers were duplicated across install, prerequisites, and diagnostics. A single source file makes them easier to maintain and keeps the scripts focused on their own logic. - Create bin/lib/common.sh with colors and helpers - Source it from install, prerequisites, and diagnostics - Diagnostics keeps local overrides for checklist format --- bin/diagnostics | 9 ++------- bin/install | 14 +------------- bin/lib/common.sh | 15 +++++++++++++++ bin/prerequisites | 4 +--- 4 files changed, 19 insertions(+), 23 deletions(-) create mode 100755 bin/lib/common.sh diff --git a/bin/diagnostics b/bin/diagnostics index 876d504..f1f034d 100755 --- a/bin/diagnostics +++ b/bin/diagnostics @@ -1,14 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -BOLD=$'\033[1m' -DIM=$'\033[2m' -GREEN=$'\033[0;32m' -YELLOW=$'\033[0;33m' -RED=$'\033[0;31m' -CYAN=$'\033[0;36m' -RESET=$'\033[0m' +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" +# Override helpers for diagnostic checklist format ok() { echo -e " ${GREEN}✓${RESET} $*"; } fail() { echo -e " ${RED}✗${RESET} $*"; } warn() { echo -e " ${YELLOW}!${RESET} $*"; } diff --git a/bin/install b/bin/install index b6abe61..a145adb 100755 --- a/bin/install +++ b/bin/install @@ -1,19 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# --- Colors --- -BOLD='\033[1m' -DIM='\033[2m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -RED='\033[0;31m' -CYAN='\033[0;36m' -RESET='\033[0m' - -info() { echo -e "${CYAN}$*${RESET}"; } -ok() { echo -e "${GREEN}✓ $*${RESET}"; } -warn() { echo -e "${YELLOW}$*${RESET}"; } -err() { echo -e "${RED}Error: $*${RESET}" >&2; exit 1; } +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" # --- Preamble --- diff --git a/bin/lib/common.sh b/bin/lib/common.sh new file mode 100755 index 0000000..adeb160 --- /dev/null +++ b/bin/lib/common.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Shared colors and helpers for agentic scripts + +BOLD=$'\033[1m' +DIM=$'\033[2m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[0;33m' +RED=$'\033[0;31m' +CYAN=$'\033[0;36m' +RESET=$'\033[0m' + +info() { echo -e "${CYAN}$*${RESET}"; } +ok() { echo -e "${GREEN}✓ $*${RESET}"; } +warn() { echo -e "${YELLOW}$*${RESET}"; } +err() { echo -e "${RED}Error: $*${RESET}" >&2; exit 1; } diff --git a/bin/prerequisites b/bin/prerequisites index feaa4a9..578d4c0 100755 --- a/bin/prerequisites +++ b/bin/prerequisites @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BOLD=$'\033[1m' -CYAN=$'\033[0;36m' -RESET=$'\033[0m' +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" cat < Date: Thu, 19 Feb 2026 20:32:53 +0000 Subject: [PATCH 04/26] refactor: Extract install steps into bin/steps/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monolithic install script (310 lines) made it impossible to re-run a single step after a mid-run failure. Each section is now its own script in bin/steps/, sourced by a slim orchestrator. A require_vars guard in common.sh lets each step validate its inputs and fail fast — steps also work standalone via env vars (e.g. FULL_REPO=acme/app bin/steps/settings). - Add require_vars helper to bin/lib/common.sh - Create 7 step scripts: repo, settings, workflows, secrets, collaborator, claude-md, commit-push - Rewrite bin/install as ~40-line orchestrator that sources steps --- bin/install | 291 ++--------------------------------------- bin/lib/common.sh | 8 ++ bin/steps/claude-md | 54 ++++++++ bin/steps/collaborator | 47 +++++++ bin/steps/commit-push | 23 ++++ bin/steps/repo | 74 +++++++++++ bin/steps/secrets | 51 ++++++++ bin/steps/settings | 16 +++ bin/steps/workflows | 20 +++ 9 files changed, 306 insertions(+), 278 deletions(-) create mode 100755 bin/steps/claude-md create mode 100755 bin/steps/collaborator create mode 100755 bin/steps/commit-push create mode 100755 bin/steps/repo create mode 100755 bin/steps/secrets create mode 100755 bin/steps/settings create mode 100755 bin/steps/workflows diff --git a/bin/install b/bin/install index a145adb..091e96d 100755 --- a/bin/install +++ b/bin/install @@ -3,302 +3,36 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" -# --- Preamble --- - for cmd in gh git jq; do command -v "$cmd" &>/dev/null || err "$cmd is not installed" done -gh auth status &>/dev/null 2>&1 || err "Not authenticated with GitHub CLI. Run 'gh auth login' first." +gh auth status &>/dev/null 2>&1 \ + || err "Not authenticated with GitHub CLI. Run 'gh auth login' first." -# Detect agentic repo from this script's location SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - AGENTIC_REMOTE=$(git -C "$AGENTIC_ROOT" remote get-url origin 2>/dev/null) \ || err "Could not detect agentic repo remote" - -# Extract owner/repo from remote URL (handles https and ssh) AGENTIC_REPO=$(echo "$AGENTIC_REMOTE" | sed -E 's|.*github\.com[:/]||; s|\.git$||') +export SCRIPT_DIR AGENTIC_ROOT AGENTIC_REPO echo "" echo -e "${BOLD}Agentic Client Repo Setup${RESET}" echo -e "${DIM}Agentic repo: $AGENTIC_REPO${RESET}" -# --- Prerequisites --- - "$SCRIPT_DIR/prerequisites" echo "" -# --- Interactive prompts --- - -echo "1) Create a new repo" -echo "2) Use an existing repo" -read -rp "Choose [1/2]: " REPO_CHOICE -echo "" - -if [[ "$REPO_CHOICE" == "1" ]]; then - # --- New repo --- - read -rp "Repo name: " REPO_NAME - [[ -z "$REPO_NAME" ]] && err "Repo name cannot be empty" - - echo "" - echo "1) Personal account" - echo "2) Organization" - read -rp "Choose [1/2]: " OWNER_TYPE - - if [[ "$OWNER_TYPE" == "2" ]]; then - read -rp "Organization name: " ORG_NAME - [[ -z "$ORG_NAME" ]] && err "Organization name cannot be empty" - FULL_REPO="$ORG_NAME/$REPO_NAME" - else - OWNER=$(gh api user -q .login) - FULL_REPO="$OWNER/$REPO_NAME" - fi - - echo "" - echo "1) Private" - echo "2) Public" - read -rp "Visibility [1/2]: " VIS_CHOICE - - VISIBILITY="private" - [[ "$VIS_CHOICE" == "2" ]] && VISIBILITY="public" - - echo "" - info "Creating $FULL_REPO ($VISIBILITY)..." - - if [[ "$OWNER_TYPE" == "2" ]]; then - gh repo create "$FULL_REPO" --"$VISIBILITY" --clone - else - gh repo create "$REPO_NAME" --"$VISIBILITY" --clone - fi - - CLIENT_DIR="$(pwd)/$REPO_NAME" - ok "Repo created and cloned to $CLIENT_DIR" - -elif [[ "$REPO_CHOICE" == "2" ]]; then - # --- Existing repo --- - echo "1) Local clone (enter path)" - echo "2) GitHub repo (enter owner/repo to clone)" - read -rp "Choose [1/2]: " EXISTING_CHOICE - echo "" - - if [[ "$EXISTING_CHOICE" == "1" ]]; then - read -rp "Path to local repo: " CLIENT_DIR_INPUT - CLIENT_DIR=$(cd "$CLIENT_DIR_INPUT" && pwd) || err "Directory not found: $CLIENT_DIR_INPUT" - FULL_REPO=$(git -C "$CLIENT_DIR" remote get-url origin 2>/dev/null \ - | sed -E 's|.*github\.com[:/]||; s|\.git$||') \ - || err "Could not detect remote for $CLIENT_DIR" - else - read -rp "owner/repo: " FULL_REPO - [[ -z "$FULL_REPO" ]] && err "owner/repo cannot be empty" - info "Cloning $FULL_REPO..." - gh repo clone "$FULL_REPO" - REPO_NAME=$(echo "$FULL_REPO" | cut -d/ -f2) - CLIENT_DIR="$(pwd)/$REPO_NAME" - fi - - ok "Using repo: $FULL_REPO ($CLIENT_DIR)" -else - err "Invalid choice" -fi - -# --- Configure repo settings --- - -echo "" -info "Configuring repo settings..." - -gh api -X PATCH "/repos/$FULL_REPO" \ - -F allow_auto_merge=false \ - -F allow_squash_merge=true \ - -F delete_branch_on_merge=true \ - --silent - -ok "Auto-merge disabled, squash merge enabled, delete branch on merge enabled" - -# --- Write workflow files --- - -echo "" -info "Writing workflow files..." - -WORKFLOW_DIR="$CLIENT_DIR/.github/workflows" -TEMPLATE_DIR="$AGENTIC_ROOT/templates/workflows" -mkdir -p "$WORKFLOW_DIR" - -for template in "$TEMPLATE_DIR"/*.yml; do - filename=$(basename "$template") - sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" - echo -e " ${DIM}$filename${RESET}" -done - -ok "Created workflow files in $WORKFLOW_DIR/" - -# --- Secrets and variables (optional) --- - -echo "" -read -rp "Configure secrets and variables now? [y/N]: " SETUP_SECRETS - -if [[ "${SETUP_SECRETS,,}" == "y" ]]; then - echo "" - echo -e "${BOLD}Secrets${RESET}" - echo -e "${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" - echo -e "${DIM}It authenticates with your Claude Pro/Max subscription and outputs an OAuth token.${RESET}" - echo "" - - echo -n "CLAUDE_CODE_OAUTH_TOKEN: " - read -rs CLAUDE_TOKEN - echo "" - [[ -n "$CLAUDE_TOKEN" ]] && echo "$CLAUDE_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$FULL_REPO" - - echo -n "AGENTIC_BOT_TOKEN: " - read -rs BOT_TOKEN - echo "" - [[ -n "$BOT_TOKEN" ]] && echo "$BOT_TOKEN" | gh secret set AGENTIC_BOT_TOKEN --repo "$FULL_REPO" - - echo "" - echo -e "${BOLD}Variables${RESET}" - - read -rp "AGENTIC_BOT_NAME: " BOT_NAME - [[ -n "$BOT_NAME" ]] && gh variable set AGENTIC_BOT_NAME --repo "$FULL_REPO" --body "$BOT_NAME" - - read -rp "AGENTIC_BOT_EMAIL: " BOT_EMAIL - [[ -n "$BOT_EMAIL" ]] && gh variable set AGENTIC_BOT_EMAIL --repo "$FULL_REPO" --body "$BOT_EMAIL" - - ok "Secrets and variables configured" -else - echo "" - warn "Remember to set these before using the workflows:" - echo -e " ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" - echo "" - echo " Secrets:" - echo " gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO" - echo " gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO" - echo "" - echo " Variables:" - echo " gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body \"bot-username\"" - echo " gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body \"123+bot@users.noreply.github.com\"" -fi - -# --- Add bot as collaborator --- - -echo "" -echo -e "${BOLD}Bot repo access${RESET}" -echo "" -echo "The bot needs write access to this repo. Two approaches:" -echo "" -echo "1) Add bot directly as collaborator (simple, per-repo)" -echo "2) Skip — I've added the bot to an org team that has repo access" -echo "" -echo -e "${DIM}Tip: If you're using a GitHub org, add the bot to a team instead.${RESET}" -echo -e "${DIM}Any repo the team can access, the bot automatically can too —${RESET}" -echo -e "${DIM}no need to add it individually each time you run this script.${RESET}" -echo "" -read -rp "Choose [1/2]: " COLLAB_CHOICE - -if [[ "$COLLAB_CHOICE" == "1" ]]; then - # Use BOT_NAME if already set from secrets step, otherwise ask - if [[ -z "${BOT_NAME:-}" ]]; then - read -rp "Bot GitHub username: " BOT_NAME - fi - - if [[ -n "$BOT_NAME" ]]; then - info "Adding $BOT_NAME as collaborator..." - gh api -X PUT "/repos/$FULL_REPO/collaborators/$BOT_NAME" -f permission=write --silent - ok "$BOT_NAME added as collaborator (write access)" - - # Accept the invitation using the bot's token - if [[ -n "${BOT_TOKEN:-}" ]]; then - info "Accepting invitation as $BOT_NAME..." - INVITE_ID=$(GH_TOKEN="$BOT_TOKEN" gh api "/user/repository_invitations" --jq ".[] | select(.repository.full_name==\"$FULL_REPO\") | .id" 2>/dev/null || true) - if [[ -n "$INVITE_ID" ]]; then - GH_TOKEN="$BOT_TOKEN" gh api -X PATCH "/user/repository_invitations/$INVITE_ID" --silent - ok "Invitation accepted" - else - warn "Could not find invitation — the bot may need to accept manually at https://github.com/$FULL_REPO/invitations" - fi - else - warn "Bot token not available — the bot will need to accept the invitation at https://github.com/$FULL_REPO/invitations" - fi - fi -else - echo "" - warn "Remember to add the bot to the repo via your team/org settings." -fi - -# --- CLAUDE.md --- - -echo "" -echo -e "${BOLD}CLAUDE.md${RESET}" -echo "" - -if [[ -f "$CLIENT_DIR/CLAUDE.md" ]]; then - warn "CLAUDE.md already exists — skipping" -else - read -rp "Create a CLAUDE.md template? [Y/n]: " CREATE_CLAUDE_MD - if [[ "${CREATE_CLAUDE_MD,,}" != "n" ]]; then - read -rp "Project name: " PROJECT_NAME - read -rp "Short description: " PROJECT_DESC - read -rp "Tech stack (e.g. Next.js, TypeScript, Prisma): " TECH_STACK - - cat > "$CLIENT_DIR/CLAUDE.md" << MD -# ${PROJECT_NAME:-Project} - -${PROJECT_DESC:-A brief description of what this project does.} - -## Tech stack - -${TECH_STACK:-Describe the technologies used here.} - -## Project structure - -Describe the key directories and files here. - -## Development - -### Setup - -\`\`\`bash -# How to install dependencies and get running locally -\`\`\` - -### Common commands - -\`\`\`bash -# dev server, tests, linting, etc. -\`\`\` - -## Conventions - -- Describe coding conventions, naming patterns, etc. -MD - - ok "Created CLAUDE.md template — edit it to add more project context" - fi -fi - -# --- Commit and push --- - -echo "" -info "Committing and pushing..." - -cd "$CLIENT_DIR" -git add .github/workflows/ -[[ -f "CLAUDE.md" ]] && git add CLAUDE.md - -if git rev-parse HEAD &>/dev/null 2>&1; then - git commit -m "Add agentic workflow files and CLAUDE.md" - git push -else - # Empty repo — create initial commit - git checkout -b main - git commit -m "Initial commit: add agentic workflow files and CLAUDE.md" - git push -u origin main -fi - -ok "Changes committed and pushed" - -# --- Summary --- +STEPS="$SCRIPT_DIR/steps" +source "$STEPS/repo" +source "$STEPS/settings" +source "$STEPS/workflows" +source "$STEPS/secrets" +source "$STEPS/collaborator" +source "$STEPS/claude-md" +source "$STEPS/commit-push" echo "" echo -e "${BOLD}${GREEN}Setup complete!${RESET}" @@ -308,4 +42,5 @@ echo " Workflows: 4 files in .github/workflows/" echo "" echo "Flesh out CLAUDE.md with detailed project context, then verify the setup:" echo "" -echo " gh issue create --repo $FULL_REPO --title 'Test agentic setup' --body '@claude Are you there?'" +echo " gh issue create --repo $FULL_REPO \\" +echo " --title 'Test agentic setup' --body '@claude Are you there?'" diff --git a/bin/lib/common.sh b/bin/lib/common.sh index adeb160..914efde 100755 --- a/bin/lib/common.sh +++ b/bin/lib/common.sh @@ -13,3 +13,11 @@ info() { echo -e "${CYAN}$*${RESET}"; } ok() { echo -e "${GREEN}✓ $*${RESET}"; } warn() { echo -e "${YELLOW}$*${RESET}"; } err() { echo -e "${RED}Error: $*${RESET}" >&2; exit 1; } + +require_vars() { + local missing=() + for var in "$@"; do + [[ -n "${!var:-}" ]] || missing+=("$var") + done + (( ${#missing[@]} == 0 )) || err "Missing required variables: ${missing[*]}" +} diff --git a/bin/steps/claude-md b/bin/steps/claude-md new file mode 100755 index 0000000..d06c355 --- /dev/null +++ b/bin/steps/claude-md @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +require_vars CLIENT_DIR + +echo "" +echo -e "${BOLD}CLAUDE.md${RESET}" +echo "" + +if [[ -f "$CLIENT_DIR/CLAUDE.md" ]]; then + warn "CLAUDE.md already exists — skipping" +else + read -rp "Create a CLAUDE.md template? [Y/n]: " CREATE_CLAUDE_MD + if [[ "${CREATE_CLAUDE_MD,,}" != "n" ]]; then + read -rp "Project name: " PROJECT_NAME + read -rp "Short description: " PROJECT_DESC + read -rp "Tech stack (e.g. Next.js, TypeScript, Prisma): " TECH_STACK + + cat > "$CLIENT_DIR/CLAUDE.md" << MD +# ${PROJECT_NAME:-Project} + +${PROJECT_DESC:-A brief description of what this project does.} + +## Tech stack + +${TECH_STACK:-Describe the technologies used here.} + +## Project structure + +Describe the key directories and files here. + +## Development + +### Setup + +\`\`\`bash +# How to install dependencies and get running locally +\`\`\` + +### Common commands + +\`\`\`bash +# dev server, tests, linting, etc. +\`\`\` + +## Conventions + +- Describe coding conventions, naming patterns, etc. +MD + + ok "Created CLAUDE.md template — edit it to add more project context" + fi +fi diff --git a/bin/steps/collaborator b/bin/steps/collaborator new file mode 100755 index 0000000..cdbc076 --- /dev/null +++ b/bin/steps/collaborator @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +require_vars FULL_REPO + +echo "" +echo -e "${BOLD}Bot repo access${RESET}" +echo "" +echo "The bot needs write access to this repo. Two approaches:" +echo "" +echo "1) Add bot directly as collaborator (simple, per-repo)" +echo "2) Skip — I've added the bot to an org team that has repo access" +echo "" +echo -e "${DIM}Tip: If you're using a GitHub org, add the bot to a team instead.${RESET}" +echo -e "${DIM}Any repo the team can access, the bot automatically can too —${RESET}" +echo -e "${DIM}no need to add it individually each time you run this script.${RESET}" +echo "" +read -rp "Choose [1/2]: " COLLAB_CHOICE + +if [[ "$COLLAB_CHOICE" == "1" ]]; then + if [[ -z "${BOT_NAME:-}" ]]; then + read -rp "Bot GitHub username: " BOT_NAME + fi + + if [[ -n "$BOT_NAME" ]]; then + info "Adding $BOT_NAME as collaborator..." + gh api -X PUT "/repos/$FULL_REPO/collaborators/$BOT_NAME" -f permission=write --silent + ok "$BOT_NAME added as collaborator (write access)" + + if [[ -n "${BOT_TOKEN:-}" ]]; then + info "Accepting invitation as $BOT_NAME..." + INVITE_ID=$(GH_TOKEN="$BOT_TOKEN" gh api "/user/repository_invitations" --jq ".[] | select(.repository.full_name==\"$FULL_REPO\") | .id" 2>/dev/null || true) + if [[ -n "$INVITE_ID" ]]; then + GH_TOKEN="$BOT_TOKEN" gh api -X PATCH "/user/repository_invitations/$INVITE_ID" --silent + ok "Invitation accepted" + else + warn "Could not find invitation — the bot may need to accept manually at https://github.com/$FULL_REPO/invitations" + fi + else + warn "Bot token not available — the bot will need to accept the invitation at https://github.com/$FULL_REPO/invitations" + fi + fi +else + echo "" + warn "Remember to add the bot to the repo via your team/org settings." +fi diff --git a/bin/steps/commit-push b/bin/steps/commit-push new file mode 100755 index 0000000..ecaccaa --- /dev/null +++ b/bin/steps/commit-push @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +require_vars CLIENT_DIR + +echo "" +info "Committing and pushing..." + +cd "$CLIENT_DIR" +git add .github/workflows/ +[[ -f "CLAUDE.md" ]] && git add CLAUDE.md + +if git rev-parse HEAD &>/dev/null 2>&1; then + git commit -m "Add agentic workflow files and CLAUDE.md" + git push +else + git checkout -b main + git commit -m "Initial commit: add agentic workflow files and CLAUDE.md" + git push -u origin main +fi + +ok "Changes committed and pushed" diff --git a/bin/steps/repo b/bin/steps/repo new file mode 100755 index 0000000..45bf428 --- /dev/null +++ b/bin/steps/repo @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +echo "1) Create a new repo" +echo "2) Use an existing repo" +read -rp "Choose [1/2]: " REPO_CHOICE +echo "" + +if [[ "$REPO_CHOICE" == "1" ]]; then + read -rp "Repo name: " REPO_NAME + [[ -z "$REPO_NAME" ]] && err "Repo name cannot be empty" + + echo "" + echo "1) Personal account" + echo "2) Organization" + read -rp "Choose [1/2]: " OWNER_TYPE + + if [[ "$OWNER_TYPE" == "2" ]]; then + read -rp "Organization name: " ORG_NAME + [[ -z "$ORG_NAME" ]] && err "Organization name cannot be empty" + FULL_REPO="$ORG_NAME/$REPO_NAME" + else + OWNER=$(gh api user -q .login) + FULL_REPO="$OWNER/$REPO_NAME" + fi + + echo "" + echo "1) Private" + echo "2) Public" + read -rp "Visibility [1/2]: " VIS_CHOICE + + VISIBILITY="private" + [[ "$VIS_CHOICE" == "2" ]] && VISIBILITY="public" + + echo "" + info "Creating $FULL_REPO ($VISIBILITY)..." + + if [[ "$OWNER_TYPE" == "2" ]]; then + gh repo create "$FULL_REPO" --"$VISIBILITY" --clone + else + gh repo create "$REPO_NAME" --"$VISIBILITY" --clone + fi + + CLIENT_DIR="$(pwd)/$REPO_NAME" + ok "Repo created and cloned to $CLIENT_DIR" + +elif [[ "$REPO_CHOICE" == "2" ]]; then + echo "1) Local clone (enter path)" + echo "2) GitHub repo (enter owner/repo to clone)" + read -rp "Choose [1/2]: " EXISTING_CHOICE + echo "" + + if [[ "$EXISTING_CHOICE" == "1" ]]; then + read -rp "Path to local repo: " CLIENT_DIR_INPUT + CLIENT_DIR=$(cd "$CLIENT_DIR_INPUT" && pwd) || err "Directory not found: $CLIENT_DIR_INPUT" + FULL_REPO=$(git -C "$CLIENT_DIR" remote get-url origin 2>/dev/null \ + | sed -E 's|.*github\.com[:/]||; s|\.git$||') \ + || err "Could not detect remote for $CLIENT_DIR" + else + read -rp "owner/repo: " FULL_REPO + [[ -z "$FULL_REPO" ]] && err "owner/repo cannot be empty" + info "Cloning $FULL_REPO..." + gh repo clone "$FULL_REPO" + REPO_NAME=$(echo "$FULL_REPO" | cut -d/ -f2) + CLIENT_DIR="$(pwd)/$REPO_NAME" + fi + + ok "Using repo: $FULL_REPO ($CLIENT_DIR)" +else + err "Invalid choice" +fi + +export FULL_REPO CLIENT_DIR diff --git a/bin/steps/secrets b/bin/steps/secrets new file mode 100755 index 0000000..ddc8a3b --- /dev/null +++ b/bin/steps/secrets @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +require_vars FULL_REPO + +echo "" +read -rp "Configure secrets and variables now? [y/N]: " SETUP_SECRETS + +if [[ "${SETUP_SECRETS,,}" == "y" ]]; then + echo "" + echo -e "${BOLD}Secrets${RESET}" + echo -e "${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" + echo -e "${DIM}It authenticates with your Claude Pro/Max subscription and outputs an OAuth token.${RESET}" + echo "" + + echo -n "CLAUDE_CODE_OAUTH_TOKEN: " + read -rs CLAUDE_TOKEN + echo "" + [[ -n "$CLAUDE_TOKEN" ]] && echo "$CLAUDE_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$FULL_REPO" + + echo -n "AGENTIC_BOT_TOKEN: " + read -rs BOT_TOKEN + echo "" + [[ -n "$BOT_TOKEN" ]] && echo "$BOT_TOKEN" | gh secret set AGENTIC_BOT_TOKEN --repo "$FULL_REPO" + + echo "" + echo -e "${BOLD}Variables${RESET}" + + read -rp "AGENTIC_BOT_NAME: " BOT_NAME + [[ -n "$BOT_NAME" ]] && gh variable set AGENTIC_BOT_NAME --repo "$FULL_REPO" --body "$BOT_NAME" + + read -rp "AGENTIC_BOT_EMAIL: " BOT_EMAIL + [[ -n "$BOT_EMAIL" ]] && gh variable set AGENTIC_BOT_EMAIL --repo "$FULL_REPO" --body "$BOT_EMAIL" + + ok "Secrets and variables configured" + + export BOT_TOKEN BOT_NAME +else + echo "" + warn "Remember to set these before using the workflows:" + echo -e " ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" + echo "" + echo " Secrets:" + echo " gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO" + echo " gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO" + echo "" + echo " Variables:" + echo " gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body \"bot-username\"" + echo " gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body \"123+bot@users.noreply.github.com\"" +fi diff --git a/bin/steps/settings b/bin/steps/settings new file mode 100755 index 0000000..faf60a0 --- /dev/null +++ b/bin/steps/settings @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +require_vars FULL_REPO + +echo "" +info "Configuring repo settings..." + +gh api -X PATCH "/repos/$FULL_REPO" \ + -F allow_auto_merge=false \ + -F allow_squash_merge=true \ + -F delete_branch_on_merge=true \ + --silent + +ok "Auto-merge disabled, squash merge enabled, delete branch on merge enabled" diff --git a/bin/steps/workflows b/bin/steps/workflows new file mode 100755 index 0000000..af8c5b7 --- /dev/null +++ b/bin/steps/workflows @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" + +require_vars CLIENT_DIR AGENTIC_ROOT AGENTIC_REPO + +echo "" +info "Writing workflow files..." + +WORKFLOW_DIR="$CLIENT_DIR/.github/workflows" +TEMPLATE_DIR="$AGENTIC_ROOT/templates/workflows" +mkdir -p "$WORKFLOW_DIR" + +for template in "$TEMPLATE_DIR"/*.yml; do + filename=$(basename "$template") + sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" + echo -e " ${DIM}$filename${RESET}" +done + +ok "Created workflow files in $WORKFLOW_DIR/" From 9d99912d6efcaa733b8bfaebc4377e0a3e4872bf Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:38:59 +0000 Subject: [PATCH 05/26] feat: Add push confirmation to commit-push step The script previously pushed without asking. Now it shows staged files and asks Push to main? [Y/n] before proceeding. Declining leaves files staged but uncommitted so you can inspect or adjust. --- bin/steps/commit-push | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/steps/commit-push b/bin/steps/commit-push index ecaccaa..ee79afe 100755 --- a/bin/steps/commit-push +++ b/bin/steps/commit-push @@ -4,13 +4,21 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" require_vars CLIENT_DIR -echo "" -info "Committing and pushing..." - cd "$CLIENT_DIR" git add .github/workflows/ [[ -f "CLAUDE.md" ]] && git add CLAUDE.md +echo "" +echo -e "${BOLD}Ready to commit and push:${RESET}" +git -c color.status=always status --short +echo "" +read -rp "Push to main? [Y/n]: " CONFIRM_PUSH + +if [[ "${CONFIRM_PUSH,,}" == "n" ]]; then + warn "Skipped — files are staged but not committed" + return 0 2>/dev/null || exit 0 +fi + if git rev-parse HEAD &>/dev/null 2>&1; then git commit -m "Add agentic workflow files and CLAUDE.md" git push From b63402c4a2031b6dc2ed77bac570670893735dc8 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:39:18 +0000 Subject: [PATCH 06/26] feat: Add --repo flag for non-interactive install The interactive prompts made it impossible to re-run the install from CI or to skip questions you've already answered. The --repo and --path flags bypass the repo selection step, and -y skips all remaining prompts (secrets, collaborator, CLAUDE.md, push confirm). - Add flag parser to bin/install (--repo, --path, -y) - Skip interactive prompts in steps/repo when both vars are set - Gate prompts in secrets, collaborator, claude-md, commit-push on NONINTERACTIVE flag - Document non-interactive usage in README --- README.md | 6 ++++++ bin/install | 10 ++++++++++ bin/steps/claude-md | 2 ++ bin/steps/collaborator | 5 +++++ bin/steps/commit-push | 17 +++++++++-------- bin/steps/repo | 7 +++++++ bin/steps/secrets | 10 ++++++++++ 7 files changed, 49 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 195f856..9b8e897 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ This interactively: 7. Creates a CLAUDE.md template 8. Commits and pushes +For non-interactive use (CI or re-runs), pass `--repo` and `--path` to skip the prompts: + +```bash +bin/install --repo acme/my-app --path ./my-app +``` + The workflow files are thin callers that reference this repo's composite actions via `uses:`. When the agentic repo's actions are updated, all client repos pick up the changes automatically on the next workflow run. ### Workflow templates diff --git a/bin/install b/bin/install index 091e96d..5c0a890 100755 --- a/bin/install +++ b/bin/install @@ -3,6 +3,16 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" +# Parse flags +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) export FULL_REPO="$2"; shift 2 ;; + --path) export CLIENT_DIR="$2"; shift 2 ;; + -y) export NONINTERACTIVE=1; shift ;; + *) err "Unknown flag: $1" ;; + esac +done + for cmd in gh git jq; do command -v "$cmd" &>/dev/null || err "$cmd is not installed" done diff --git a/bin/steps/claude-md b/bin/steps/claude-md index d06c355..83e3a0a 100755 --- a/bin/steps/claude-md +++ b/bin/steps/claude-md @@ -10,6 +10,8 @@ echo "" if [[ -f "$CLIENT_DIR/CLAUDE.md" ]]; then warn "CLAUDE.md already exists — skipping" +elif [[ -n "${NONINTERACTIVE:-}" ]]; then + warn "Skipping CLAUDE.md creation (non-interactive mode)" else read -rp "Create a CLAUDE.md template? [Y/n]: " CREATE_CLAUDE_MD if [[ "${CREATE_CLAUDE_MD,,}" != "n" ]]; then diff --git a/bin/steps/collaborator b/bin/steps/collaborator index cdbc076..b452f72 100755 --- a/bin/steps/collaborator +++ b/bin/steps/collaborator @@ -4,6 +4,11 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" require_vars FULL_REPO +if [[ -n "${NONINTERACTIVE:-}" ]]; then + warn "Skipping collaborator setup (non-interactive mode)" + return 0 2>/dev/null || exit 0 +fi + echo "" echo -e "${BOLD}Bot repo access${RESET}" echo "" diff --git a/bin/steps/commit-push b/bin/steps/commit-push index ee79afe..22450b8 100755 --- a/bin/steps/commit-push +++ b/bin/steps/commit-push @@ -9,14 +9,15 @@ git add .github/workflows/ [[ -f "CLAUDE.md" ]] && git add CLAUDE.md echo "" -echo -e "${BOLD}Ready to commit and push:${RESET}" -git -c color.status=always status --short -echo "" -read -rp "Push to main? [Y/n]: " CONFIRM_PUSH - -if [[ "${CONFIRM_PUSH,,}" == "n" ]]; then - warn "Skipped — files are staged but not committed" - return 0 2>/dev/null || exit 0 +if [[ -z "${NONINTERACTIVE:-}" ]]; then + echo -e "${BOLD}Ready to commit and push:${RESET}" + git -c color.status=always status --short + echo "" + read -rp "Push to main? [Y/n]: " CONFIRM_PUSH + if [[ "${CONFIRM_PUSH,,}" == "n" ]]; then + warn "Skipped — files are staged but not committed" + return 0 2>/dev/null || exit 0 + fi fi if git rev-parse HEAD &>/dev/null 2>&1; then diff --git a/bin/steps/repo b/bin/steps/repo index 45bf428..48f404e 100755 --- a/bin/steps/repo +++ b/bin/steps/repo @@ -2,6 +2,13 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" +if [[ -n "${FULL_REPO:-}" && -n "${CLIENT_DIR:-}" ]]; then + CLIENT_DIR=$(cd "$CLIENT_DIR" && pwd) || err "Directory not found: $CLIENT_DIR" + ok "Using repo: $FULL_REPO ($CLIENT_DIR)" + export FULL_REPO CLIENT_DIR + return 0 2>/dev/null || exit 0 +fi + echo "1) Create a new repo" echo "2) Use an existing repo" read -rp "Choose [1/2]: " REPO_CHOICE diff --git a/bin/steps/secrets b/bin/steps/secrets index ddc8a3b..882999e 100755 --- a/bin/steps/secrets +++ b/bin/steps/secrets @@ -4,6 +4,16 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" require_vars FULL_REPO +if [[ -n "${NONINTERACTIVE:-}" ]]; then + warn "Skipping secrets setup (non-interactive mode)" + warn "Set secrets manually before using the workflows:" + echo " gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO" + echo " gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO" + echo " gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body \"bot-username\"" + echo " gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body \"123+bot@users.noreply.github.com\"" + return 0 2>/dev/null || exit 0 +fi + echo "" read -rp "Configure secrets and variables now? [y/N]: " SETUP_SECRETS From cd6bc118cad583643d62adfcb8c75cc53f66b077 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:49:09 +0000 Subject: [PATCH 07/26] feat: Warn before overwriting workflow files Running install against a repo that already has workflow files would silently clobber them. Now it lists existing files and asks Overwrite? [y/N] before proceeding. Non-interactive mode (-y) overwrites automatically with a log message. --- bin/steps/workflows | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bin/steps/workflows b/bin/steps/workflows index af8c5b7..014f7d5 100755 --- a/bin/steps/workflows +++ b/bin/steps/workflows @@ -11,6 +11,24 @@ WORKFLOW_DIR="$CLIENT_DIR/.github/workflows" TEMPLATE_DIR="$AGENTIC_ROOT/templates/workflows" mkdir -p "$WORKFLOW_DIR" +EXISTING=() +for template in "$TEMPLATE_DIR"/*.yml; do + filename=$(basename "$template") + [[ -f "$WORKFLOW_DIR/$filename" ]] && EXISTING+=("$filename") +done + +if (( ${#EXISTING[@]} > 0 )); then + warn "These workflow files already exist:" + for f in "${EXISTING[@]}"; do echo -e " ${DIM}$f${RESET}"; done + echo "" + if [[ -z "${NONINTERACTIVE:-}" ]]; then + read -rp "Overwrite? [y/N]: " OVERWRITE + [[ "${OVERWRITE,,}" != "y" ]] && { warn "Skipped workflow files"; return 0 2>/dev/null || exit 0; } + else + info "Overwriting (non-interactive mode)" + fi +fi + for template in "$TEMPLATE_DIR"/*.yml; do filename=$(basename "$template") sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" From 10d5f4c660493108240da7d174cb5a2143ec8776 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:53:27 +0000 Subject: [PATCH 08/26] feat: Run diagnostics after install Previously you had to remember to run bin/diagnostics manually after setup. Now the install orchestrator runs it automatically to verify settings, workflows, secrets, and bot access are all wired up correctly. --- bin/install | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/install b/bin/install index 5c0a890..b2b87c4 100755 --- a/bin/install +++ b/bin/install @@ -44,6 +44,8 @@ source "$STEPS/collaborator" source "$STEPS/claude-md" source "$STEPS/commit-push" +"$SCRIPT_DIR/diagnostics" "$FULL_REPO" + echo "" echo -e "${BOLD}${GREEN}Setup complete!${RESET}" echo "" From 77a1ec4547726237448b8bd18a38d05356a9fb79 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 20:55:49 +0000 Subject: [PATCH 09/26] style: Replace echo blocks with heredocs Multi-line echo sequences are harder to read and maintain than heredocs. Converted the display blocks in install, collaborator, and secrets to cat << EOF for clearer formatting. --- bin/install | 22 ++++++++++--------- bin/steps/collaborator | 26 ++++++++++++----------- bin/steps/secrets | 48 ++++++++++++++++++++++++------------------ 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/bin/install b/bin/install index b2b87c4..63b0c08 100755 --- a/bin/install +++ b/bin/install @@ -46,13 +46,15 @@ source "$STEPS/commit-push" "$SCRIPT_DIR/diagnostics" "$FULL_REPO" -echo "" -echo -e "${BOLD}${GREEN}Setup complete!${RESET}" -echo "" -echo " Repo: https://github.com/$FULL_REPO" -echo " Workflows: 4 files in .github/workflows/" -echo "" -echo "Flesh out CLAUDE.md with detailed project context, then verify the setup:" -echo "" -echo " gh issue create --repo $FULL_REPO \\" -echo " --title 'Test agentic setup' --body '@claude Are you there?'" +cat << EOF + +${BOLD}${GREEN}Setup complete!${RESET} + + Repo: https://github.com/$FULL_REPO + Workflows: 4 files in .github/workflows/ + +Flesh out CLAUDE.md with detailed project context, then verify the setup: + + gh issue create --repo $FULL_REPO \\ + --title 'Test agentic setup' --body '@claude Are you there?' +EOF diff --git a/bin/steps/collaborator b/bin/steps/collaborator index b452f72..e42686e 100755 --- a/bin/steps/collaborator +++ b/bin/steps/collaborator @@ -9,18 +9,20 @@ if [[ -n "${NONINTERACTIVE:-}" ]]; then return 0 2>/dev/null || exit 0 fi -echo "" -echo -e "${BOLD}Bot repo access${RESET}" -echo "" -echo "The bot needs write access to this repo. Two approaches:" -echo "" -echo "1) Add bot directly as collaborator (simple, per-repo)" -echo "2) Skip — I've added the bot to an org team that has repo access" -echo "" -echo -e "${DIM}Tip: If you're using a GitHub org, add the bot to a team instead.${RESET}" -echo -e "${DIM}Any repo the team can access, the bot automatically can too —${RESET}" -echo -e "${DIM}no need to add it individually each time you run this script.${RESET}" -echo "" +cat << EOF + +${BOLD}Bot repo access${RESET} + +The bot needs write access to this repo. Two approaches: + +1) Add bot directly as collaborator (simple, per-repo) +2) Skip — I've added the bot to an org team that has repo access + +${DIM}Tip: If you're using a GitHub org, add the bot to a team instead. +Any repo the team can access, the bot automatically can too — +no need to add it individually each time you run this script.${RESET} + +EOF read -rp "Choose [1/2]: " COLLAB_CHOICE if [[ "$COLLAB_CHOICE" == "1" ]]; then diff --git a/bin/steps/secrets b/bin/steps/secrets index 882999e..6fce073 100755 --- a/bin/steps/secrets +++ b/bin/steps/secrets @@ -6,11 +6,13 @@ require_vars FULL_REPO if [[ -n "${NONINTERACTIVE:-}" ]]; then warn "Skipping secrets setup (non-interactive mode)" - warn "Set secrets manually before using the workflows:" - echo " gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO" - echo " gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO" - echo " gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body \"bot-username\"" - echo " gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body \"123+bot@users.noreply.github.com\"" + cat << EOF + ${YELLOW}Set secrets manually before using the workflows:${RESET} + gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO + gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO + gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body "bot-username" + gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body "123+bot@users.noreply.github.com" +EOF return 0 2>/dev/null || exit 0 fi @@ -18,11 +20,13 @@ echo "" read -rp "Configure secrets and variables now? [y/N]: " SETUP_SECRETS if [[ "${SETUP_SECRETS,,}" == "y" ]]; then - echo "" - echo -e "${BOLD}Secrets${RESET}" - echo -e "${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" - echo -e "${DIM}It authenticates with your Claude Pro/Max subscription and outputs an OAuth token.${RESET}" - echo "" + cat << EOF + +${BOLD}Secrets${RESET} +${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN. +It authenticates with your Claude Pro/Max subscription and outputs an OAuth token.${RESET} + +EOF echo -n "CLAUDE_CODE_OAUTH_TOKEN: " read -rs CLAUDE_TOKEN @@ -47,15 +51,17 @@ if [[ "${SETUP_SECRETS,,}" == "y" ]]; then export BOT_TOKEN BOT_NAME else - echo "" - warn "Remember to set these before using the workflows:" - echo -e " ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET}" - echo "" - echo " Secrets:" - echo " gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO" - echo " gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO" - echo "" - echo " Variables:" - echo " gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body \"bot-username\"" - echo " gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body \"123+bot@users.noreply.github.com\"" + cat << EOF + +${YELLOW}Remember to set these before using the workflows:${RESET} + ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET} + + Secrets: + gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO + gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO + + Variables: + gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body "bot-username" + gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body "123+bot@users.noreply.github.com" +EOF fi From a40c0cae26d6644092118b68402cc946e664c4fc Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:16:29 +0000 Subject: [PATCH 10/26] refactor: Extract functions across install cluster Flat procedural blocks replaced with named functions so readers can scan intent without parsing implementation. Shared helpers added to common.sh: - require_commands, require_gh_auth, detect_remote Local functions extracted in: - diagnostics: 7 check functions, main body is now a checklist - collaborator: add_collaborator, accept_invitation - workflows: find_existing_workflows, confirm_overwrite, render_templates - commit-push: commit_and_push --- bin/diagnostics | 231 +++++++++++++++++++++-------------------- bin/install | 11 +- bin/lib/common.sh | 16 +++ bin/steps/collaborator | 36 ++++--- bin/steps/commit-push | 21 ++-- bin/steps/repo | 3 +- bin/steps/workflows | 50 +++++---- 7 files changed, 205 insertions(+), 163 deletions(-) diff --git a/bin/diagnostics b/bin/diagnostics index f1f034d..b377745 100755 --- a/bin/diagnostics +++ b/bin/diagnostics @@ -8,125 +8,111 @@ ok() { echo -e " ${GREEN}✓${RESET} $*"; } fail() { echo -e " ${RED}✗${RESET} $*"; } warn() { echo -e " ${YELLOW}!${RESET} $*"; } -# --- Get repo --- - -if [[ -n "${1:-}" ]]; then - REPO="$1" -else - read -rp "Repo to check (owner/repo): " REPO -fi - -echo "" -echo -e "${BOLD}Diagnostics for $REPO${RESET}" -echo "" - -# --- Check gh auth --- - -echo -e "${BOLD}GitHub CLI${RESET}" -if gh auth status &>/dev/null 2>&1; then - ok "gh authenticated" -else - fail "gh not authenticated — run 'gh auth login'" -fi -echo "" - -# --- Check repo exists and is accessible --- - -echo -e "${BOLD}Repo access${RESET}" -if gh api "/repos/$REPO" &>/dev/null 2>&1; then - ok "Repo $REPO is accessible" -else - fail "Cannot access $REPO — check the repo exists and you have access" -fi -echo "" - -# --- Check repo settings --- - -echo -e "${BOLD}Repo settings${RESET}" -SETTINGS=$(gh api "/repos/$REPO" --jq '{auto_merge: .allow_auto_merge, squash: .allow_squash_merge, delete_branch: .delete_branch_on_merge}' 2>/dev/null || echo "{}") - -if echo "$SETTINGS" | jq -e '.auto_merge == false' &>/dev/null; then - ok "Auto-merge disabled" -else - fail "Auto-merge should be disabled" -fi - -if echo "$SETTINGS" | jq -e '.squash == true' &>/dev/null; then - ok "Squash merge enabled" -else - fail "Squash merge should be enabled" -fi - -if echo "$SETTINGS" | jq -e '.delete_branch == true' &>/dev/null; then - ok "Delete branch on merge enabled" -else - fail "Delete branch on merge should be enabled" -fi -echo "" - -# --- Check workflow files --- - -echo -e "${BOLD}Workflow files${RESET}" -for wf in gh-commented gh-start-work pr-changes-requested pr-approved; do - if gh api "/repos/$REPO/contents/.github/workflows/${wf}.yml" &>/dev/null 2>&1; then - ok "${wf}.yml" +check_gh_auth() { + echo -e "${BOLD}GitHub CLI${RESET}" + if gh auth status &>/dev/null 2>&1; then + ok "gh authenticated" else - fail "${wf}.yml not found" + fail "gh not authenticated — run 'gh auth login'" fi -done -echo "" + echo "" +} -# --- Check secrets --- - -echo -e "${BOLD}Secrets${RESET}" -SECRETS=$(gh secret list --repo "$REPO" 2>/dev/null || echo "") - -if echo "$SECRETS" | grep -q "CLAUDE_CODE_OAUTH_TOKEN"; then - ok "CLAUDE_CODE_OAUTH_TOKEN is set" -else - fail "CLAUDE_CODE_OAUTH_TOKEN not set" -fi - -if echo "$SECRETS" | grep -q "AGENTIC_BOT_TOKEN"; then - ok "AGENTIC_BOT_TOKEN is set" -else - fail "AGENTIC_BOT_TOKEN not set" -fi -echo "" - -# --- Check variables --- - -echo -e "${BOLD}Variables${RESET}" -VARS=$(gh variable list --repo "$REPO" 2>/dev/null || echo "") - -if echo "$VARS" | grep -q "AGENTIC_BOT_NAME"; then - BOT_NAME=$(echo "$VARS" | grep "AGENTIC_BOT_NAME" | awk '{print $2}') - ok "AGENTIC_BOT_NAME = $BOT_NAME" -else - fail "AGENTIC_BOT_NAME not set" - BOT_NAME="" -fi +check_repo_access() { + echo -e "${BOLD}Repo access${RESET}" + if gh api "/repos/$1" &>/dev/null 2>&1; then + ok "Repo $1 is accessible" + else + fail "Cannot access $1 — check the repo exists and you have access" + fi + echo "" +} -if echo "$VARS" | grep -q "AGENTIC_BOT_EMAIL"; then - ok "AGENTIC_BOT_EMAIL is set" -else - fail "AGENTIC_BOT_EMAIL not set" -fi -echo "" +check_repo_settings() { + echo -e "${BOLD}Repo settings${RESET}" + local settings + settings=$(gh api "/repos/$1" --jq '{auto_merge: .allow_auto_merge, squash: .allow_squash_merge, delete_branch: .delete_branch_on_merge}' 2>/dev/null || echo "{}") -# --- Test bot token and access --- + if echo "$settings" | jq -e '.auto_merge == false' &>/dev/null; then + ok "Auto-merge disabled" + else + fail "Auto-merge should be disabled" + fi + if echo "$settings" | jq -e '.squash == true' &>/dev/null; then + ok "Squash merge enabled" + else + fail "Squash merge should be enabled" + fi + if echo "$settings" | jq -e '.delete_branch == true' &>/dev/null; then + ok "Delete branch on merge enabled" + else + fail "Delete branch on merge should be enabled" + fi + echo "" +} + +check_workflow_files() { + echo -e "${BOLD}Workflow files${RESET}" + for wf in gh-commented gh-start-work pr-changes-requested pr-approved; do + if gh api "/repos/$1/contents/.github/workflows/${wf}.yml" &>/dev/null 2>&1; then + ok "${wf}.yml" + else + fail "${wf}.yml not found" + fi + done + echo "" +} + +check_secrets() { + echo -e "${BOLD}Secrets${RESET}" + local secrets + secrets=$(gh secret list --repo "$1" 2>/dev/null || echo "") + + if echo "$secrets" | grep -q "CLAUDE_CODE_OAUTH_TOKEN"; then + ok "CLAUDE_CODE_OAUTH_TOKEN is set" + else + fail "CLAUDE_CODE_OAUTH_TOKEN not set" + fi + if echo "$secrets" | grep -q "AGENTIC_BOT_TOKEN"; then + ok "AGENTIC_BOT_TOKEN is set" + else + fail "AGENTIC_BOT_TOKEN not set" + fi + echo "" +} -echo -e "${BOLD}Bot repo access${RESET}" -echo -n "Paste AGENTIC_BOT_TOKEN to test (or press Enter to skip): " -read -rs TEST_TOKEN -echo "" +check_variables() { + echo -e "${BOLD}Variables${RESET}" + local vars + vars=$(gh variable list --repo "$1" 2>/dev/null || echo "") -if [[ -n "$TEST_TOKEN" ]]; then - if GH_TOKEN="$TEST_TOKEN" gh api "/repos/$REPO" &>/dev/null 2>&1; then - ok "Bot token can access $REPO" + if echo "$vars" | grep -q "AGENTIC_BOT_NAME"; then + BOT_NAME=$(echo "$vars" | grep "AGENTIC_BOT_NAME" | awk '{print $2}') + ok "AGENTIC_BOT_NAME = $BOT_NAME" + else + fail "AGENTIC_BOT_NAME not set" + BOT_NAME="" + fi + if echo "$vars" | grep -q "AGENTIC_BOT_EMAIL"; then + ok "AGENTIC_BOT_EMAIL is set" else - fail "Bot token cannot access $REPO" - cat </dev/null 2>&1; then + ok "Bot token can access $1" + else + fail "Bot token cannot access $1" + cat </dev/null || err "$cmd is not installed" -done - -gh auth status &>/dev/null 2>&1 \ - || err "Not authenticated with GitHub CLI. Run 'gh auth login' first." +require_commands gh git jq +require_gh_auth SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -AGENTIC_REMOTE=$(git -C "$AGENTIC_ROOT" remote get-url origin 2>/dev/null) \ +AGENTIC_REPO=$(detect_remote "$AGENTIC_ROOT") \ || err "Could not detect agentic repo remote" -AGENTIC_REPO=$(echo "$AGENTIC_REMOTE" | sed -E 's|.*github\.com[:/]||; s|\.git$||') export SCRIPT_DIR AGENTIC_ROOT AGENTIC_REPO echo "" diff --git a/bin/lib/common.sh b/bin/lib/common.sh index 914efde..a73e6a7 100755 --- a/bin/lib/common.sh +++ b/bin/lib/common.sh @@ -21,3 +21,19 @@ require_vars() { done (( ${#missing[@]} == 0 )) || err "Missing required variables: ${missing[*]}" } + +require_commands() { + for cmd in "$@"; do + command -v "$cmd" &>/dev/null || err "$cmd is not installed" + done +} + +require_gh_auth() { + gh auth status &>/dev/null 2>&1 \ + || err "Not authenticated with GitHub CLI. Run 'gh auth login' first." +} + +detect_remote() { + git -C "$1" remote get-url origin 2>/dev/null \ + | sed -E 's|.*github\.com[:/]||; s|\.git$||' +} diff --git a/bin/steps/collaborator b/bin/steps/collaborator index e42686e..3c6ea4b 100755 --- a/bin/steps/collaborator +++ b/bin/steps/collaborator @@ -4,6 +4,28 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" require_vars FULL_REPO +add_collaborator() { + info "Adding $BOT_NAME as collaborator..." + gh api -X PUT "/repos/$FULL_REPO/collaborators/$BOT_NAME" \ + -f permission=write --silent + ok "$BOT_NAME added as collaborator (write access)" +} + +accept_invitation() { + info "Accepting invitation as $BOT_NAME..." + local invite_id + invite_id=$(GH_TOKEN="$BOT_TOKEN" gh api "/user/repository_invitations" \ + --jq ".[] | select(.repository.full_name==\"$FULL_REPO\") | .id" \ + 2>/dev/null || true) + if [[ -n "$invite_id" ]]; then + GH_TOKEN="$BOT_TOKEN" gh api -X PATCH \ + "/user/repository_invitations/$invite_id" --silent + ok "Invitation accepted" + else + warn "Could not find invitation — the bot may need to accept manually at https://github.com/$FULL_REPO/invitations" + fi +} + if [[ -n "${NONINTERACTIVE:-}" ]]; then warn "Skipping collaborator setup (non-interactive mode)" return 0 2>/dev/null || exit 0 @@ -31,19 +53,9 @@ if [[ "$COLLAB_CHOICE" == "1" ]]; then fi if [[ -n "$BOT_NAME" ]]; then - info "Adding $BOT_NAME as collaborator..." - gh api -X PUT "/repos/$FULL_REPO/collaborators/$BOT_NAME" -f permission=write --silent - ok "$BOT_NAME added as collaborator (write access)" - + add_collaborator if [[ -n "${BOT_TOKEN:-}" ]]; then - info "Accepting invitation as $BOT_NAME..." - INVITE_ID=$(GH_TOKEN="$BOT_TOKEN" gh api "/user/repository_invitations" --jq ".[] | select(.repository.full_name==\"$FULL_REPO\") | .id" 2>/dev/null || true) - if [[ -n "$INVITE_ID" ]]; then - GH_TOKEN="$BOT_TOKEN" gh api -X PATCH "/user/repository_invitations/$INVITE_ID" --silent - ok "Invitation accepted" - else - warn "Could not find invitation — the bot may need to accept manually at https://github.com/$FULL_REPO/invitations" - fi + accept_invitation else warn "Bot token not available — the bot will need to accept the invitation at https://github.com/$FULL_REPO/invitations" fi diff --git a/bin/steps/commit-push b/bin/steps/commit-push index 22450b8..23c996e 100755 --- a/bin/steps/commit-push +++ b/bin/steps/commit-push @@ -4,6 +4,17 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" require_vars CLIENT_DIR +commit_and_push() { + if git rev-parse HEAD &>/dev/null 2>&1; then + git commit -m "Add agentic workflow files and CLAUDE.md" + git push + else + git checkout -b main + git commit -m "Initial commit: add agentic workflow files and CLAUDE.md" + git push -u origin main + fi +} + cd "$CLIENT_DIR" git add .github/workflows/ [[ -f "CLAUDE.md" ]] && git add CLAUDE.md @@ -20,13 +31,5 @@ if [[ -z "${NONINTERACTIVE:-}" ]]; then fi fi -if git rev-parse HEAD &>/dev/null 2>&1; then - git commit -m "Add agentic workflow files and CLAUDE.md" - git push -else - git checkout -b main - git commit -m "Initial commit: add agentic workflow files and CLAUDE.md" - git push -u origin main -fi - +commit_and_push ok "Changes committed and pushed" diff --git a/bin/steps/repo b/bin/steps/repo index 48f404e..5e9f749 100755 --- a/bin/steps/repo +++ b/bin/steps/repo @@ -61,8 +61,7 @@ elif [[ "$REPO_CHOICE" == "2" ]]; then if [[ "$EXISTING_CHOICE" == "1" ]]; then read -rp "Path to local repo: " CLIENT_DIR_INPUT CLIENT_DIR=$(cd "$CLIENT_DIR_INPUT" && pwd) || err "Directory not found: $CLIENT_DIR_INPUT" - FULL_REPO=$(git -C "$CLIENT_DIR" remote get-url origin 2>/dev/null \ - | sed -E 's|.*github\.com[:/]||; s|\.git$||') \ + FULL_REPO=$(detect_remote "$CLIENT_DIR") \ || err "Could not detect remote for $CLIENT_DIR" else read -rp "owner/repo: " FULL_REPO diff --git a/bin/steps/workflows b/bin/steps/workflows index 014f7d5..b291340 100755 --- a/bin/steps/workflows +++ b/bin/steps/workflows @@ -4,35 +4,45 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/common.sh" require_vars CLIENT_DIR AGENTIC_ROOT AGENTIC_REPO -echo "" -info "Writing workflow files..." - WORKFLOW_DIR="$CLIENT_DIR/.github/workflows" TEMPLATE_DIR="$AGENTIC_ROOT/templates/workflows" -mkdir -p "$WORKFLOW_DIR" -EXISTING=() -for template in "$TEMPLATE_DIR"/*.yml; do - filename=$(basename "$template") - [[ -f "$WORKFLOW_DIR/$filename" ]] && EXISTING+=("$filename") -done +find_existing_workflows() { + EXISTING=() + for template in "$TEMPLATE_DIR"/*.yml; do + local filename=$(basename "$template") + [[ -f "$WORKFLOW_DIR/$filename" ]] && EXISTING+=("$filename") + done +} -if (( ${#EXISTING[@]} > 0 )); then +confirm_overwrite() { warn "These workflow files already exist:" for f in "${EXISTING[@]}"; do echo -e " ${DIM}$f${RESET}"; done echo "" - if [[ -z "${NONINTERACTIVE:-}" ]]; then - read -rp "Overwrite? [y/N]: " OVERWRITE - [[ "${OVERWRITE,,}" != "y" ]] && { warn "Skipped workflow files"; return 0 2>/dev/null || exit 0; } - else + if [[ -n "${NONINTERACTIVE:-}" ]]; then info "Overwriting (non-interactive mode)" + return 0 fi -fi + read -rp "Overwrite? [y/N]: " OVERWRITE + [[ "${OVERWRITE,,}" == "y" ]] +} + +render_templates() { + for template in "$TEMPLATE_DIR"/*.yml; do + local filename=$(basename "$template") + sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" + echo -e " ${DIM}$filename${RESET}" + done +} -for template in "$TEMPLATE_DIR"/*.yml; do - filename=$(basename "$template") - sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" - echo -e " ${DIM}$filename${RESET}" -done +echo "" +info "Writing workflow files..." +mkdir -p "$WORKFLOW_DIR" + +find_existing_workflows +if (( ${#EXISTING[@]} > 0 )); then + confirm_overwrite || { warn "Skipped workflow files"; return 0 2>/dev/null || exit 0; } +fi +render_templates ok "Created workflow files in $WORKFLOW_DIR/" From ba8d268f2fddf1d6b51fb751562b92c076626429 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:18:40 +0000 Subject: [PATCH 11/26] refactor: Split common.sh into output.sh and guards.sh A single grab-bag file made it hard to know what you were sourcing. Two focused files let the source line communicate intent: output.sh for colors and messages, guards.sh for precondition checks and environment detection. - output.sh: colors, info, ok, warn, err - guards.sh: sources output.sh, adds require_vars, require_commands, require_gh_auth, detect_remote - diagnostics and prerequisites source output.sh directly - All other scripts source guards.sh --- bin/diagnostics | 2 +- bin/install | 2 +- bin/lib/{common.sh => guards.sh} | 15 ++------------- bin/lib/output.sh | 15 +++++++++++++++ bin/prerequisites | 2 +- bin/steps/claude-md | 2 +- bin/steps/collaborator | 2 +- bin/steps/commit-push | 2 +- bin/steps/repo | 2 +- bin/steps/secrets | 2 +- bin/steps/settings | 2 +- bin/steps/workflows | 2 +- 12 files changed, 27 insertions(+), 23 deletions(-) rename bin/lib/{common.sh => guards.sh} (61%) mode change 100755 => 100644 create mode 100644 bin/lib/output.sh diff --git a/bin/diagnostics b/bin/diagnostics index b377745..a16c982 100755 --- a/bin/diagnostics +++ b/bin/diagnostics @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/output.sh" # Override helpers for diagnostic checklist format ok() { echo -e " ${GREEN}✓${RESET} $*"; } diff --git a/bin/install b/bin/install index 8620e9e..eaf877f 100755 --- a/bin/install +++ b/bin/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/guards.sh" # Parse flags while [[ $# -gt 0 ]]; do diff --git a/bin/lib/common.sh b/bin/lib/guards.sh old mode 100755 new mode 100644 similarity index 61% rename from bin/lib/common.sh rename to bin/lib/guards.sh index a73e6a7..e16abf7 --- a/bin/lib/common.sh +++ b/bin/lib/guards.sh @@ -1,18 +1,7 @@ #!/usr/bin/env bash -# Shared colors and helpers for agentic scripts +# Precondition checks and environment detection -BOLD=$'\033[1m' -DIM=$'\033[2m' -GREEN=$'\033[0;32m' -YELLOW=$'\033[0;33m' -RED=$'\033[0;31m' -CYAN=$'\033[0;36m' -RESET=$'\033[0m' - -info() { echo -e "${CYAN}$*${RESET}"; } -ok() { echo -e "${GREEN}✓ $*${RESET}"; } -warn() { echo -e "${YELLOW}$*${RESET}"; } -err() { echo -e "${RED}Error: $*${RESET}" >&2; exit 1; } +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/output.sh" require_vars() { local missing=() diff --git a/bin/lib/output.sh b/bin/lib/output.sh new file mode 100644 index 0000000..0be0478 --- /dev/null +++ b/bin/lib/output.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Colors and message functions for agentic scripts + +BOLD=$'\033[1m' +DIM=$'\033[2m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[0;33m' +RED=$'\033[0;31m' +CYAN=$'\033[0;36m' +RESET=$'\033[0m' + +info() { echo -e "${CYAN}$*${RESET}"; } +ok() { echo -e "${GREEN}✓ $*${RESET}"; } +warn() { echo -e "${YELLOW}$*${RESET}"; } +err() { echo -e "${RED}Error: $*${RESET}" >&2; exit 1; } diff --git a/bin/prerequisites b/bin/prerequisites index 578d4c0..2a527c9 100755 --- a/bin/prerequisites +++ b/bin/prerequisites @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/common.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/output.sh" cat < Date: Thu, 19 Feb 2026 21:20:17 +0000 Subject: [PATCH 12/26] fix: Derive workflow names from templates dir Diagnostics hardcoded four workflow names while the install step iterated the templates directory. Adding a fifth workflow template would silently skip the diagnostics check. - Iterate templates/workflows/*.yml instead of a fixed list - Same source of truth as bin/steps/workflows --- bin/diagnostics | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/diagnostics b/bin/diagnostics index a16c982..8762d7f 100755 --- a/bin/diagnostics +++ b/bin/diagnostics @@ -1,7 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/output.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATE_DIR="$SCRIPT_DIR/../templates/workflows" + +source "$SCRIPT_DIR/lib/output.sh" # Override helpers for diagnostic checklist format ok() { echo -e " ${GREEN}✓${RESET} $*"; } @@ -53,7 +56,8 @@ check_repo_settings() { check_workflow_files() { echo -e "${BOLD}Workflow files${RESET}" - for wf in gh-commented gh-start-work pr-changes-requested pr-approved; do + for template in "$TEMPLATE_DIR"/*.yml; do + local wf=$(basename "$template" .yml) if gh api "/repos/$1/contents/.github/workflows/${wf}.yml" &>/dev/null 2>&1; then ok "${wf}.yml" else From 413f12a05e00cebae14b272bf16268a1a6b53b4b Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:21:15 +0000 Subject: [PATCH 13/26] fix: Escape sed special chars in render_templates Sed treats & as "matched text" and \ as escape in the replacement string. If AGENTIC_REPO ever contained these characters, the rendered YAML would be silently corrupted. - Escape \ and & before passing to sed replacement --- bin/steps/workflows | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/steps/workflows b/bin/steps/workflows index da108f3..fde94f8 100755 --- a/bin/steps/workflows +++ b/bin/steps/workflows @@ -28,9 +28,11 @@ confirm_overwrite() { } render_templates() { + local safe_repo="${AGENTIC_REPO//\\/\\\\}" + safe_repo="${safe_repo//&/\\&}" for template in "$TEMPLATE_DIR"/*.yml; do local filename=$(basename "$template") - sed "s|__AGENTIC_REPO__|$AGENTIC_REPO|g" "$template" > "$WORKFLOW_DIR/$filename" + sed "s|__AGENTIC_REPO__|$safe_repo|g" "$template" > "$WORKFLOW_DIR/$filename" echo -e " ${DIM}$filename${RESET}" done } From e9a0b803cf4bd19905ef2d942522a7af3b8b6bfd Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:23:06 +0000 Subject: [PATCH 14/26] fix: Surface API errors in settings step The --silent flag suppressed both the response body and error messages. A permissions failure would kill the script with no explanation of what went wrong. - Redirect stdout to suppress success JSON - Let stderr through for gh error details - Add explicit error message on failure --- bin/steps/settings | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/steps/settings b/bin/steps/settings index 367f783..2903e19 100755 --- a/bin/steps/settings +++ b/bin/steps/settings @@ -11,6 +11,7 @@ gh api -X PATCH "/repos/$FULL_REPO" \ -F allow_auto_merge=false \ -F allow_squash_merge=true \ -F delete_branch_on_merge=true \ - --silent + > /dev/null \ + || err "Failed to update repo settings — check permissions" ok "Auto-merge disabled, squash merge enabled, delete branch on merge enabled" From 507ea756d41570ec94343bd9f42c21649aed7f00 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:24:12 +0000 Subject: [PATCH 15/26] refactor: Deduplicate manual commands in secrets The non-interactive path and the "skip" path both printed the same gh secret/variable set commands with slightly different formatting. Now both call print_manual_commands with their own intro message. - Extract print_manual_commands function - Non-interactive path also gets the setup-token hint and Secrets/Variables headings it was missing --- bin/steps/secrets | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/bin/steps/secrets b/bin/steps/secrets index 9cef90f..f5b0344 100755 --- a/bin/steps/secrets +++ b/bin/steps/secrets @@ -4,15 +4,23 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/guards.sh" require_vars FULL_REPO -if [[ -n "${NONINTERACTIVE:-}" ]]; then - warn "Skipping secrets setup (non-interactive mode)" +print_manual_commands() { cat << EOF - ${YELLOW}Set secrets manually before using the workflows:${RESET} + ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET} + + Secrets: gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO + + Variables: gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body "bot-username" gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body "123+bot@users.noreply.github.com" EOF +} + +if [[ -n "${NONINTERACTIVE:-}" ]]; then + warn "Skipping secrets setup (non-interactive mode)" + print_manual_commands return 0 2>/dev/null || exit 0 fi @@ -51,17 +59,7 @@ EOF export BOT_TOKEN BOT_NAME else - cat << EOF - -${YELLOW}Remember to set these before using the workflows:${RESET} - ${DIM}Run 'claude setup-token' to get your CLAUDE_CODE_OAUTH_TOKEN.${RESET} - - Secrets: - gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo $FULL_REPO - gh secret set AGENTIC_BOT_TOKEN --repo $FULL_REPO - - Variables: - gh variable set AGENTIC_BOT_NAME --repo $FULL_REPO --body "bot-username" - gh variable set AGENTIC_BOT_EMAIL --repo $FULL_REPO --body "123+bot@users.noreply.github.com" -EOF + echo "" + warn "Remember to set these before using the workflows:" + print_manual_commands fi From 3d17f0da3543413317ea111ef215f424803a0b10 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:26:53 +0000 Subject: [PATCH 16/26] fix: Derive workflow count from templates dir The success message hardcoded "4 files" which would be wrong if a template were added or removed. --- bin/install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install b/bin/install index eaf877f..72cc689 100755 --- a/bin/install +++ b/bin/install @@ -46,7 +46,7 @@ cat << EOF ${BOLD}${GREEN}Setup complete!${RESET} Repo: https://github.com/$FULL_REPO - Workflows: 4 files in .github/workflows/ + Workflows: $(ls -1 "$AGENTIC_ROOT"/templates/workflows/*.yml | wc -l | tr -d ' ') files in .github/workflows/ Flesh out CLAUDE.md with detailed project context, then verify the setup: From 8b57d99ed5e4b0b2e3bfb1f3128bc9c4bd2f30d0 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:32:24 +0000 Subject: [PATCH 17/26] fix: Fix silent exit in find_existing_workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The && short-circuit on the last loop iteration leaked a non-zero exit code when no matching file existed. This became the function's return value, which set -e caught and killed the script silently after "Writing workflow files...". The pattern was safe inline but broke when extracted into a function — the function's exit status matters under set -e. - Replace [[ -f ]] && append with if/then/fi which always returns 0 from the if statement --- bin/steps/workflows | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/steps/workflows b/bin/steps/workflows index fde94f8..8030f6c 100755 --- a/bin/steps/workflows +++ b/bin/steps/workflows @@ -11,7 +11,9 @@ find_existing_workflows() { EXISTING=() for template in "$TEMPLATE_DIR"/*.yml; do local filename=$(basename "$template") - [[ -f "$WORKFLOW_DIR/$filename" ]] && EXISTING+=("$filename") + if [[ -f "$WORKFLOW_DIR/$filename" ]]; then + EXISTING+=("$filename") + fi done } From 6c13d6e6fc6722f5bb7ce5a4fddefc66743c210a Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:38:22 +0000 Subject: [PATCH 18/26] docs: Document how to resume interrupted install --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b8e897..5dd157f 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,14 @@ This interactively: 7. Creates a CLAUDE.md template 8. Commits and pushes -For non-interactive use (CI or re-runs), pass `--repo` and `--path` to skip the prompts: +For non-interactive use, CI, or to resume an interrupted install, pass `--repo` and `--path` to skip the repo step: ```bash bin/install --repo acme/my-app --path ./my-app ``` +Already-configured settings are applied idempotently, and the workflow step will ask before overwriting existing files. This makes it safe to re-run after a failure. + The workflow files are thin callers that reference this repo's composite actions via `uses:`. When the agentic repo's actions are updated, all client repos pick up the changes automatically on the next workflow run. ### Workflow templates From e2d7fb31be3e73d0d42a2552f1ca6f13f948887a Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:57:09 +0000 Subject: [PATCH 19/26] feat: Load secrets from .env for unattended re-runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting up multiple client repos meant re-typing the same 4 secrets each time. Now drop a .env in the agentic root and the install script picks it up — no prompts, no typos. - bin/install sources .env before running steps - bin/steps/secrets checks env vars before prompting - bin/diagnostics uses AGENTIC_BOT_TOKEN from env if available - Add .env.example with the 4 required values - Document .env usage in README --- .env.example | 4 +++ README.md | 13 ++++++++ bin/diagnostics | 9 ++++-- bin/install | 2 ++ bin/steps/secrets | 80 ++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0b0b669 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" +AGENTIC_BOT_TOKEN="your-bot-pat" +AGENTIC_BOT_NAME="your-bot-username" +AGENTIC_BOT_EMAIL="123+bot@users.noreply.github.com" diff --git a/README.md b/README.md index 5dd157f..9a7019b 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,19 @@ bin/install --repo acme/my-app --path ./my-app Already-configured settings are applied idempotently, and the workflow step will ask before overwriting existing files. This makes it safe to re-run after a failure. +### Skipping prompts with `.env` + +To avoid re-typing secrets when setting up multiple client repos, create a `.env` file in the agentic repo root: + +```bash +CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" +AGENTIC_BOT_TOKEN="your-bot-pat" +AGENTIC_BOT_NAME="your-bot-username" +AGENTIC_BOT_EMAIL="123+bot@users.noreply.github.com" +``` + +The install script loads `.env` automatically if it exists. With all four values set, secrets are configured without prompts. The file is already in `.gitignore`. + The workflow files are thin callers that reference this repo's composite actions via `uses:`. When the agentic repo's actions are updated, all client repos pick up the changes automatically on the next workflow run. ### Workflow templates diff --git a/bin/diagnostics b/bin/diagnostics index 8762d7f..fc59874 100755 --- a/bin/diagnostics +++ b/bin/diagnostics @@ -107,9 +107,12 @@ check_variables() { check_bot_access() { echo -e "${BOLD}Bot repo access${RESET}" - echo -n "Paste AGENTIC_BOT_TOKEN to test (or press Enter to skip): " - read -rs TEST_TOKEN - echo "" + local TEST_TOKEN="${AGENTIC_BOT_TOKEN:-}" + if [[ -z "$TEST_TOKEN" ]]; then + echo -n "Paste AGENTIC_BOT_TOKEN to test (or press Enter to skip): " + read -rs TEST_TOKEN + echo "" + fi if [[ -n "$TEST_TOKEN" ]]; then if GH_TOKEN="$TEST_TOKEN" gh api "/repos/$1" &>/dev/null 2>&1; then diff --git a/bin/install b/bin/install index 72cc689..00a54e5 100755 --- a/bin/install +++ b/bin/install @@ -18,6 +18,8 @@ require_gh_auth SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +[[ -f "$AGENTIC_ROOT/.env" ]] && source "$AGENTIC_ROOT/.env" AGENTIC_REPO=$(detect_remote "$AGENTIC_ROOT") \ || err "Could not detect agentic repo remote" export SCRIPT_DIR AGENTIC_ROOT AGENTIC_REPO diff --git a/bin/steps/secrets b/bin/steps/secrets index f5b0344..4fe23dd 100755 --- a/bin/steps/secrets +++ b/bin/steps/secrets @@ -18,6 +18,40 @@ print_manual_commands() { EOF } +set_secret() { + echo "$2" | gh secret set "$1" --repo "$FULL_REPO" + ok "$1 set" +} + +set_variable() { + gh variable set "$1" --repo "$FULL_REPO" --body "$2" + ok "$1 = $2" +} + +all_env_set() { + [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]] \ + && [[ -n "${AGENTIC_BOT_TOKEN:-}" ]] \ + && [[ -n "${AGENTIC_BOT_NAME:-}" ]] \ + && [[ -n "${AGENTIC_BOT_EMAIL:-}" ]] +} + +apply_from_env() { + echo "" + info "Setting secrets and variables from environment..." + set_secret CLAUDE_CODE_OAUTH_TOKEN "$CLAUDE_CODE_OAUTH_TOKEN" + set_secret AGENTIC_BOT_TOKEN "$AGENTIC_BOT_TOKEN" + set_variable AGENTIC_BOT_NAME "$AGENTIC_BOT_NAME" + set_variable AGENTIC_BOT_EMAIL "$AGENTIC_BOT_EMAIL" + export BOT_TOKEN="$AGENTIC_BOT_TOKEN" BOT_NAME="$AGENTIC_BOT_NAME" +} + +# All 4 values from env — set them directly, no prompts +if all_env_set; then + apply_from_env + return 0 2>/dev/null || exit 0 +fi + +# Non-interactive with missing env vars — can't prompt if [[ -n "${NONINTERACTIVE:-}" ]]; then warn "Skipping secrets setup (non-interactive mode)" print_manual_commands @@ -36,24 +70,42 @@ It authenticates with your Claude Pro/Max subscription and outputs an OAuth toke EOF - echo -n "CLAUDE_CODE_OAUTH_TOKEN: " - read -rs CLAUDE_TOKEN - echo "" - [[ -n "$CLAUDE_TOKEN" ]] && echo "$CLAUDE_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$FULL_REPO" - - echo -n "AGENTIC_BOT_TOKEN: " - read -rs BOT_TOKEN - echo "" - [[ -n "$BOT_TOKEN" ]] && echo "$BOT_TOKEN" | gh secret set AGENTIC_BOT_TOKEN --repo "$FULL_REPO" + if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then + set_secret CLAUDE_CODE_OAUTH_TOKEN "$CLAUDE_CODE_OAUTH_TOKEN" + else + echo -n "CLAUDE_CODE_OAUTH_TOKEN: " + read -rs CLAUDE_TOKEN + echo "" + [[ -n "$CLAUDE_TOKEN" ]] && set_secret CLAUDE_CODE_OAUTH_TOKEN "$CLAUDE_TOKEN" + fi + + if [[ -n "${AGENTIC_BOT_TOKEN:-}" ]]; then + set_secret AGENTIC_BOT_TOKEN "$AGENTIC_BOT_TOKEN" + BOT_TOKEN="$AGENTIC_BOT_TOKEN" + else + echo -n "AGENTIC_BOT_TOKEN: " + read -rs BOT_TOKEN + echo "" + [[ -n "$BOT_TOKEN" ]] && set_secret AGENTIC_BOT_TOKEN "$BOT_TOKEN" + fi echo "" echo -e "${BOLD}Variables${RESET}" - read -rp "AGENTIC_BOT_NAME: " BOT_NAME - [[ -n "$BOT_NAME" ]] && gh variable set AGENTIC_BOT_NAME --repo "$FULL_REPO" --body "$BOT_NAME" - - read -rp "AGENTIC_BOT_EMAIL: " BOT_EMAIL - [[ -n "$BOT_EMAIL" ]] && gh variable set AGENTIC_BOT_EMAIL --repo "$FULL_REPO" --body "$BOT_EMAIL" + if [[ -n "${AGENTIC_BOT_NAME:-}" ]]; then + set_variable AGENTIC_BOT_NAME "$AGENTIC_BOT_NAME" + BOT_NAME="$AGENTIC_BOT_NAME" + else + read -rp "AGENTIC_BOT_NAME: " BOT_NAME + [[ -n "$BOT_NAME" ]] && set_variable AGENTIC_BOT_NAME "$BOT_NAME" + fi + + if [[ -n "${AGENTIC_BOT_EMAIL:-}" ]]; then + set_variable AGENTIC_BOT_EMAIL "$AGENTIC_BOT_EMAIL" + else + read -rp "AGENTIC_BOT_EMAIL: " BOT_EMAIL + [[ -n "$BOT_EMAIL" ]] && set_variable AGENTIC_BOT_EMAIL "$BOT_EMAIL" + fi ok "Secrets and variables configured" From a11a8b0c6d73667ca70bfc2f1112cf17c47dc828 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 21:59:57 +0000 Subject: [PATCH 20/26] feat: Skip prerequisites when .env is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prerequisites wall of text explains how to create a bot account and get an OAuth token. When .env already has all 4 values, the user has done this — no need to ask again. - Source .env and check all 4 values before showing the prompt - Skip silently in non-interactive mode when .env is missing --- bin/prerequisites | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bin/prerequisites b/bin/prerequisites index 2a527c9..f82d76e 100755 --- a/bin/prerequisites +++ b/bin/prerequisites @@ -1,7 +1,28 @@ #!/usr/bin/env bash set -euo pipefail -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/output.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +source "$SCRIPT_DIR/lib/output.sh" + +# .env has all secrets — skip the prerequisites prompt +if [[ -f "$AGENTIC_ROOT/.env" ]]; then + source "$AGENTIC_ROOT/.env" + if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]] \ + && [[ -n "${AGENTIC_BOT_TOKEN:-}" ]] \ + && [[ -n "${AGENTIC_BOT_NAME:-}" ]] \ + && [[ -n "${AGENTIC_BOT_EMAIL:-}" ]]; then + ok "Prerequisites satisfied (.env found)" + exit 0 + fi +fi + +# Non-interactive with no .env — can't prompt +if [[ -n "${NONINTERACTIVE:-}" ]]; then + warn "Skipping prerequisites check (non-interactive mode)" + exit 0 +fi cat < Date: Thu, 19 Feb 2026 22:02:06 +0000 Subject: [PATCH 21/26] feat: Load .env in diagnostics for bot check Diagnostics runs as a subprocess, so it doesn't inherit env vars from the parent install script. Source .env directly so the bot access check uses AGENTIC_BOT_TOKEN without prompting. --- bin/diagnostics | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/diagnostics b/bin/diagnostics index fc59874..70c2d9c 100755 --- a/bin/diagnostics +++ b/bin/diagnostics @@ -2,10 +2,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TEMPLATE_DIR="$SCRIPT_DIR/../templates/workflows" +AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +TEMPLATE_DIR="$AGENTIC_ROOT/templates/workflows" source "$SCRIPT_DIR/lib/output.sh" +[[ -f "$AGENTIC_ROOT/.env" ]] && source "$AGENTIC_ROOT/.env" + # Override helpers for diagnostic checklist format ok() { echo -e " ${GREEN}✓${RESET} $*"; } fail() { echo -e " ${RED}✗${RESET} $*"; } From 0ed0218faa74373c7338eb9a68edd1ccb002c1c8 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 22:13:43 +0000 Subject: [PATCH 22/26] feat: Derive bot name and email from token Users had to manually look up the bot's GitHub username and noreply email. Both can be derived from the bot token via the GitHub API, cutting the setup from 4 values to 2. - Add derive_bot_identity to secrets step (gh api /user) - Prerequisites now only checks for the 2 tokens - Update .env.example and README to reflect 2 required values - Remove 'note the noreply email' from prerequisites text --- .env.example | 7 +++++-- README.md | 4 +--- bin/prerequisites | 9 +++------ bin/steps/secrets | 21 ++++++++++++++++++++- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 0b0b669..3db319f 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ +# Required — both tokens must be created manually CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" AGENTIC_BOT_TOKEN="your-bot-pat" -AGENTIC_BOT_NAME="your-bot-username" -AGENTIC_BOT_EMAIL="123+bot@users.noreply.github.com" + +# Optional — derived automatically from the bot token if omitted +# AGENTIC_BOT_NAME="your-bot-username" +# AGENTIC_BOT_EMAIL="123+bot@users.noreply.github.com" diff --git a/README.md b/README.md index 9a7019b..a7299b3 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,9 @@ To avoid re-typing secrets when setting up multiple client repos, create a `.env ```bash CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" AGENTIC_BOT_TOKEN="your-bot-pat" -AGENTIC_BOT_NAME="your-bot-username" -AGENTIC_BOT_EMAIL="123+bot@users.noreply.github.com" ``` -The install script loads `.env` automatically if it exists. With all four values set, secrets are configured without prompts. The file is already in `.gitignore`. +The install script loads `.env` automatically if it exists. The bot's username and noreply email are derived from the token automatically — you only need the two tokens. The file is already in `.gitignore`. The workflow files are thin callers that reference this repo's composite actions via `uses:`. When the agentic repo's actions are updated, all client repos pick up the changes automatically on the next workflow run. diff --git a/bin/prerequisites b/bin/prerequisites index f82d76e..d522910 100755 --- a/bin/prerequisites +++ b/bin/prerequisites @@ -6,13 +6,12 @@ AGENTIC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" source "$SCRIPT_DIR/lib/output.sh" -# .env has all secrets — skip the prerequisites prompt +# .env has both tokens — skip the prerequisites prompt +# (bot name and email are derived from the token automatically) if [[ -f "$AGENTIC_ROOT/.env" ]]; then source "$AGENTIC_ROOT/.env" if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]] \ - && [[ -n "${AGENTIC_BOT_TOKEN:-}" ]] \ - && [[ -n "${AGENTIC_BOT_NAME:-}" ]] \ - && [[ -n "${AGENTIC_BOT_EMAIL:-}" ]]; then + && [[ -n "${AGENTIC_BOT_TOKEN:-}" ]]; then ok "Prerequisites satisfied (.env found)" exit 0 fi @@ -46,8 +45,6 @@ Before proceeding, you need two things: Org settings > Personal access tokens > Pending requests - Repository access: All repositories - Permissions: Contents (read/write), Pull requests (read/write), Issues (read/write), Metadata (read) - Also note the bot's noreply email from https://github.com/settings/emails - - It looks like: ID+username@users.noreply.github.com ${BOLD}2. Claude OAuth token${RESET} Run 'claude setup-token' in your terminal. It authenticates with your diff --git a/bin/steps/secrets b/bin/steps/secrets index 4fe23dd..daee5b4 100755 --- a/bin/steps/secrets +++ b/bin/steps/secrets @@ -28,6 +28,19 @@ set_variable() { ok "$1 = $2" } +derive_bot_identity() { + [[ -n "${AGENTIC_BOT_NAME:-}" ]] && [[ -n "${AGENTIC_BOT_EMAIL:-}" ]] && return 0 + info "Looking up bot identity from token..." + local login id + login=$(GH_TOKEN="$1" gh api /user --jq '.login' 2>/dev/null) \ + || err "Could not look up bot identity — check the token is valid" + id=$(GH_TOKEN="$1" gh api /user --jq '.id' 2>/dev/null) + AGENTIC_BOT_NAME="${AGENTIC_BOT_NAME:-$login}" + AGENTIC_BOT_EMAIL="${AGENTIC_BOT_EMAIL:-${id}+${login}@users.noreply.github.com}" + ok "AGENTIC_BOT_NAME = $AGENTIC_BOT_NAME" + ok "AGENTIC_BOT_EMAIL = $AGENTIC_BOT_EMAIL" +} + all_env_set() { [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]] \ && [[ -n "${AGENTIC_BOT_TOKEN:-}" ]] \ @@ -45,7 +58,10 @@ apply_from_env() { export BOT_TOKEN="$AGENTIC_BOT_TOKEN" BOT_NAME="$AGENTIC_BOT_NAME" } -# All 4 values from env — set them directly, no prompts +# Derive bot name/email from token if not already set +[[ -n "${AGENTIC_BOT_TOKEN:-}" ]] && derive_bot_identity "$AGENTIC_BOT_TOKEN" + +# All 4 values available — set them directly, no prompts if all_env_set; then apply_from_env return 0 2>/dev/null || exit 0 @@ -92,6 +108,9 @@ EOF echo "" echo -e "${BOLD}Variables${RESET}" + # Derive name/email from bot token if available + [[ -n "${BOT_TOKEN:-}" ]] && derive_bot_identity "$BOT_TOKEN" + if [[ -n "${AGENTIC_BOT_NAME:-}" ]]; then set_variable AGENTIC_BOT_NAME "$AGENTIC_BOT_NAME" BOT_NAME="$AGENTIC_BOT_NAME" From 808b5e6bfc92d90e6ea9da3befbfa62ada1751a8 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 22:16:43 +0000 Subject: [PATCH 23/26] fix: Separate repo create from clone gh repo create --clone tries to add an origin remote in the current directory. When running bin/install from inside the agentic repo, origin already exists and the command fails. Split into two steps: create the repo, then clone it into a subdirectory. --- bin/steps/repo | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/steps/repo b/bin/steps/repo index f7b12fd..5ec1aab 100755 --- a/bin/steps/repo +++ b/bin/steps/repo @@ -44,11 +44,12 @@ if [[ "$REPO_CHOICE" == "1" ]]; then info "Creating $FULL_REPO ($VISIBILITY)..." if [[ "$OWNER_TYPE" == "2" ]]; then - gh repo create "$FULL_REPO" --"$VISIBILITY" --clone + gh repo create "$FULL_REPO" --"$VISIBILITY" else - gh repo create "$REPO_NAME" --"$VISIBILITY" --clone + gh repo create "$REPO_NAME" --"$VISIBILITY" fi + gh repo clone "$FULL_REPO" "$REPO_NAME" CLIENT_DIR="$(pwd)/$REPO_NAME" ok "Repo created and cloned to $CLIENT_DIR" From 3a6c22b5ec40a064974eb8bc2e92002809691059 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 22:19:22 +0000 Subject: [PATCH 24/26] fix: Reuse existing directory on re-run When re-running after a failure, the target directory may already exist from the previous attempt. Instead of failing on clone, check for the directory first and reuse it. Applies to both the 'create new repo' and 'clone existing repo' paths. --- bin/steps/repo | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/steps/repo b/bin/steps/repo index 5ec1aab..a23510d 100755 --- a/bin/steps/repo +++ b/bin/steps/repo @@ -49,8 +49,12 @@ if [[ "$REPO_CHOICE" == "1" ]]; then gh repo create "$REPO_NAME" --"$VISIBILITY" fi - gh repo clone "$FULL_REPO" "$REPO_NAME" CLIENT_DIR="$(pwd)/$REPO_NAME" + if [[ -d "$CLIENT_DIR" ]]; then + warn "$REPO_NAME/ already exists locally — reusing it" + else + gh repo clone "$FULL_REPO" "$REPO_NAME" + fi ok "Repo created and cloned to $CLIENT_DIR" elif [[ "$REPO_CHOICE" == "2" ]]; then @@ -67,10 +71,14 @@ elif [[ "$REPO_CHOICE" == "2" ]]; then else read -rp "owner/repo: " FULL_REPO [[ -z "$FULL_REPO" ]] && err "owner/repo cannot be empty" - info "Cloning $FULL_REPO..." - gh repo clone "$FULL_REPO" REPO_NAME=$(echo "$FULL_REPO" | cut -d/ -f2) CLIENT_DIR="$(pwd)/$REPO_NAME" + if [[ -d "$CLIENT_DIR" ]]; then + warn "$REPO_NAME/ already exists locally — reusing it" + else + info "Cloning $FULL_REPO..." + gh repo clone "$FULL_REPO" + fi fi ok "Using repo: $FULL_REPO ($CLIENT_DIR)" From a9220767999088adca76ec6def7e23fca48e1812 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Thu, 19 Feb 2026 22:20:57 +0000 Subject: [PATCH 25/26] refactor: Extract clone_or_reuse in repo step The directory-exists check was duplicated across the 'create new' and 'clone existing' paths. One function handles both. --- bin/steps/repo | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/bin/steps/repo b/bin/steps/repo index a23510d..2f54a2b 100755 --- a/bin/steps/repo +++ b/bin/steps/repo @@ -2,6 +2,16 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/guards.sh" +clone_or_reuse() { + if [[ -d "$REPO_NAME" ]]; then + warn "$REPO_NAME/ already exists locally — reusing it" + else + info "Cloning $FULL_REPO..." + gh repo clone "$FULL_REPO" "$REPO_NAME" + fi + CLIENT_DIR="$(pwd)/$REPO_NAME" +} + if [[ -n "${FULL_REPO:-}" && -n "${CLIENT_DIR:-}" ]]; then CLIENT_DIR=$(cd "$CLIENT_DIR" && pwd) || err "Directory not found: $CLIENT_DIR" ok "Using repo: $FULL_REPO ($CLIENT_DIR)" @@ -49,12 +59,7 @@ if [[ "$REPO_CHOICE" == "1" ]]; then gh repo create "$REPO_NAME" --"$VISIBILITY" fi - CLIENT_DIR="$(pwd)/$REPO_NAME" - if [[ -d "$CLIENT_DIR" ]]; then - warn "$REPO_NAME/ already exists locally — reusing it" - else - gh repo clone "$FULL_REPO" "$REPO_NAME" - fi + clone_or_reuse ok "Repo created and cloned to $CLIENT_DIR" elif [[ "$REPO_CHOICE" == "2" ]]; then @@ -72,13 +77,7 @@ elif [[ "$REPO_CHOICE" == "2" ]]; then read -rp "owner/repo: " FULL_REPO [[ -z "$FULL_REPO" ]] && err "owner/repo cannot be empty" REPO_NAME=$(echo "$FULL_REPO" | cut -d/ -f2) - CLIENT_DIR="$(pwd)/$REPO_NAME" - if [[ -d "$CLIENT_DIR" ]]; then - warn "$REPO_NAME/ already exists locally — reusing it" - else - info "Cloning $FULL_REPO..." - gh repo clone "$FULL_REPO" - fi + clone_or_reuse fi ok "Using repo: $FULL_REPO ($CLIENT_DIR)" From a633116b22c0747dcece935f585b9553e8e2bed6 Mon Sep 17 00:00:00 2001 From: Mike Mindel Date: Fri, 20 Feb 2026 09:58:28 +0000 Subject: [PATCH 26/26] docs: Explain two-layer workflow architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key insight — workflows are installed once, actions update automatically via @main — was buried in the .env section. Moved it to the workflow templates section and expanded it so readers understand why the system is structured this way. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7299b3..938741b 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ AGENTIC_BOT_TOKEN="your-bot-pat" The install script loads `.env` automatically if it exists. The bot's username and noreply email are derived from the token automatically — you only need the two tokens. The file is already in `.gitignore`. -The workflow files are thin callers that reference this repo's composite actions via `uses:`. When the agentic repo's actions are updated, all client repos pick up the changes automatically on the next workflow run. - ### Workflow templates -The templates live in `templates/workflows/` and use a `__AGENTIC_REPO__` placeholder that the install script replaces with this repo's `owner/repo` path: +The system has two layers: **workflow files** installed into each client repo, and **composite actions** that live in the agentic repo. + +The workflow files are thin wiring — they define which GitHub events trigger which actions in which order. The templates live in `templates/workflows/` and use a `__AGENTIC_REPO__` placeholder that the install script replaces with this repo's `owner/repo` path: | Template | Trigger | What it does | |----------|---------|-------------| @@ -54,6 +54,8 @@ The templates live in `templates/workflows/` and use a `__AGENTIC_REPO__` placeh | `pr-changes-requested.yml` | PR review with changes requested starting with `@claude` | Fixes code, pushes | | `pr-approved.yml` | PR review approved starting with `@claude` | Squash merges | +The composite actions are where the real logic lives. Because the workflows reference them at `@main`, every client repo pulls the latest version of each action on every run. You update an action once in the agentic repo, and every client gets the change automatically — no need to re-run the install script or touch the workflow files. + ### Required secrets and variables **Secrets** (Settings → Secrets and variables → Actions):