# Loops in Copilot CLI

Run a goal loop that stays on the ticket-service flaky suite until it is green, and a nightly triage loop, with Copilot CLI autopilot, the agentStop hook and a scheduled Actions workflow.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/loops/copilot-cli

## At a glance

| | Copilot CLI |
|---|---|
| Term | Autopilot mode (`/goal`), scheduled prompts (`/every`, `/after`), `agentStop` hook |
| Configured in | `--autopilot` and `--max-autopilot-continues` flags, `.github/hooks/*.json`, `.github/workflows/*.yml` |
| Loads / runs | Autopilot sends automatic continuation messages until the agent calls `task_complete`; `/every` fires only while a session is open; a workflow starts a fresh `copilot -p` run each time |
| Scope & precedence | Autopilot and `/every` are per session; the hook matches the repository; the workflow is per repository |

## Build the scenario

Goal loop: the agent works in autopilot, and an `agentStop` hook runs the real check before the agent may stop.

| Step | What you write or run | Where |
|---|---|---|
| 1 | Register the hook | `.github/hooks/flaky-loop.json` |
| 2 | `gate.sh`: run `pnpm test` three times, block the stop if any run fails | `.github/hooks/scripts/gate.sh` |
| 3 | Start the loop with caps | Terminal |
| 4 | Stop at success, a cap or `Ctrl+C` | Terminal |

```json
{ "version": 1, "hooks": { "agentStop": [
  { "type": "command", "bash": "./.github/hooks/scripts/gate.sh",
    "cwd": ".", "timeoutSec": 900 }
] } }
```

```bash
#!/bin/bash
IN=$(cat); cd "$(jq -r .cwd <<<"$IN")"; mkdir -p .agent
[ -f .agent/loop-active ] || exit 0
for i in 1 2 3; do
  pnpm test > .agent/last-test.log 2>&1 || {
    jq -cn --arg r "pnpm test failed on run $i of 3. Read .agent/last-test.log, fix the cause, append to .agent/loop-notes.md, commit." \
      '{decision:"block",reason:$r}'; exit 0; }
done
```

```bash
touch .agent/loop-active
GITHUB_COPILOT_PROMPT_MODE_REPO_HOOKS=true copilot --autopilot \
  --allow-tool='write, shell(pnpm:*), shell(git:*)' \
  --max-autopilot-continues 10 --max-ai-credits 200 \
  -p "Make the flaky test suite green: \
pnpm test must pass 3 runs in a row. Read .agent/loop-notes.md first. After each attempt, \
note what you tried and commit. Call task_complete only when the 3 runs pass."
```

In an interactive session, the same goal is `/goal Make the flaky test suite green --max-ai-credits 50`.

Recurring loop: a scheduled workflow runs one triage pass; the state lives in a single GitHub issue labeled `flaky`.

```yaml
name: Nightly flaky triage
on: { schedule: [{ cron: '0 2 * * *' }], workflow_dispatch: }
permissions: { contents: read, issues: write }
jobs:
  triage:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
      - run: npm install -g @github/copilot pnpm && pnpm install
      - env: { COPILOT_GITHUB_TOKEN: "${{ secrets.COPILOT_PAT }}", GH_TOKEN: "${{ github.token }}" }
        run: copilot -p "$(cat .github/flaky-triage.md)" --no-ask-user --max-ai-credits 60
          --allow-tool='shell(pnpm:*)' --allow-tool='shell(gh:*)'
```

`.github/flaky-triage.md` tells the run to: run `pnpm test`, find the open issue labeled `flaky` with `gh issue list`, add a comment per newly failing test or create the issue if none exists, and never open a second one.

## Specifics

### Goal loops

| Feature | Start | Stops when | Runs |
|---|---|---|---|
| Autopilot mode | `--autopilot`, Shift+Tab, `/autopilot [OBJECTIVE]` | Agent calls `task_complete`, a blocking problem, `Ctrl+C`, continuation limit | Current session |
| `/goal [OBJECTIVE]` | Slash command; `/goal on` and `/goal off` toggle the mode | Same as autopilot, plus the credit cap | Current session |
| `copilot --autopilot -p` | Shell, script or CI | Same as autopilot; the process then exits | Local or runner |

