# Iteration Loop Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a revision cycle to the cage-orchestrator so users can review exported work, provide feedback, and send inner Claude back to revise — all within the same cage session.

**Architecture:** Two new message types (`task_revision`, `going_idle`) added to the existing messaging protocol. Inner agent prompt gains a post-completion polling loop. Outer workflow gains a revision decision step that loops back to export. CLI changes are minimal — new message type in `cage-send` validation, `going_idle` as a poll-terminating condition in `tc outbox --poll`, and help text updates.

**Tech Stack:** Python (Typer/Rich CLI), Bash (cage-send script), Markdown (SKILL.md)

**Spec:** `docs/superpowers/specs/2026-03-28-iteration-loop-design.md`

---

## Chunk 1: trusty-cage CLI Changes

### Task 1: Add `going_idle` to `cage-send` valid types

**Files:**
- Modify: `src/trusty_cage/assets/cage-send:21`

- [ ] **Step 1: Add `going_idle` to VALID_TYPES**

In `src/trusty_cage/assets/cage-send`, change line 21 from:
```bash
VALID_TYPES="task_complete progress_update error info_request"
```
to:
```bash
VALID_TYPES="task_complete progress_update error info_request going_idle"
```

Note: `task_revision` is intentionally excluded from `cage-send`. It flows outer → inner via `tc inbox`, not via `cage-send` (which is inner → outer only).

- [ ] **Step 2: Commit**

```bash
git add src/trusty_cage/assets/cage-send
git commit -m "feat: add going_idle to cage-send valid message types"
```

---

### Task 2: Add `going_idle` as poll-terminating condition in `outbox_read`

**Files:**
- Modify: `src/trusty_cage/cli.py:1134-1135` (poll help text)
- Modify: `src/trusty_cage/cli.py:1161-1172` (poll loop task_complete handler)
- Test: `tests/test_cli.py` (new tests)

- [ ] **Step 1: Write failing test for going_idle poll termination**

Add to `tests/test_cli.py`:

Note: Add imports (`MagicMock`, `patch`, `Message`, `CliRunner`, `app`) at the top of the test file, not inline. The project requires all imports at the top of the file.

```python
class TestOutboxPollGoingIdle:
    """Test that --poll exits with code 2 on going_idle message."""

    def test_poll_exits_on_going_idle(self):
        """tc outbox --poll should exit with code 2 when going_idle is received."""
        mock_meta = MagicMock()
        mock_meta.container_name = "isolated-dev-test"

        going_idle_msg = Message(
            id="msg-test-idle",
            type="going_idle",
            timestamp="2026-03-28T15:00:00.000Z",
            payload={"reason": "No task_revision received", "waited_seconds": 3600},
            version=1,
        )

        with (
            patch("trusty_cage.cli._require_env_running", return_value=mock_meta),
            patch(
                "trusty_cage.cli.read_outbox", return_value=[going_idle_msg]
            ),
            patch("trusty_cage.cli.set_cursor"),
        ):
            runner = CliRunner()
            result = runner.invoke(app, ["outbox", "test", "--poll"])
            assert result.exit_code == 2
            assert "Inner agent idle" in result.output
            assert "3600" in result.output

    def test_poll_continues_past_progress_then_exits_on_going_idle(self):
        """Non-terminal messages don't stop polling; going_idle does."""
        mock_meta = MagicMock()
        mock_meta.container_name = "isolated-dev-test"

        progress_msg = Message(
            id="msg-test-progress",
            type="progress_update",
            timestamp="2026-03-28T14:00:00.000Z",
            payload={"status": "working", "detail": "halfway done"},
            version=1,
        )
        going_idle_msg = Message(
            id="msg-test-idle",
            type="going_idle",
            timestamp="2026-03-28T15:00:00.000Z",
            payload={"reason": "No task_revision received", "waited_seconds": 3600},
            version=1,
        )

        with (
            patch("trusty_cage.cli._require_env_running", return_value=mock_meta),
            patch(
                "trusty_cage.cli.read_outbox",
                return_value=[progress_msg, going_idle_msg],
            ),
            patch("trusty_cage.cli.set_cursor"),
        ):
            runner = CliRunner()
            result = runner.invoke(app, ["outbox", "test", "--poll"])
            assert result.exit_code == 2
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pytest tests/test_cli.py::TestOutboxPollGoingIdle -v`
Expected: FAIL — no handling for `going_idle` in poll loop yet.

- [ ] **Step 3: Add going_idle handling to poll loop**

In `src/trusty_cage/cli.py`, find the poll loop's `task_complete` handler (around line 1161). Add a new `elif` branch **before** the `task_complete` check:

