ccc-ci · diff
git:20260722.9d3ab82 to git:20261013.fbe187d
111 added, 109 removed. Audit A to A.
---
name: ccc-ci
- description: "CI/CD webhook channel. Receive GitHub Actions, Vercel, Railway deploy events in your session. Auto-triggers $ccc-doctor on failures."
- model: sonnet
- effort: medium
+ description: "Diagnose CI and deploy failures: pull the failing run's log, classify the cause, propose a fix, and follow runs with /loop or a Routine; optional signed-webhook push mode. Use when CI is red, a deploy failed, a prod approval waits, or the user wants pipeline events in the session."
allowed-tools:
- Bash
- Read
+ - Grep
+ - Glob
+ - Edit
+ - Write
- Agent
- argument-hint: "[init | status | stop | test <event>]"
+ - AskUserQuestion
+ argument-hint: "[status | diagnose [run-id] | watch [run-id] | init [--port N] [--tunnel] [--rotate] | test <event>]"
+ model: sonnet
+ effort: medium
---
- # $ccc-ci — CI/CD Webhook Channel
-
- Bridges your CI/CD pipeline to your active Claude Code session. Deploy events arrive in real time; failures trigger automatic diagnosis; successes get celebrated.
-
- ## What it does
-
- `$ccc-ci` runs a lightweight HTTP receiver on `localhost:7891` that accepts signed webhook payloads from GitHub Actions, Vercel, Railway, and Fly.io. Each inbound event is classified, routed to a handler, and surfaced in chat — so you see deploy failures, test results, and approval gates without leaving your session. On failure, CCC automatically loads `$ccc-doctor`, reads the CI log, classifies the error, and proposes a concrete fix. On success, it drafts a celebration tweet you can post with one click.
-
- ## Setup
+ # $ccc-ci — CI failures, diagnosed in-session
- ```bash
- # 1. Start the local receiver (default port 7891)
- $ccc-ci init
+ **CC Commander** · $ccc-ci · the failing run, its classified cause and a copy-paste fix, without leaving the session.
- # 2. (Optional) pick a different port
- $ccc-ci init --port 8321
+ Pull mode (default) reads runs through the provider CLI. Push mode (`init`) receives HMAC-signed webhooks on a loopback receiver the user approves; its setup lives in [references/webhook-receiver.md](references/webhook-receiver.md). The user's selected model is preserved; the only delegation is the optional `debugger` agent in step 4.
- # 3. Expose publicly if your CI provider can't reach localhost
- $ccc-ci init --tunnel # wraps with ngrok or cloudflared
- ```
+ Codex: ask the pickers below as plain-text questions, and run the step 4 delegation with `codex exec --json` in a fresh context.
- `init` prints a webhook URL, copies it to clipboard, and emits ready-to-paste configs for each supported provider.
+ ## When to use / when not to
- ### GitHub Actions — paste into your workflow YAML
+ - Use when a GitHub Actions run, Vercel/Fly.io/Railway deploy or test job failed, when a production approval gate is waiting, or to follow a run until it finishes.
+ - Not for: writing a new pipeline (`$ccc-devops`), pre-deploy readiness (`$ccc-deploy-check`), Commander's own install health (`$ccc-doctor`), or a bug with no CI context (`$ccc-systematic-debugging`).
+ - Skip it for projects with no CI and fully manual deploys.
- ```yaml
- - name: Notify CCC
- if: always()
- run: |
- curl -s -X POST ${{ secrets.CCC_WEBHOOK_URL }} \
- -H "X-CCC-Signature: $(echo -n '${{ toJSON(github.event) }}' | openssl dgst -sha256 -hmac '${{ secrets.CCC_SECRET }}' | awk '{print $2}')" \
- -H "Content-Type: application/json" \
- -d '{"event":"${{ job.status == 'success' && 'deploy_succeeded' || 'deploy_failed' }}","ref":"${{ github.ref }}","run_url":"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
- ```
+ ## Events
- ### Vercel — Dashboard → Settings → Webhooks → Add
+ | Event | Meaning | Handled in |
+ |---|---|---|
+ | `push` | New commit on a branch | step 2 (listed only) |
+ | `deploy_started` | Deploy job started | step 2 |
+ | `deploy_failed` | Deploy exited non-zero | step 3 |
+ | `deploy_succeeded` | Deploy completed and healthy | step 2 (duration, environment, URL) |
+ | `test_failed` | Test job reported failures | steps 3–4 |
+ | `test_succeeded` | All tests green | step 2 |
+ | `approval_required` | Manual production gate waiting | step 6 |
- Point to the URL printed by `$ccc-ci init`. Select: `deployment.created`, `deployment.succeeded`, `deployment.error`.
+ ## What happens
- ### Fly.io — `fly webhooks create`
+ 1. **Detect the provider and access.**
+ ```bash
+ ls -d .github/workflows vercel.json .vercel fly.toml railway.json railway.toml .gitlab-ci.yml 2>/dev/null
+ command -v gh vercel fly railway 2>/dev/null
+ gh auth status 2>&1 | head -n 3
+ ```
+ No CI files and no provider CLI: say so and stop. `gh` present but not authenticated: tell the user to run `gh auth login` themselves; never ask for a token. If a Vercel connector is connected, its deployment and runtime-log tools replace the `vercel` CLI calls below.
- ```bash
- fly webhooks create --app myapp \
- --url "$(pbpaste)" \
- --events instance_state_change
- ```
+ 2. **Status** (`status`, or no argument).
+ ```bash
+ gh run list --limit 10 --json databaseId,name,status,conclusion,headBranch,event,createdAt,url
+ tail -n 20 ~/.claude/commander/ccc-ci-events.jsonl 2>/dev/null
+ ```
+ The second line returns push-mode events only when `init` was set up. Show one line per run (name, branch, conclusion, age, URL). If any run failed, go to step 3 for the newest failure; otherwise ask the next-step picker.
- ## Supported Events
+ 3. **Diagnose** (`diagnose [run-id]`, or a `deploy_failed`/`test_failed` event). Fetch the failing log, newest failure when no id is given:
+ ```bash
+ RUN_ID=${RUN_ID:-$(gh run list --status failure --limit 1 --json databaseId --jq '.[0].databaseId')}
+ gh run view "$RUN_ID" --log-failed | tail -n 200
+ ```
+ Other providers: `vercel inspect <deployment-url> --logs`, `fly logs --app <app> --no-tail`, `railway logs`. Read the workflow file, `fly.toml` or `vercel.json` the log points at. Before quoting any log line, mask anything that looks like a token or key. Classify the failure:
- | Event | Trigger |
- |---|---|
- | `push` | New commit pushed to any branch |
- | `deploy_started` | Deploy job kicked off |
- | `deploy_failed` | Deploy exited non-zero |
- | `deploy_succeeded` | Deploy completed and healthy |
- | `test_failed` | Test suite reported failures |
- | `test_succeeded` | All tests green |
- | `approval_required` | Manual prod gate waiting |
+ | Class | Signal in the log | Typical fix |
+ |---|---|---|
+ | Dependency | resolve/lockfile/peer errors, `ERESOLVE`, 404 on a package | pin or update the package, regenerate the lockfile |
+ | Type/build | `TS\d+`, compiler errors with `path:line` | fix the cited line |
+ | Test | assertion output, failing test names | step 4 |
+ | OOM | `OOMKilled`, exit 137, heap out of memory | raise the memory limit or cut peak usage |
+ | Config drift | missing env var, wrong secret name, changed runner image | restore the setting in the provider or workflow |
+ | Timeout | job or step exceeded its limit | cache, split, or raise the limit |
+ | Flaky/infra | passes on rerun, network or runner errors | rerun once (step 5), then quarantine the test |
+ | Unknown | none of the above | step 4 |
- ## Auto-actions per event
+ Report: run id and URL, the class, the 3–10 log lines that prove it (masked), and one copy-paste fix command or edit with `path:line`.
- ### `deploy_failed`
- 1. Load `$ccc-doctor` in the current session
- 2. Read the error log URL from the payload
- 3. Classify: dependency error / OOM / config drift / timeout / unknown
- 4. Propose targeted fix command in chat (copy-paste ready)
+ 4. **Root cause for test and unknown failures.** Reproduce locally first: derive the failing command from the workflow step (for example `npm test -- <file>`) and run it. Then delegate to the plugin's `debugger` agent with the Agent tool, `effort: "medium"`, passing the masked failing output, the workflow step and the local result, and the instruction: "Diagnose only; do not edit files. Return ranked hypotheses, most likely first, each with the evidence for it and the minimal reproduction command." Run the top hypothesis's reproduction command yourself before calling it the root cause.
- ### `test_failed`
- 1. Spawn a debugger agent with the error output
- 2. Generate a ranked hypothesis list (most likely root cause first)
- 3. Suggest the minimal reproduction command
+ 5. **Next step.** Ask the next-step picker (Decision points). On "Apply the fix", edit the files, rerun the local failing command and show its output. On "Rerun failed jobs", run:
+ ```bash
+ gh run rerun "$RUN_ID" --failed
+ ```
- ### `deploy_succeeded`
- 1. Print deploy stats: duration, provider, environment, URL
- 2. Draft a celebration tweet: "Just shipped X to production in Ys. 🚀 #buildinpublic"
+ 6. **Approval gates** (`approval_required`). Show the run URL and what is waiting:
+ ```bash
+ gh api "repos/{owner}/{repo}/actions/runs/$RUN_ID/pending_deployments" --jq '.[] | {environment: .environment.name, can_approve: .current_user_can_approve}'
+ ```
+ Approving deploys to a protected environment; do it only after the approval picker and an explicit yes, with the environment id from the call above:
+ ```bash
+ gh api -X POST "repos/{owner}/{repo}/actions/runs/$RUN_ID/pending_deployments" -F "environment_ids[]=$ENV_ID" -f state=approved -f comment="approved via $ccc-ci"
+ ```
+ Add `id: .environment.id` to the `--jq` filter to read `ENV_ID`. For phone approval while away, point to `$ccc-nightwatch`.
- ### `approval_required`
- 1. Forward notification to `$ccc-nightwatch` so you're alerted even if your session is idle
- 2. Print the approval URL and the command to approve via CLI
+ 7. **Watch** (`watch [run-id]`). Ask the watch picker. Follow once: `gh run watch "$RUN_ID" --exit-status` as a background Bash command (stops at the background time limit). Recurring: tell the user the command to run, `/loop 5m $ccc-ci status` for this session, or `/schedule` for a Routine that checks on a cron. Creating a Routine is the user's action or needs their yes.
- ## Architecture
+ 8. **Push mode** (`init`, `test <event>`). Follow [references/webhook-receiver.md](references/webhook-receiver.md) step by step: port check, secret creation, receiver file, provider configuration, optional tunnel, signed test event. Every step that binds a port, exposes it publicly, or writes a provider secret asks first.
- ```
- GitHub Actions / Vercel / Fly.io / Railway
- │ (HTTPS POST, HMAC-signed)
- ▼
- localhost:7891 ◄── $ccc-ci init (Node http.Server)
- │
- signature check (HMAC-SHA256)
- │ pass │ fail
- ▼ ▼
- event classifier drop + warn
- push | deploy_* | test_*
- │
- session bus (stdout pipe to Claude Code session)
- │
- skill router
- ├── deploy_failed → $ccc-doctor + fix proposal
- ├── test_failed → debugger agent
- ├── deploy_succeeded → celebration + tweet draft
- └── approval_required → $ccc-nightwatch relay
- ```
+ ## Decision points
- ## Examples
+ Next step after a diagnosis (AskUserQuestion, 2–4 options):
+ - 🟢 Show the fix only: nothing changes; you apply it
+ - Apply the fix locally: edits files, reruns the failing command here
+ - Rerun failed jobs: `gh run rerun --failed` starts new CI minutes
+ - Done
- **1 — PR fails CI on TypeScript error**
- GitHub Actions calls the webhook with `test_failed` and the log URL. CCC fetches the log, extracts the TS error, and posts: "TS2339 on `src/auth.ts:42` — property `user` does not exist on type `Session`. Fix: add `user?: User` to the `Session` interface."
+ Approval gate:
+ - 🟢 Open the run URL: you approve in the provider UI
+ - Approve from here: deploys to the protected environment now
+ - Leave it waiting
- **2 — Vercel deploy preview ready**
- `deploy_succeeded` arrives with preview URL. CCC posts: "Preview live → https://my-app-abc123.vercel.app — running Lighthouse... Score: Performance 94, A11y 100, SEO 91."
+ Watch mode:
+ - 🟢 Follow this run once: background `gh run watch`, reports on exit
+ - Poll this session: `/loop 5m $ccc-ci status`
+ - Scheduled Routine: `/schedule`, runs without this session
- **3 — Fly.io deploy crashes**
- `deploy_failed` arrives with instance logs. CCC reads the log, finds `OOMKilled`, and posts: "Memory limit hit (256MB). Fix: increase `[[vm]] memory_mb = 512` in fly.toml, then `fly deploy`."
+ Push-mode setup (`init`):
+ - 🟢 Loopback only: `127.0.0.1`, reachable from local tools and self-hosted runners
+ - Loopback plus tunnel: public URL for hosted CI; signature check is the only gate
+ - Cancel: stay in pull mode
- ## Security
+ ## Done when
- - **Signature validation:** every request is verified with HMAC-SHA256 against a shared secret generated at `init` time and stored in `~/.claude/commander/ccc-ci.secret`.
- - **Loopback only by default:** the receiver binds `127.0.0.1:7891` — not reachable from the internet unless `--tunnel` is passed.
- - **Tunnel exposure:** `--tunnel` launches `ngrok http 7891` or `cloudflared tunnel --url http://localhost:7891`. The tunnel URL is rotated each `init` run; update your webhook config accordingly.
- - **Secret rotation:** run `$ccc-ci init --rotate` to generate a new secret and print updated webhook configs.
+ - Diagnosis: the class and the quoted log lines come from a log fetched in this turn, with the run id and URL.
+ - Root cause: the reproduction command ran in this turn and its output matches the hypothesis; otherwise report "not confirmed" and what was ruled out.
+ - Applied fix: the local failing command reran after the edit and its result is reported.
+ - Rerun: `gh run view "$RUN_ID" --json status,conclusion` was read after the rerun and reported (`in_progress` is a valid report).
+ - Push mode: the signed test event returned status 204 and appears in `~/.claude/commander/ccc-ci-events.jsonl`.
- ## When NOT to use
+ ## Guardrails
- - Solo dev with fully manual deploys and no CI configured
- - Monorepo where CI runs are owned by another team — webhook noise without context
- - Air-gapped environments where `localhost:7891` can never receive external webhooks
+ - Rerunning jobs, approving a gate, binding a port, opening a tunnel, editing workflow files and setting provider secrets each need the matching picker and an explicit yes in this conversation.
+ - Never print, echo or commit the webhook secret or any provider token; mask secrets in quoted log lines.
+ - Never invent a root cause: unknown stays "unknown" until a reproduction confirms it.
+ - Bind the receiver to `127.0.0.1` only; a tunnel is the only public path, and it needs the signature check.
+ - Never post, tweet or message anyone about a run.
- ## Tools used
+ ## See also
- | Tool | Why |
- |---|---|
- | `Bash` | Start the HTTP receiver, validate HMAC, curl CI log URLs |
- | `Read` | Parse `.github/workflows/`, `fly.toml`, `vercel.json` for CI context |
- | `Agent` | Spawn debugger sub-agent on `test_failed` with error payload |
+ - `$ccc-devops` — build or harden the pipeline itself
+ - `$ccc-deploy-check` — readiness gate before a deploy
+ - `$ccc-systematic-debugging` — root-cause method used in step 4
+ - `$ccc-loop` — choosing between `/loop`, `/goal` and `/schedule`
---
> ⚙️ **Fable contract:** plan before build · verifier ≠ worker · prove before alarm · loops need gates · leave durable state — `rules/fable-method.md`
+
+ > (On Codex, present these options as a numbered list and ask the user to reply with a number — AskUserQuestion is Claude-only.)