The agent decides the goal is met. The documentation describes no built-in check in autopilot, so your "3 green runs" lives in the prompt, in a hook, or both. By default the mode is sticky after a task (`stayInAutopilot`). Without full permissions, tool requests that need approval are denied. The example allows only file writes, `pnpm` and `git`, so everything else is refused instead of asked; `--yolo` would allow everything and belongs only in a [sandbox](https://ai-sw-factory.mellicci.dev/fundamentals/sandboxing).

### Stop conditions and limits

| Loop stop | Mechanism | Notes |
|---|---|---|
| Success | `task_complete`, or the `agentStop` gate lets the turn end | The hook is the one check that runs outside the model |
| Max 10 rounds | `--max-autopilot-continues 10` | Documented default is 5 in the autopilot article and unlimited in the command reference; always set it |
| Budget | `--max-ai-credits N`, `/goal ... --max-ai-credits N`, `/limits set max-ai-credits N` | Soft limit, minimum 30 (the how-to calls it a session limit, the command reference a per-response one); `-p` runs end at the limit, interactive runs pause and ask for a new amount |
| Time | The documentation doesn't specify a wall-clock cap | Use `timeout` locally or `timeout-minutes` in Actions |
| Human | `Ctrl+C`, `/goal off`, delete a schedule | |

### Progress between iterations

| Where | What survives | Notes |
|---|---|---|
| `.agent/loop-notes.md` and commits | What you told the agent to write | You own this; it survives a new session |
| Session | Conversation, `[Scheduled prompt]` entries | `--continue` or `--resume` restores it |
| Compaction | Summary of older turns, saved as a numbered checkpoint | Starts near 80% of the context window; `/session checkpoints` lists them; fine detail is lost |
| Goal panel | Objective, credits used, todo progress | Display only |
| GitHub issue | Recurring loop state | Each workflow run starts with no memory |

### Recurring and scheduled runs

| Mechanism | Start | Stops when | Runs |
|---|---|---|---|
| `/every [INTERVAL] PROMPT` (alias `/loop`) | Interactive, experimental (`/experimental on`) | Schedule deleted or session ends; self-paced ones stop when nothing is left to watch | Open session only |
| `/after DELAY PROMPT` | Same | Fires once | Open session only |
| cron or Task Scheduler with `copilot -p` | External scheduler | Process exits | Your machine |
| Actions `schedule` + `copilot -p` | Workflow file | Process exits | Runner |

Fixed intervals run from 10 seconds to 1 day. Commands that change session or configuration (`/model`, `/clear`, `/compact`) can't be scheduled. Because nothing runs when the session is closed, the nightly loop uses Actions. The docs also recommend GitHub Agentic Workflows for most automation.

### Hooks as loop control

| Item | Detail |
|---|---|
| Event | `agentStop`, when the main agent finishes a turn |
| Input | `sessionId`, `cwd`, `transcriptPath`, `stopReason`, `stop_hook_active` |
| Output | `decision: "block"` with `reason` forces another turn, using `reason` as the prompt |
| Cap | After 8 consecutive blocks the CLI ends the turn anyway |
| Self-limit | `stop_hook_active` is true when a previous block already forced this turn |
| `subagentStop` | Same decision fields |

## Gotchas

- The 8-block cap is lower than the 10-round goal. The documentation doesn't specify whether blocks count toward `--max-autopilot-continues`, so check the limits against a real run.
- A gate that exceeds `timeoutSec` fails open and the agent stops. Three `pnpm test` runs need a generous timeout, as in the example.
- In `-p` mode, repository hooks load only for a trusted folder, `COPILOT_ALLOW_ALL` or `GITHUB_COPILOT_PROMPT_MODE_REPO_HOOKS=true`.
- Unattended autopilot needs full permissions, which can delete files. Use a sandbox or a throwaway branch and keep the credit cap.
- `/every` ends with the session and is experimental; never rely on it for the nightly job.