```python
elif msg.type == "going_idle":
    reason = msg.payload.get("reason", "Inner agent went idle")
    waited = msg.payload.get("waited_seconds", 0)
    rprint(
        f"[bold yellow]Inner agent idle:[/bold yellow] {reason} "
        f"(waited {waited}s)"
    )
    if messages:
        set_cursor(meta.container_name, messages[-1].timestamp)
    raise typer.Exit(2)
```

- [ ] **Step 4: Update --poll help text**

In `src/trusty_cage/cli.py`, change the `poll` option help text (around line 1134) from:
```python
poll: bool = typer.Option(
    False, "--poll", help="Poll until a task_complete message arrives"
),
```
to:
```python
poll: bool = typer.Option(
    False,
    "--poll",
    help="Poll until a task_complete or going_idle message arrives",
),
```

- [ ] **Step 5: Run test to verify it passes**

Run: `pytest tests/test_cli.py::TestOutboxPollGoingIdle -v`
Expected: PASS

- [ ] **Step 6: Run full test suite**

Run: `pytest`
Expected: All tests pass.

- [ ] **Step 7: Lint**

Run: `ruff format . && ruff check --fix .`

- [ ] **Step 8: Commit**

```bash
git add src/trusty_cage/cli.py tests/test_cli.py
git commit -m "feat: treat going_idle as poll-terminating condition (exit code 2)"
```

---

### Task 3: Update `inbox_send` help text

**Files:**
- Modify: `src/trusty_cage/cli.py:1211`

- [ ] **Step 1: Update msg_type help text**

In `src/trusty_cage/cli.py`, change line 1211 from:
```python
msg_type: str = typer.Argument(help="Message type (info_response, ack)"),
```
to:
```python
msg_type: str = typer.Argument(
    help="Message type (info_response, ack, task_revision)"
),
```

- [ ] **Step 2: Lint**

Run: `ruff format . && ruff check --fix .`

- [ ] **Step 3: Commit**

```bash
git add src/trusty_cage/cli.py
git commit -m "docs: add task_revision to inbox_send help text"
```

---

## Chunk 2: cage-orchestrator SKILL.md Changes

### Task 4: Add new message types to protocol reference

**Files:**
- Modify: `/Users/areese/projects/personal/agent_skills/.agent/skills/cage-orchestrator/SKILL.md:379-386`

- [ ] **Step 1: Add `task_revision` and `going_idle` to message types table**

Find the message types table (around line 379) and add two rows:

```markdown
| `task_revision` | outer → inner | `{"instructions": str}` |
| `going_idle` | inner → outer | `{"reason": str, "waited_seconds": int}` |
```

These go after the existing `ack` row.

- [ ] **Step 2: Commit**

```bash
cd /Users/areese/projects/personal/agent_skills
git add .agent/skills/cage-orchestrator/SKILL.md
git commit -m "feat: add task_revision and going_idle to messaging protocol reference"
```

---

### Task 5: Add post-completion polling loop to inner agent prompt

**Files:**
- Modify: `/Users/areese/projects/personal/agent_skills/.agent/skills/cage-orchestrator/SKILL.md:95-160`

- [ ] **Step 1: Add "After Task Completion" section to inner agent prompt**

Find the end of the inner agent prompt template (after the messaging protocol instructions, around line 160). Append the following block:

```markdown
## After Task Completion

When you have completed the task:
1. Send a `task_complete` message via `cage-send`
2. Run the following polling script to wait for revised instructions:
```bash
INTERVAL=10; MAX_WAIT=3600; ELAPSED=0
CURSOR_FILE=~/.cage/cursor/inbox.cursor
CURSOR=$(cat "$CURSOR_FILE" 2>/dev/null || echo "")
while [ $ELAPSED -lt $MAX_WAIT ]; do
  for f in $(ls -1 ~/.cage/inbox/ 2>/dev/null | sort); do
    if [ -z "$CURSOR" ] || [ "$f" \> "$CURSOR" ]; then
      cat ~/.cage/inbox/"$f"
      echo "$f" > "$CURSOR_FILE"
      exit 0
    fi
  done
  sleep $INTERVAL
  ELAPSED=$((ELAPSED + INTERVAL))
  INTERVAL=$((INTERVAL * 2 > 300 ? 300 : INTERVAL * 2))
done
echo "POLL_TIMEOUT"
```
3. If the script returns JSON, parse it — it will be a `task_revision` message. Read the `payload.instructions` field and continue working on the project accordingly. When done, repeat from step 1.
4. If the script returns `POLL_TIMEOUT`, send a `going_idle` message via `cage-send`:
   ```bash
   cage-send going_idle '{"reason":"No task_revision received within polling timeout","waited_seconds":3600}'
   ```
   Then stop working.
```

- [ ] **Step 2: Commit**

