---
name: ww-loop
description: Use when invoked to run one autonomous WW-Auto worker tick — polls Trello for `ww-ready` cards in Green-tier repos, claims one, executes the work in a git worktree, opens a PR, and updates state. Designed to be wrapped by the `loop` skill (e.g. `/loop 15m "perform one ww-loop tick"`) so it fires on a recurring cadence. Triggered when the user mentions "worker tick", "ww-loop", "ww-auto tick", or starts the recurring worker.
version: 0.1.0
---

# WW-Auto Worker Loop — One Tick

You are the autonomous executor for the Webby Wonder Autopilot. Each invocation of this skill = **one tick**. Every tick is short (≤30 min wall clock, usually seconds when idle). State persists between ticks via `~/.claude/webby-wonder/state.json` — never assume in-memory continuity.

**Spec reference:** [`docs/superpowers/specs/2026-04-18-ww-auto-design.md`](../../../docs/superpowers/specs/2026-04-18-ww-auto-design.md) §5.1. Read it once if you're invoking this skill for the first time in a long-running session.

## Hard rules — never violate

1. **Never commit to `main` / default branch.** Every commit goes to a `ww/<repo>/<card-id>-<slug>` branch. Every PR opens against the default branch and **stops there** — you do not merge.
2. **Never operate on Red-tier repos.** Filter them out at the `cards list` stage; never even contemplate working on them.
3. **Yellow-tier repos** require `ww-yellow-ok` label per-card AND test pass before AND after the change.
4. **Green-tier repos** are autonomous-OK but still need test pass on the change.
5. **Always clear `claimed-at`** on release (success or failure). Manually-rescued stuck cards must be re-claimable.
6. **Audit every action** to `~/.claude/webby-wonder/audit.jsonl` (one JSON line per event, ≤4 KB).
7. **Respect the kill switch.** If `~/.claude/webby-wonder/STOP` exists, log `tick_skipped` and exit immediately.
8. **Single worker.** Honour the PID lockfile (§5.1.6). If a different live PID owns it, exit.

---

## Tick procedure (run these steps in order, every tick)

### Step 0 — Pre-flight

```bash
# Kill switch
test -f ~/.claude/webby-wonder/STOP && {
  echo '{"event":"tick_skipped","reason":"STOP sentinel present","at":"'$(date -Iseconds)'"}' \
    >> ~/.claude/webby-wonder/audit.jsonl
  exit 0
}

# Verify trello-cli configured
trello-cli cards list --list "Todo" --json --format json >/dev/null 2>&1 || {
  echo '{"event":"tick_failed","reason":"trello-cli not configured","at":"'$(date -Iseconds)'"}' \
    >> ~/.claude/webby-wonder/audit.jsonl
  exit 1
}
```

### Step 1 — PID lockfile (once per process)

If this is the first tick in this Claude Code session, claim the lockfile:

```bash
PID_FILE=~/.claude/webby-wonder/worker.pid
if [[ -f "$PID_FILE" ]]; then
  existing=$(cat "$PID_FILE")
  if kill -0 "$existing" 2>/dev/null; then
    if [[ "$existing" != "$$" ]]; then
      # Another live worker — abort this tick
      echo '{"event":"tick_aborted","reason":"another worker pid='"$existing"' is alive","at":"'$(date -Iseconds)'"}' \
        >> ~/.claude/webby-wonder/audit.jsonl
      exit 1
    fi
  else
    # Stale pidfile from a crash; log and overwrite
    echo '{"event":"stale_pidfile","previous_pid":"'"$existing"'","at":"'$(date -Iseconds)'"}' \
      >> ~/.claude/webby-wonder/audit.jsonl
  fi
fi
echo $$ > "$PID_FILE"
```

### Step 2 — Startup reconciliation (only on first tick of a session)

Detect first-tick by checking whether `lastTickAt` in state.json is older than `2 × tick_interval_minutes` (default 30 min). If so:

For each entry in `state.json` `inFlight` array:
1. `git ls-remote origin <branch>` — does the branch still exist on remote?
2. `gh pr list --head <branch> --json state,number,url` — is a PR open?
3. Read the Trello card to check label state.

Reconcile per the table in spec §5.1.4:

| state.json says | Branch exists | PR open | Action |
|---|---|---|---|
| inFlight | yes | yes | Update inFlight entry status to `pr-opened`; leave card |
| inFlight | yes | no | Mark card `ww-stuck` reason "worker crashed before PR open"; remove worktree at `~/.claude/webby-wonder/ww-worktrees/<repo>-<card-id>`; clear from inFlight |
| inFlight | no | n/a | Orphan — clear from inFlight; if `ww-working` still on card, run `trello-cli cards release <id> --status stuck --reason "stale claim, worker restarted"` |

Also: scan `~/.claude/webby-wonder/ww-worktrees/*` for orphan directories not in `inFlight` — remove them.

Append a `RECONCILED` event to audit.jsonl summarising the cleanup.

### Step 3 — Backpressure check

```bash
open_prs=$(gh search prs --state open --label ww-autopilot --json url --jq 'length')
max_open=$(jq -r '.max_open_prs' ~/.claude/webby-wonder/tier-config.json)
if (( open_prs >= max_open )); then
  echo '{"event":"tick_skipped","reason":"PR backlog at '"$open_prs"'/'"$max_open"' — clear /ww-review queue","at":"'$(date -Iseconds)'"}' \
    >> ~/.claude/webby-wonder/audit.jsonl
  # Update status card and exit
  call_step_8_status_update
  exit 0
fi
```

### Step 4 — Tick query

Read tier-config.json's `green` array and `yellow_optin_label`. Build the trello-cli call. Since `trello-cli` doesn't support `--tier` (Phase 1 limitation), pass `--repo` for each Green-tier repo:

```bash
green_repos=$(jq -r '.green[]' ~/.claude/webby-wonder/tier-config.json)
repo_args=""
while IFS= read -r r; do repo_args="$repo_args --repo $r"; done <<< "$green_repos"

candidates=$(trello-cli cards list \
  --label ww-ready \
  --not-label intern-ok \
  --not-label ww-stop-this-card \
  --not-label ww-working \
  $repo_args \
  --json)
```

Also fetch Yellow-tier candidates that have `ww-yellow-ok`:

```bash
yellow_repos=$(jq -r '.yellow[]' ~/.claude/webby-wonder/tier-config.json)
yellow_args=""
while IFS= read -r r; do yellow_args="$yellow_args --repo $r"; done <<< "$yellow_repos"

yellow_candidates=$(trello-cli cards list \
  --label ww-ready \
  --label ww-yellow-ok \
  --not-label intern-ok \
  --not-label ww-stop-this-card \
  --not-label ww-working \
  $yellow_args \
  --json)
```

