diff --git a/plugins/README.md b/plugins/README.md
index cf4a21ecc5..8795748bd0 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -13,6 +13,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
| Name | Description | Contents |
|------|-------------|----------|
| [agent-sdk-dev](./agent-sdk-dev/) | Development kit for working with the Claude Agent SDK | **Command:** `/new-sdk-app` - Interactive setup for new Agent SDK projects
**Agents:** `agent-sdk-verifier-py`, `agent-sdk-verifier-ts` - Validate SDK applications against best practices |
+| [cc-taskrunner](./cc-taskrunner/) | Autonomous task queue with safety hooks, branch isolation, and automatic PR creation | **Commands:** `/taskrunner`, `/taskrunner-add`, `/taskrunner-list` - Queue and execute tasks in headless Claude Code sessions
**Agent:** `task-executor` - Debug and monitor autonomous task execution |
| [claude-opus-4-5-migration](./claude-opus-4-5-migration/) | Migrate code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5 | **Skill:** `claude-opus-4-5-migration` - Automated migration of model strings, beta headers, and prompt adjustments |
| [code-review](./code-review/) | Automated PR code review using multiple specialized agents with confidence-based scoring to filter false positives | **Command:** `/code-review` - Automated PR review workflow
**Agents:** 5 parallel Sonnet agents for CLAUDE.md compliance, bug detection, historical context, PR history, and code comments |
| [commit-commands](./commit-commands/) | Git workflow automation for committing, pushing, and creating pull requests | **Commands:** `/commit`, `/commit-push-pr`, `/clean_gone` - Streamlined git operations |
diff --git a/plugins/cc-taskrunner/.claude-plugin/plugin.json b/plugins/cc-taskrunner/.claude-plugin/plugin.json
new file mode 100755
index 0000000000..3ea9f7fd55
--- /dev/null
+++ b/plugins/cc-taskrunner/.claude-plugin/plugin.json
@@ -0,0 +1,9 @@
+{
+ "name": "cc-taskrunner",
+ "version": "1.0.0",
+ "description": "Autonomous task queue for Claude Code with safety hooks, branch isolation, and automatic PR creation",
+ "author": {
+ "name": "Kurt Overmier",
+ "email": "admin@stackbilt.dev"
+ }
+}
diff --git a/plugins/cc-taskrunner/README.md b/plugins/cc-taskrunner/README.md
new file mode 100755
index 0000000000..694d415445
--- /dev/null
+++ b/plugins/cc-taskrunner/README.md
@@ -0,0 +1,78 @@
+# cc-taskrunner
+
+Autonomous task queue for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with safety hooks, branch isolation, and automatic PR creation.
+
+Queue tasks. Go to sleep. Wake up to PRs.
+
+## Installation
+
+```bash
+claude plugin:add /path/to/cc-taskrunner
+```
+
+Or clone from the [cc-taskrunner repository](https://github.com/Stackbilt-dev/cc-taskrunner) and add the `plugin/` directory.
+
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| `/taskrunner` | Run pending tasks from the queue |
+| `/taskrunner-add` | Add a task to the queue |
+| `/taskrunner-list` | Show all tasks and their status |
+
+### Quick Start
+
+```
+> /taskrunner-add Write unit tests for the auth middleware in src/middleware.ts
+
+Added task a1b2c3d4: Write unit tests for the auth middleware
+
+> /taskrunner --max 1
+```
+
+## Safety Architecture
+
+Three layers of protection prevent autonomous sessions from causing damage:
+
+1. **Safety hooks** — Block `AskUserQuestion`, destructive commands (`rm -rf`, `git push --force`, `DROP TABLE`), production deploys, and secret access
+2. **CLI constraints** — `--max-turns` caps agentic loops, `--output-format json` enables structured parsing
+3. **Mission brief** — Every task gets explicit constraints: no questions, no deploys, no destructive ops, commit work, output completion signal
+
+Safety hooks are **only active during task execution**, not during interactive Claude Code sessions.
+
+## Branch Isolation
+
+Each task runs on its own branch (`auto/{task-id}`). Main is never directly modified.
+
+- Uncommitted work is stashed before task execution and restored after
+- Tasks that produce commits get automatic PRs via `gh` CLI
+- Empty branches (no commits) are cleaned up automatically
+
+## Agents
+
+The `task-executor` agent helps monitor and debug task execution:
+
+```
+> "Why did my last task fail?"
+```
+
+It checks queue status, exit codes, Claude Code process state, and provides debugging guidance.
+
+## Requirements
+
+- bash 4+, python3, jq
+- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
+- Optional: `gh` CLI for automatic PR creation
+
+## Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `CC_QUEUE_FILE` | `./queue.json` | Path to the task queue |
+| `CC_POLL_INTERVAL` | `60` | Seconds between polls in loop mode |
+| `CC_MAX_TASKS` | `0` | Max tasks per run (0 = unlimited) |
+| `CC_MAX_TURNS` | `25` | Default Claude Code turns per task |
+
+## License
+
+Apache License 2.0 — Copyright 2026 Stackbilt LLC
diff --git a/plugins/cc-taskrunner/agents/task-executor.md b/plugins/cc-taskrunner/agents/task-executor.md
new file mode 100755
index 0000000000..4e6f1de60c
--- /dev/null
+++ b/plugins/cc-taskrunner/agents/task-executor.md
@@ -0,0 +1,62 @@
+---
+name: task-executor
+description: Use this agent to monitor or debug autonomous task execution. Examples - "Why did my task fail?", "Show me the output from the last task run", "Check if the taskrunner is still running"
+model: inherit
+color: cyan
+tools: ["Bash", "Read", "Grep", "Glob"]
+---
+
+You are a task execution specialist that helps users monitor, debug, and understand cc-taskrunner execution.
+
+## Capabilities
+
+1. **Check running tasks**: Look for active Claude Code processes
+2. **Read task output**: Parse JSON output files from completed tasks
+3. **Debug failures**: Analyze why tasks failed or got blocked
+4. **Queue analysis**: Identify duplicate tasks, conflicting file targets, or sizing issues
+
+## Debugging a Failed Task
+
+When a task fails, check these in order:
+
+1. **Queue status** — Read the queue file to find the failed task and its `result` field:
+ ```bash
+ cat ${CC_QUEUE_FILE:-queue.json} | python3 -c "import json,sys; [print(json.dumps(t, indent=2)) for t in json.load(sys.stdin) if t.get('status')=='failed']"
+ ```
+
+2. **Exit code meaning**:
+ - `0` = success with TASK_COMPLETE signal
+ - `1` = Claude Code error or crash
+ - `2` = TASK_BLOCKED reported
+ - `3` = no completion signal (task may have run out of turns)
+
+3. **Common failure patterns**:
+ - **Out of turns**: Task needed more than `max_turns`. Suggest increasing or splitting the task.
+ - **Blocked by safety hook**: Check if the task tried a destructive operation. Look for "BLOCKED:" in output.
+ - **TASK_BLOCKED**: Task hit an obstacle it couldn't resolve. Read the reason.
+ - **Branch conflict**: Task branch already exists with divergent history. Check `git branch -a`.
+
+4. **PR status** — If a task completed but PR creation failed:
+ ```bash
+ gh pr list --head "auto/" --state all
+ ```
+
+## Queue Health Check
+
+Analyze the queue for issues:
+- Tasks targeting the same files (merge conflict risk)
+- Tasks with overly broad prompts (scope creep risk)
+- Tasks with `max_turns` > 25 (should be split)
+- Stale `running` tasks (process may have crashed)
+
+## Process Check
+
+Look for running taskrunner processes:
+```bash
+ps aux | grep taskrunner.sh | grep -v grep
+```
+
+Look for active Claude Code sessions:
+```bash
+ps aux | grep "claude -p" | grep -v grep
+```
diff --git a/plugins/cc-taskrunner/commands/taskrunner-add.md b/plugins/cc-taskrunner/commands/taskrunner-add.md
new file mode 100755
index 0000000000..74e27a2c95
--- /dev/null
+++ b/plugins/cc-taskrunner/commands/taskrunner-add.md
@@ -0,0 +1,75 @@
+---
+description: Add a task to the autonomous queue
+argument-hint: ""
+allowed-tools: ["Bash", "Read", "Write", "AskUserQuestion"]
+---
+
+# cc-taskrunner — Add Task
+
+Add a task to the cc-taskrunner queue for autonomous execution.
+
+## Behavior
+
+**If $ARGUMENTS is provided:**
+Use it as the task title and prompt.
+
+**If $ARGUMENTS is empty:**
+Ask the user what task to add using AskUserQuestion with these fields:
+- Title: short description of the task
+- Prompt: detailed instructions (file paths, constraints, completion criteria)
+- Authority: `auto_safe` (branch + PR) or `operator` (run on current branch)
+- Max turns: 5-25 (default 25)
+
+## Steps
+
+1. Parse or gather task details.
+
+2. Add the task:
+ ```bash
+ bash ${CLAUDE_PLUGIN_ROOT}/taskrunner.sh add "$TASK_TITLE"
+ ```
+
+ For more control (custom repo, prompt, authority, turns), write directly to the queue file:
+ ```bash
+ python3 -c "
+ import json, uuid, datetime
+ task = {
+ 'id': str(uuid.uuid4()),
+ 'title': '''$TITLE''',
+ 'repo': '$REPO',
+ 'prompt': '''$PROMPT''',
+ 'authority': '$AUTHORITY',
+ 'max_turns': $MAX_TURNS,
+ 'status': 'pending',
+ 'created_at': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
+ }
+ queue_file = '${CC_QUEUE_FILE:-queue.json}'
+ try:
+ with open(queue_file) as f:
+ queue = json.load(f)
+ except (FileNotFoundError, json.JSONDecodeError):
+ queue = []
+ queue.append(task)
+ with open(queue_file, 'w') as f:
+ json.dump(queue, f, indent=2)
+ print(f'Added task {task[\"id\"][:8]}: {task[\"title\"]}')
+ "
+ ```
+
+3. Show confirmation with task ID and suggest running `/taskrunner` to execute.
+
+## Prompt Writing Tips
+
+Good prompts are specific:
+- Name file paths explicitly: "Read `src/services/quota.ts`"
+- State completion criteria: "Tests should pass"
+- Include context: each task is a fresh session
+- Say what NOT to do: "Do NOT modify the database schema"
+- End with: "Commit your work with a descriptive message"
+
+## Authority Levels
+
+| Authority | Branch? | PR? | Use for |
+|-----------|---------|-----|---------|
+| `operator` | No | No | Tasks that run on your current branch |
+| `auto_safe` | Yes | Yes | Tests, docs, research, refactors |
diff --git a/plugins/cc-taskrunner/commands/taskrunner-list.md b/plugins/cc-taskrunner/commands/taskrunner-list.md
new file mode 100755
index 0000000000..d969f38e64
--- /dev/null
+++ b/plugins/cc-taskrunner/commands/taskrunner-list.md
@@ -0,0 +1,26 @@
+---
+description: Show the current task queue
+argument-hint: ""
+allowed-tools: ["Bash", "Read"]
+---
+
+# cc-taskrunner — List Queue
+
+Show all tasks in the queue with their status.
+
+## Steps
+
+1. Run the list command:
+ ```bash
+ bash ${CLAUDE_PLUGIN_ROOT}/taskrunner.sh list
+ ```
+
+2. If the queue file doesn't exist or is empty, tell the user and suggest `/taskrunner-add`.
+
+3. Present the results. Status symbols:
+ - `○` pending — waiting to run
+ - `▶` running — currently executing
+ - `✓` completed — finished successfully
+ - `✗` failed — task errored or was blocked
+
+4. If there are pending tasks, suggest `/taskrunner` to execute them.
diff --git a/plugins/cc-taskrunner/commands/taskrunner.md b/plugins/cc-taskrunner/commands/taskrunner.md
new file mode 100755
index 0000000000..fc13da46ca
--- /dev/null
+++ b/plugins/cc-taskrunner/commands/taskrunner.md
@@ -0,0 +1,70 @@
+---
+description: Run pending tasks from the queue, show status, or execute a specific number of tasks
+argument-hint: "[--max N] [--dry-run] [--loop]"
+allowed-tools: ["Bash", "Read", "Glob", "Grep", "Agent"]
+---
+
+# cc-taskrunner — Run Task Queue
+
+Execute pending tasks from the queue using headless Claude Code sessions with safety hooks, branch isolation, and automatic PR creation.
+
+## Behavior
+
+**If $ARGUMENTS is empty or contains only flags:**
+Run the taskrunner with the provided flags (or defaults).
+
+**If $ARGUMENTS contains "--dry-run":**
+Preview what would run without executing.
+
+**If $ARGUMENTS contains "--max N":**
+Run at most N tasks.
+
+**If $ARGUMENTS contains "--loop":**
+Run in polling mode (check queue every 60s).
+
+## Steps
+
+1. Verify prerequisites exist:
+ ```bash
+ command -v claude && command -v jq && command -v python3
+ ```
+ If any are missing, tell the user what to install.
+
+2. Check that the taskrunner script exists:
+ ```bash
+ ls ${CLAUDE_PLUGIN_ROOT}/taskrunner.sh
+ ```
+
+3. Check current queue status first:
+ ```bash
+ bash ${CLAUDE_PLUGIN_ROOT}/taskrunner.sh list
+ ```
+
+4. If queue is empty and no `--loop` flag, inform user and suggest `/taskrunner-add` to queue tasks.
+
+5. If queue has pending tasks, execute:
+ ```bash
+ bash ${CLAUDE_PLUGIN_ROOT}/taskrunner.sh $ARGUMENTS
+ ```
+
+6. After execution completes, show a summary:
+ - Number of tasks completed vs failed
+ - Links to any PRs created
+ - Any tasks that reported TASK_BLOCKED
+
+## Important Notes
+
+- Tasks run in headless Claude Code sessions — safety hooks block interactive questions, destructive operations, and production deploys
+- Each task gets its own git branch (`auto/{task-id}`) unless it has `operator` authority
+- Uncommitted work in the repo is automatically stashed and restored
+- Tasks that produce commits get automatic PRs via `gh` CLI
+- The `--dry-run` flag previews tasks without executing them
+
+## Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `CC_QUEUE_FILE` | `./queue.json` | Path to the task queue |
+| `CC_POLL_INTERVAL` | `60` | Seconds between polls in loop mode |
+| `CC_MAX_TASKS` | `0` | Max tasks per run (0 = unlimited) |
+| `CC_MAX_TURNS` | `25` | Default Claude Code turns per task |
diff --git a/plugins/cc-taskrunner/safety/block-interactive.sh b/plugins/cc-taskrunner/safety/block-interactive.sh
new file mode 100755
index 0000000000..d071523433
--- /dev/null
+++ b/plugins/cc-taskrunner/safety/block-interactive.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# block-interactive.sh — Blocks AskUserQuestion in unattended sessions
+#
+# PreToolUse hook: exits 2 to block, 0 to allow.
+# When Claude tries to ask a question, this forces it to decide instead.
+#
+# Copyright 2026 Stackbilt LLC — Apache 2.0
+
+INPUT=$(cat)
+TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
+
+if [[ "$TOOL" == "AskUserQuestion" ]]; then
+ echo "BLOCKED: Autonomous mode — do not ask questions. Make a reasonable decision and document your reasoning." >&2
+ exit 2
+fi
+
+exit 0
diff --git a/plugins/cc-taskrunner/safety/safety-gate.sh b/plugins/cc-taskrunner/safety/safety-gate.sh
new file mode 100755
index 0000000000..eec34175ce
--- /dev/null
+++ b/plugins/cc-taskrunner/safety/safety-gate.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# safety-gate.sh — Blocks destructive commands in unattended sessions
+#
+# PreToolUse hook for Bash tool: checks command for dangerous patterns.
+# Blocks: rm -rf, git push --force, DROP TABLE, deploys, secret access.
+#
+# Copyright 2026 Stackbilt LLC — Apache 2.0
+
+INPUT=$(cat)
+TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
+
+if [[ "$TOOL" != "Bash" ]]; then
+ exit 0
+fi
+
+CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
+
+# Destructive filesystem operations
+if echo "$CMD" | grep -qiE '(rm\s+-rf|rm\s+-r\s+/|>\s*/dev/)'; then
+ echo "BLOCKED: Destructive filesystem operation not allowed in autonomous mode" >&2
+ exit 2
+fi
+
+# Destructive git operations
+if echo "$CMD" | grep -qiE '(git\s+reset\s+--hard|git\s+push\s+--force|git\s+push\s+-f|git\s+clean\s+-f)'; then
+ echo "BLOCKED: Destructive git operation not allowed in autonomous mode" >&2
+ exit 2
+fi
+
+# Database destruction
+if echo "$CMD" | grep -qiE '(DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM\s+\w+\s*$)'; then
+ echo "BLOCKED: Destructive database operation not allowed in autonomous mode" >&2
+ exit 2
+fi
+
+# Production deploys (require human approval)
+if echo "$CMD" | grep -qiE '(wrangler\s+deploy|wrangler\s+publish|npm\s+run\s+deploy|kubectl\s+apply|terraform\s+apply)'; then
+ echo "BLOCKED: Production deploys require human approval. Commit your work and stop." >&2
+ exit 2
+fi
+
+# Secret management
+if echo "$CMD" | grep -qiE '(wrangler\s+secret|echo\s+.*API_KEY|echo\s+.*TOKEN|echo\s+.*SECRET)'; then
+ echo "BLOCKED: Secret management not allowed in autonomous mode" >&2
+ exit 2
+fi
+
+exit 0
diff --git a/plugins/cc-taskrunner/safety/syntax-check.sh b/plugins/cc-taskrunner/safety/syntax-check.sh
new file mode 100755
index 0000000000..3df35bdbc4
--- /dev/null
+++ b/plugins/cc-taskrunner/safety/syntax-check.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# syntax-check.sh — PostToolUse hook for Edit/Write
+#
+# After editing TypeScript/JavaScript files, runs a quick syntax check
+# so errors are caught immediately rather than 50 tool calls later.
+#
+# Copyright 2026 Stackbilt LLC — Apache 2.0
+
+INPUT=$(cat)
+TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
+
+if [[ "$TOOL" != "Edit" && "$TOOL" != "Write" ]]; then
+ exit 0
+fi
+
+FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
+
+# Only check TypeScript/JavaScript files
+case "$FILE" in
+ *.ts|*.tsx|*.js|*.jsx)
+ # Find nearest tsconfig
+ DIR=$(dirname "$FILE")
+ TSCONFIG=""
+ while [[ "$DIR" != "/" && "$DIR" != "." ]]; do
+ if [[ -f "${DIR}/tsconfig.json" ]]; then
+ TSCONFIG="${DIR}/tsconfig.json"
+ break
+ fi
+ DIR=$(dirname "$DIR")
+ done
+
+ if [[ -n "$TSCONFIG" ]]; then
+ ERRORS=$(cd "$(dirname "$TSCONFIG")" && npx tsc --noEmit --pretty false 2>&1 | grep -c "error TS" || true)
+ if [[ "$ERRORS" -gt 0 ]]; then
+ echo "WARNING: ${ERRORS} TypeScript error(s) detected after editing ${FILE}. Run typecheck to see details." >&2
+ fi
+ fi
+ ;;
+esac
+
+# Never block — advisory only
+exit 0
diff --git a/plugins/cc-taskrunner/taskrunner.sh b/plugins/cc-taskrunner/taskrunner.sh
new file mode 100755
index 0000000000..b4f147d15f
--- /dev/null
+++ b/plugins/cc-taskrunner/taskrunner.sh
@@ -0,0 +1,506 @@
+#!/usr/bin/env bash
+# cc-taskrunner — Autonomous task queue for Claude Code
+#
+# Plugin-compatible version. Executes tasks from a local queue file using
+# headless Claude Code sessions with safety hooks, branch-per-task isolation,
+# and automatic PR creation.
+#
+# Copyright 2026 Stackbilt LLC
+# Licensed under Apache License 2.0
+#
+# Usage:
+# ./taskrunner.sh # Run until queue empty
+# ./taskrunner.sh --max 5 # Run at most 5 tasks
+# ./taskrunner.sh --loop # Loop forever (poll every 60s)
+# ./taskrunner.sh --dry-run # Show what would run without executing
+# ./taskrunner.sh add "Fix the bug" # Add a task to the queue
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+QUEUE_FILE="${CC_QUEUE_FILE:-$(pwd)/queue.json}"
+SAFETY_DIR="${SCRIPT_DIR}/safety"
+HOOKS_SETTINGS=""
+POLL_INTERVAL="${CC_POLL_INTERVAL:-60}"
+MAX_TASKS="${CC_MAX_TASKS:-0}" # 0 = unlimited
+MAX_TURNS="${CC_MAX_TURNS:-25}"
+DRY_RUN=false
+LOOP_MODE=false
+TASKS_RUN=0
+
+# ─── Parse args ──────────────────────────────────────────────
+
+ACTION=""
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --max) MAX_TASKS="$2"; shift 2 ;;
+ --loop) LOOP_MODE=true; shift ;;
+ --dry-run) DRY_RUN=true; shift ;;
+ --turns) MAX_TURNS="$2"; shift 2 ;;
+ add) ACTION="add"; shift; break ;;
+ list) ACTION="list"; shift ;;
+ *) echo "Unknown arg: $1" >&2; exit 1 ;;
+ esac
+done
+
+# ─── Helpers ─────────────────────────────────────────────────
+
+log() { echo "[$(date '+%H:%M:%S')] $*"; }
+err() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }
+
+# ─── Queue management ───────────────────────────────────────
+
+init_queue() {
+ if [[ ! -f "$QUEUE_FILE" ]]; then
+ echo '[]' > "$QUEUE_FILE"
+ fi
+}
+
+add_task() {
+ local title="$1"
+ local repo="${2:-.}"
+ local prompt="${3:-$title}"
+ local authority="${4:-auto_safe}"
+ local max_turns="${5:-$MAX_TURNS}"
+
+ init_queue
+
+ local task_id
+ task_id=$(python3 -c 'import uuid; print(str(uuid.uuid4()))')
+
+ python3 -c "
+import json, sys
+
+task = {
+ 'id': '$task_id',
+ 'title': sys.argv[1],
+ 'repo': sys.argv[2],
+ 'prompt': sys.argv[3],
+ 'authority': '$authority',
+ 'max_turns': int('$max_turns'),
+ 'status': 'pending',
+ 'created_at': '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
+}
+
+with open('$QUEUE_FILE', 'r') as f:
+ queue = json.load(f)
+queue.append(task)
+with open('$QUEUE_FILE', 'w') as f:
+ json.dump(queue, f, indent=2)
+
+print(f'Added task {task[\"id\"][:8]}: {task[\"title\"]}')
+" "$title" "$repo" "$prompt"
+}
+
+list_tasks() {
+ init_queue
+ python3 -c "
+import json
+with open('$QUEUE_FILE') as f:
+ queue = json.load(f)
+if not queue:
+ print('Queue is empty.')
+else:
+ for t in queue:
+ status = t.get('status', 'pending')
+ symbol = {'pending': '○', 'running': '▶', 'completed': '✓', 'failed': '✗'}.get(status, '?')
+ print(f'{symbol} {t[\"id\"][:8]} {status:10} {t[\"title\"][:60]}')
+"
+}
+
+fetch_next_task() {
+ init_queue
+ python3 -c "
+import json
+with open('$QUEUE_FILE') as f:
+ queue = json.load(f)
+for t in queue:
+ if t.get('status') == 'pending':
+ print(json.dumps(t))
+ break
+else:
+ print('')
+"
+}
+
+update_task_status() {
+ local task_id="$1" status="$2" result="${3:-}"
+ python3 -c "
+import json, sys
+with open('$QUEUE_FILE', 'r') as f:
+ queue = json.load(f)
+for t in queue:
+ if t['id'] == sys.argv[1]:
+ t['status'] = sys.argv[2]
+ if sys.argv[3]:
+ t['result'] = sys.argv[3][:4000]
+ break
+with open('$QUEUE_FILE', 'w') as f:
+ json.dump(queue, f, indent=2)
+" "$task_id" "$status" "$result"
+}
+
+# ─── Generate hooks settings ────────────────────────────────
+
+ensure_hooks_settings() {
+ HOOKS_SETTINGS=$(mktemp /tmp/cc-hooks-XXXXXX.json)
+
+ cat > "$HOOKS_SETTINGS" </dev/null)
+ authority=$(echo "$task_json" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("authority", "auto_safe"))' 2>/dev/null)
+
+ log "┌─ Task: ${title}"
+ log "│ ID: ${task_id:0:8}"
+ log "│ Repo: ${repo}"
+ log "│ Turns: ${max_turns}"
+
+ if $DRY_RUN; then
+ log "└─ [DRY RUN] Would execute. Skipping."
+ return 0
+ fi
+
+ # Resolve repo path
+ local repo_path
+ if [[ "$repo" == "." ]]; then
+ repo_path="$(pwd)"
+ elif [[ -d "$repo" ]]; then
+ repo_path="$(cd "$repo" && pwd)"
+ else
+ err "Repo not found: ${repo}"
+ update_task_status "$task_id" "failed" "Repo not found: ${repo}"
+ return 1
+ fi
+
+ # Mark as running
+ update_task_status "$task_id" "running"
+
+ # ─── Branch lifecycle ─────────────────────────────────────
+ local branch=""
+ local use_branch=false
+ local stashed=false
+ cd "$repo_path"
+
+ # Non-operator tasks get their own branch
+ if [[ "$authority" != "operator" ]]; then
+ use_branch=true
+ branch="auto/${task_id:0:8}"
+
+ # Stash uncommitted changes to protect live work
+ if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
+ git stash push -m "cc-taskrunner:${task_id:0:8}" --include-untracked 2>/dev/null && stashed=true
+ log "│ Stashed uncommitted changes"
+ fi
+
+ # Start from main
+ git checkout main 2>/dev/null || git checkout master 2>/dev/null
+ git pull --ff-only 2>/dev/null || true
+
+ # Create or reset task branch
+ if git rev-parse --verify "$branch" >/dev/null 2>&1; then
+ git checkout "$branch"
+ git reset --hard main 2>/dev/null
+ else
+ git checkout -b "$branch"
+ fi
+ log "│ Branch: ${branch}"
+ fi
+
+ # Build mission prompt
+ local mission_prompt
+ mission_prompt="$(cat <
+MISSION
+)"
+
+ # Snapshot tree state before task runs (to avoid auto-committing pre-existing files)
+ local pre_snapshot
+ pre_snapshot=$(mktemp /tmp/cc-pre-XXXXXX.txt)
+ cd "$repo_path"
+ git diff --name-only 2>/dev/null > "$pre_snapshot"
+ git ls-files --others --exclude-standard 2>/dev/null >> "$pre_snapshot"
+
+ # Execute
+ local output_file exit_code=0
+ output_file=$(mktemp /tmp/cc-task-XXXXXX.json)
+ trap "rm -f ${output_file} ${pre_snapshot} ${HOOKS_SETTINGS}" RETURN
+
+ log "│ Starting Claude Code session..."
+
+ cd "$repo_path"
+ unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT 2>/dev/null || true
+ eval "$(build_claude_cmd "$mission_prompt" "$max_turns")" \
+ > "$output_file" 2>&1 || exit_code=$?
+
+ # Extract result
+ local result_text
+ result_text=$(python3 -c '
+import json, sys
+try:
+ data = json.load(open(sys.argv[1]))
+ print(data.get("result", ""))
+except:
+ with open(sys.argv[1]) as f:
+ print(f.read()[:4000])
+' "$output_file" 2>/dev/null || cat "$output_file" | head -c 4000)
+
+ # ─── Handle commits, push, PR ──────────────────────────────
+ local pr_url=""
+ cd "$repo_path"
+
+ if $use_branch; then
+ local commit_count
+ commit_count=$(git rev-list main..HEAD --count 2>/dev/null || echo "0")
+
+ # Only auto-commit files that the TASK created/modified (not pre-existing dirty files)
+ local task_dirty_files=()
+ while IFS= read -r f; do
+ [[ -z "$f" ]] && continue
+ if ! grep -qxF "$f" "$pre_snapshot" 2>/dev/null; then
+ task_dirty_files+=("$f")
+ fi
+ done < <(git diff --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
+
+ if [[ ${#task_dirty_files[@]} -gt 0 ]]; then
+ log "│ Auto-committing ${#task_dirty_files[@]} task-created files (skipping pre-existing changes)"
+ for f in "${task_dirty_files[@]}"; do
+ git add "$f" 2>/dev/null
+ done
+ git commit -m "auto: uncommitted changes from task ${task_id:0:8}
+
+Task: ${title}" 2>/dev/null || true
+ commit_count=$((commit_count + 1))
+ fi
+
+ # Push and create PR if there are commits
+ if [[ "$commit_count" -gt 0 ]]; then
+ log "│ Pushing ${commit_count} commit(s) to ${branch}..."
+ git push -u origin "$branch" 2>/dev/null || true
+
+ # Create PR if gh CLI is available
+ if command -v gh >/dev/null 2>&1; then
+ local remote_url repo_slug
+ remote_url=$(git remote get-url origin 2>/dev/null)
+ repo_slug=$(echo "$remote_url" | sed -E 's|.*github\.com[:/](.+)(\.git)?$|\1|' | sed 's/\.git$//')
+
+ pr_url=$(gh pr create \
+ --repo "$repo_slug" \
+ --base main \
+ --head "$branch" \
+ --title "[auto] ${title}" \
+ --body "$(cat </dev/null || echo "")
+
+ if [[ -n "$pr_url" ]]; then
+ log "│ PR created: ${pr_url}"
+ result_text="${result_text}
+
+[cc-taskrunner] PR: ${pr_url}"
+ else
+ log "│ WARNING: PR creation failed"
+ fi
+ fi
+ else
+ log "│ No commits on branch — cleaning up"
+ git checkout main 2>/dev/null || git checkout master 2>/dev/null
+ git branch -D "$branch" 2>/dev/null || true
+ branch=""
+ if [[ "$stashed" == "true" ]]; then
+ git stash pop 2>/dev/null && log "│ Restored stashed changes" || true
+ stashed=false
+ fi
+ fi
+
+ # Return to main
+ git checkout main 2>/dev/null || git checkout master 2>/dev/null
+
+ # Restore stashed changes
+ if [[ "$stashed" == "true" ]]; then
+ git stash pop 2>/dev/null && log "│ Restored stashed changes" || log "│ WARNING: stash pop failed"
+ fi
+ fi
+
+ # Check completion signal
+ if echo "$result_text" | grep -qF "TASK_COMPLETE"; then
+ log "│ Completion signal found"
+ elif echo "$result_text" | grep -qF "TASK_BLOCKED"; then
+ log "│ Task reported BLOCKED"
+ exit_code=2
+ else
+ log "│ WARNING: No completion signal in output"
+ if [[ $exit_code -eq 0 ]]; then
+ exit_code=3
+ fi
+ fi
+
+ # Update queue
+ local status="completed"
+ [[ $exit_code -ne 0 ]] && status="failed"
+ update_task_status "$task_id" "$status" "$result_text"
+
+ if [[ $exit_code -eq 0 ]]; then
+ log "└─ COMPLETED${pr_url:+ (PR: ${pr_url})}"
+ else
+ log "└─ FAILED (exit code ${exit_code})"
+ fi
+
+ TASKS_RUN=$((TASKS_RUN + 1))
+ return $exit_code
+}
+
+# ─── Handle subcommands ─────────────────────────────────────
+
+if [[ "$ACTION" == "add" ]]; then
+ add_task "$*"
+ exit 0
+fi
+
+if [[ "$ACTION" == "list" ]]; then
+ list_tasks
+ exit 0
+fi
+
+# ─── Main loop ───────────────────────────────────────────────
+
+main() {
+ ensure_hooks_settings
+
+ log "cc-taskrunner starting"
+ log " Queue: ${QUEUE_FILE}"
+ log " Safety: ${SAFETY_DIR}"
+ log " Max: $([ "$MAX_TASKS" -eq 0 ] && echo 'unlimited' || echo "$MAX_TASKS")"
+ log " Turns: ${MAX_TURNS}"
+ log " Mode: $(${DRY_RUN} && echo 'DRY RUN' || echo 'LIVE')"
+
+ if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
+ log " GitHub: authenticated"
+ else
+ log " GitHub: not authenticated (PRs will be skipped)"
+ fi
+ log ""
+
+ while true; do
+ if [[ "$MAX_TASKS" -gt 0 && "$TASKS_RUN" -ge "$MAX_TASKS" ]]; then
+ log "Task limit reached (${TASKS_RUN}/${MAX_TASKS}). Stopping."
+ break
+ fi
+
+ local task_json
+ task_json=$(fetch_next_task)
+
+ if [[ -z "$task_json" ]]; then
+ if $LOOP_MODE; then
+ log "Queue empty. Polling again in ${POLL_INTERVAL}s..."
+ sleep "$POLL_INTERVAL"
+ continue
+ else
+ log "Queue empty. ${TASKS_RUN} task(s) completed. Done."
+ break
+ fi
+ fi
+
+ execute_task "$task_json" || true
+ sleep 2
+ done
+}
+
+main