```bash
cd /Users/areese/projects/personal/agent_skills
git add .agent/skills/cage-orchestrator/SKILL.md
git commit -m "feat: add post-completion polling loop to inner agent prompt"
```

---

### Task 6: Restructure outer workflow with revision cycle

**Files:**
- Modify: `/Users/areese/projects/personal/agent_skills/.agent/skills/cage-orchestrator/SKILL.md:172-260`

- [ ] **Step 1: Replace raw docker exec polling with `tc outbox --poll` in Step 8**

Find Step 8 (Monitor via Messaging, around line 172). Replace the raw `docker exec ls` polling instructions with:

```markdown
### Step 8: Monitor Progress

Monitor via CLI commands:
- Stream inner Claude's reasoning: `trusty-cage logs "$ENV_NAME" -f`
- Poll for structured messages: `trusty-cage outbox "$ENV_NAME" --poll`

`tc outbox --poll` will:
- Print `progress_update` and `error` messages as they arrive
- Exit with code 0 when `task_complete` is received
- Exit with code 2 when `going_idle` is received (inner Claude timed out waiting for revisions)
- Exit with code 1 on error or timeout

Handle the exit code:
- **Exit 0** → proceed to Step 9
- **Exit 2** → inform user that inner Claude went idle; offer to re-launch if needed
- **Exit 1** → diagnose (check container status, `tc logs`)
```

- [ ] **Step 2: Restructure Steps 9-11 into revision cycle**

Replace the current Steps 9-11 (Review Results, Export and Overlay, Cleanup) with:

```markdown
### Step 9: Export and Overlay

Run: `trusty-cage export "$ENV_NAME" --yes --output-dir .`

This rsyncs container files onto the host clone, preserving the host's `.git/` directory.

### Step 10: Review Changes with User

Show the user what changed:
```bash
git diff
git diff --stat
```

Discuss the results. Let the user test, inspect, or ask questions.

### Step 11: Revision Decision

Ask the user:
> "Would you like to revise and send inner Claude back to work, or are we done?"

**If revise:**
1. Gather revised instructions from the user — what to change, fix, or improve
2. Send to inner Claude:
   ```bash
   trusty-cage inbox "$ENV_NAME" task_revision '{"instructions": "<user feedback here>"}'
   ```
3. Go back to **Step 8** (Monitor Progress)

**If done:**
Proceed to Step 12.

### Step 12: Cleanup

Ask the user whether to:
- `trusty-cage destroy "$ENV_NAME" --yes` — remove container + volume (host clone preserved)
- Keep the cage alive for later use
```

- [ ] **Step 3: Add edge case handling notes**

After Step 12, add:

```markdown
### Edge Cases

**Inner Claude goes idle during revision cycle:**
If `tc outbox --poll` exits with code 2 (`going_idle`), inner Claude's polling timed out. The cage is still alive. Inform the user and offer to re-launch:
```bash
trusty-cage launch "$ENV_NAME" --prompt "Check your inbox for instructions and continue working on the project"
```
This starts a fresh Claude session (losing conversational context) but preserves all file state.

**Inner Claude crashes:**
If `tc outbox --poll` times out (exit 1) without receiving `task_complete`, verify inner Claude is alive before sending a `task_revision`. Check container status or try `tc logs "$ENV_NAME"`. If dead, offer to re-launch.

**Multiple rapid revisions:**
Not supported — always wait for `task_complete` before offering another revision cycle.
```

- [ ] **Step 4: Commit**

```bash
cd /Users/areese/projects/personal/agent_skills
git add .agent/skills/cage-orchestrator/SKILL.md
git commit -m "feat: restructure outer workflow with revision cycle and tc outbox/logs"
```

---

## Chunk 3: Verification

### Task 7: End-to-end verification

- [ ] **Step 1: Verify cage-send accepts going_idle**

```bash
# Inside a cage container (or test locally):
bash src/trusty_cage/assets/cage-send going_idle '{"reason":"test","waited_seconds":0}'
```
Expected: Should not error on type validation. (Will fail on missing directory if not in a cage, but type validation happens first.)

- [ ] **Step 2: Run full trusty-cage test suite**

```bash
cd /Users/areese/projects/personal/trusty-cage
pytest
```
Expected: All tests pass.

- [ ] **Step 3: Lint both repos**

```bash
cd /Users/areese/projects/personal/trusty-cage
ruff format . && ruff check --fix .
```

- [ ] **Step 4: Review SKILL.md reads correctly**

Read through the updated SKILL.md end-to-end to verify:
- Inner prompt has the polling loop section
- Outer workflow steps 8-12 flow logically with the revision cycle
- Message types table has all 8 types (6 original + `task_revision` + `going_idle`)
- No dangling references to old step numbers