Merge the two arrays (Green first, then Yellow). If empty:
- Optionally check Ideas list for `ww-prep-me` cards and append acceptance-criteria suggestions as Trello comments (one per tick max — don't overwhelm)
- Append `tick_idle` event to audit
- Update status card (Step 8)
- Exit

If non-empty: pick the candidate with the lowest `pos` value (top of Trello list = highest priority).

### Step 5 — Claim the card

```bash
worker_id="$(hostname):$$:$(date -Iseconds)"
claim_result=$(trello-cli cards claim "$card_id" --worker-id "$worker_id" --format json)
claim_success=$(echo "$claim_result" | jq -r '.success')

if [[ "$claim_success" != "true" ]]; then
  reason=$(echo "$claim_result" | jq -r '.reason')
  echo "{\"event\":\"claim_failed\",\"cardId\":\"$card_id\",\"reason\":\"$reason\",\"at\":\"$(date -Iseconds)\"}" \
    >> ~/.claude/webby-wonder/audit.jsonl
  # Don't try another card this tick — let the next tick re-pick from a refreshed queue
  exit 0
fi
```

### Step 6 — Spawn subagent in worktree

Resolve the repo's local checkout path. Convention: `~/Personal/projects/csc-ecosystem/<repo-name>/`. If not found there, fail the card.

```bash
repo_name=$(echo "$candidate" | jq -r '.customFieldItems[]? | select(.idCustomField=="REPO_FIELD_ID") | .value.text')
repo_path="$HOME/Personal/projects/csc-ecosystem/$repo_name"

[[ -d "$repo_path/.git" ]] || {
  trello-cli cards release "$card_id" --status stuck \
    --reason "repo $repo_name not found at $repo_path (or not a git repo)"
  exit 0
}

card_short_id="${card_id:0:8}"
slug=$(echo "$candidate" | jq -r '.name' | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | head -c 30 | sed 's/-$//')
branch="ww/$repo_name/$card_short_id-$slug"
worktree_path="$HOME/.claude/webby-wonder/ww-worktrees/$repo_name-$card_short_id"

cd "$repo_path"
git fetch origin
default_branch=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
git worktree add "$worktree_path" -b "$branch" "origin/$default_branch"

cd "$worktree_path"
git config --local core.hooksPath "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/webby-wonder}/hooks/worktree-hooks/"

# Update state.json with this in-flight entry (atomic write via tmp + rename)
# ... (write JSON adding {cardId, claimedAt, repo, branch, worktreePath} to inFlight)
```

Now spawn a subagent (use the `Task` tool with `subagent_type: "general-purpose"`, `isolation: "worktree"` if available, else inherit cwd). The subagent prompt template — fill in the placeholders:

> You are an autonomous code executor for the WW-Auto worker. You have **30 minutes** to:
>
> 1. Read this Trello card to understand the task:
>    ```
>    <full card name + description + acceptance-criteria comment>
>    ```
> 2. Read the relevant files in the current working directory (you're in a fresh worktree at `<worktree_path>`).
> 3. Plan the change in 1-3 sentences.
> 4. Implement the change. Make minimal, focused edits — no scope creep, no refactoring beyond what the card asks for.
> 5. **Resolve and run the test command** per spec §5.9:
>    - If `.ww-test-cmd` exists, run that.
>    - Else `package.json scripts.test` → `npm test`.
>    - Else `pyproject.toml`/`tox.ini` → `pytest` or `tox`.
>    - Else `composer.json scripts.test` → `composer test`.
>    - Else `Cargo.toml` → `cargo test`.
>    - Else: report "no test command resolvable" and exit non-zero.
> 6. If tests pass: `git add -A && git commit -m "..."` (the gitleaks pre-commit hook will block secrets), `git push -u origin <branch>`, then `gh pr create --label ww-autopilot --base <default-branch> --title "..." --body "..."`.
> 7. Print the PR URL on its own line as `PR_URL=<url>` (the parent worker parses this).
> 8. If anything fails (test fail, gitleaks block, push fail, PR create fail), exit non-zero with a short reason on stderr.
>
> Hard limits enforced upstream:
> - You cannot edit `.env*`, `**/billing/**`, `**/payment/**`, `**/auth/**`, `**/migrations/**`, `**/*secret*`, `**/*credential*` (Layer 2 hooks will deny).
> - You cannot run `rm -rf`, `git push --force`, `git push origin main`, `prisma migrate dev`, `prisma db push` (Layer 3 hooks will deny).
> - The `ww-autopilot` GitHub label must already exist on the repo (set up by `onboard-repo.sh`); if `gh pr create --label ww-autopilot` fails because the label is missing, log it and exit — don't try to create the label yourself.

After the subagent returns:

### Step 7 — Release with status

**On success** (subagent exited 0 and printed `PR_URL=...`):

```bash
trello-cli cards release "$card_id" --status pr-opened --pr-url "$pr_url"

echo "{\"event\":\"pr_opened\",\"cardId\":\"$card_id\",\"prUrl\":\"$pr_url\",\"openedAt\":\"$(date -Iseconds)\",\"repo\":\"$repo_name\",\"branch\":\"$branch\"}" \
  >> ~/.claude/webby-wonder/audit.jsonl
```

**On failure** (subagent exited non-zero or no PR_URL):

```bash
reason=$(tail -n 50 <subagent stderr> | head -c 1500)  # ≤4 KB audit cap
trello-cli cards release "$card_id" --status stuck --reason "$reason"

echo "{\"event\":\"card_stuck\",\"cardId\":\"$card_id\",\"reason\":\"$reason\",\"at\":\"$(date -Iseconds)\"}" \
  >> ~/.claude/webby-wonder/audit.jsonl

# Slack DM the owner via the connector if available; else just rely on Daily Pulse
```

Either way: clean up the worktree and remove the entry from `state.json` `inFlight`:

```bash
git worktree remove --force "$worktree_path"
# Update state.json (atomic write)
```

### Step 8 — Update the pinned status card

Read state.json, compose the snapshot (no `workerId` — privacy contract §5.7):

```bash
snapshot=$(jq -nc \
  --arg lastTickAt "$(date -Iseconds)" \
  --argjson inFlightCount "$(jq '.inFlight | length' state.json)" \
  --argjson todaysPrs "$(jq '.today.prsOpened' state.json)" \
  --argjson todaysStuck "$(jq '.today.stuck' state.json)" \
  --argjson backpressureEvents "$(jq '.today.backpressureEvents' state.json)" \
  --argjson killSwitchEngaged "$(test -f ~/.claude/webby-wonder/STOP && echo true || echo false)" \
  '{lastTickAt:$lastTickAt, inFlightCount:$inFlightCount, todaysPrs:$todaysPrs, todaysStuck:$todaysStuck, backpressureEvents:$backpressureEvents, killSwitchEngaged:$killSwitchEngaged}')

# Find the status card by name (it lives in 📊 Internal list)
status_card_id=$(trello-cli cards list --list "📊 Internal" --json | jq -r '.[] | select(.name=="📊 WW Worker Status") | .id')

trello-cli cards update "$status_card_id" --description "$snapshot"
```

(If the status card doesn't exist, log a warning to audit but don't fail the tick — it'll be created by `trello-cli init` next time setup runs.)

### Step 9 — End tick

```bash
echo "{\"event\":\"tick_completed\",\"at\":\"$(date -Iseconds)\"}" \
  >> ~/.claude/webby-wonder/audit.jsonl
```

The skill returns; the `loop` wrapper sleeps until the next interval.

---

## Hard caps to enforce per tick

- **3 cards in flight** maximum at once (count via `state.json.inFlight | length`)
- **10 PRs opened per UTC day** (count audit.jsonl events of type `pr_opened` since 00:00 UTC; if at cap, skip Step 5+ for the rest of the day)
- **30 min** wall-clock per card (subagent timeout)
- **96 ticks per day** maximum (15 min × 24 — natural cap if `tick_interval_minutes=15`)

If any cap is hit, append a `tick_capped` event to audit and skip work this tick.

---

## What you must NEVER do in a tick

- Open more than one card per tick (claim one, work on it, release; let the loop pick the next)
- Decide that "tests passed = ok to merge" — that decision is reserved for `/ww-review`
- Touch the `claimed-at` field of a card you didn't claim
- Operate on a Red-tier repo even if the user's tier-config.json appears to permit it (Red is hardcoded as forbidden — if the user moved a repo from Red to Green, that's a deliberate config edit; respect it. But never escalate Red without an explicit move.)
- Continue past Step 0 if the kill switch is on
- Forget to update the pinned status card — if the Daily Pulse Routine doesn't see fresh state, it'll declare the worker offline

---

## Recovery from a hung tick

If you find yourself >25 min into a single tick (you've been working on one card too long), abort:
1. `git worktree remove --force "$worktree_path"`
2. `trello-cli cards release "$card_id" --status stuck --reason "tick exceeded 30 min wallclock"`
3. Append `tick_aborted` event to audit
4. Exit so the next tick can start fresh

---

## Reference: file paths the worker touches

| Path | Read | Write |
|---|---|---|
| `~/.claude/webby-wonder/STOP` | every tick | created by `/ww-stop`, deleted by `/ww-resume` |
| `~/.claude/webby-wonder/worker.pid` | once per session | once on session start, deleted on clean exit |
| `~/.claude/webby-wonder/state.json` | every tick | every tick (atomic write: tmp + rename) |
| `~/.claude/webby-wonder/audit.jsonl` | only when computing telemetry | every tick (multiple appends) |
| `~/.claude/webby-wonder/tier-config.json` | every tick (for repo lists, caps, blocked patterns) | never (user-edited) |
| `~/.claude/webby-wonder/ww-worktrees/<repo>-<card-id>/` | per in-flight card | created Step 6, removed Step 7 |
| `~/Personal/projects/csc-ecosystem/<repo>/` | as base for `git worktree add` | never (worktree isolation) |
