# `/loop` — Especificação funcional v0

## Propósito

`/loop` transforma uma resposta pontual em supervisão contínua dentro da sessão atual do Pi. A extensão acorda o agente periodicamente para reavaliar um objetivo fixo usando o histórico atualizado da mesma conversa.

O mecanismo é apropriado para esperar estado externo não notificável pelo harness, reproduzir flakiness, acompanhar PR/MR e fatiar trabalho longo. Não substitui:

- a reinvocação nativa do harness para processos que o próprio agente iniciou em background;
- uma execução única;
- `/schedule` ou outro serviço que sobreviva sem a sessão aberta.

## Invariantes

1. Existe no máximo um loop ativo ou pausado por sessão. Essa restrição permite que `status`, `pause`, `resume`, `stop` e `update` não recebam ID; suportar múltiplos loops exigirá introduzir identidade explícita em toda a CLI.
2. O objetivo é imutável e repetido literalmente em toda iteração.
3. Toda iteração termina por uma decisão estruturada de `loop_control`.
4. Uma decisão estruturada é a última ação da iteração; a tool retorna `terminate: true`.
5. Toda supervisão tem limites de prazo e iterações.
6. Pausa preserva objetivo e contadores, mas não suspende o prazo de calendário.
7. Toda transição definitiva cancela timers e despertares ainda não entregues quando isso for possível.
8. Despertares concorrentes são coalescidos: nunca há mais de uma iteração do loop pendente.
9. Decisões do agente são reportadas, não verificadas pela extensão.

## Interface de comando

```text
/loop start --every <duração|auto> [--after <duração>]
            [--for <duração>] [--max-iterations <n>]
            [--min-delay <duração>] [--max-delay <duração>]
            <objetivo>
/loop status
/loop pause
/loop resume
/loop stop
/loop update [--every <duração|auto>] [--for <duração>]
             [--max-iterations <n>] [--min-delay <duração>]
             [--max-delay <duração>]
```

### Defaults

- `--for 24h`
- `--max-iterations 50`
- `--min-delay 1m`
- `--max-delay 24h`
- sem `--after`: primeira iteração imediata

`--after` sempre exige uma duração concreta, inclusive em auto-ritmo. A espera inicial conta no horizonte. A CLI deve avisar quando ela consumir a maior parte do horizonte; o limiar exato pode ser calibrado durante a implementação.

Durações fixas e atrasos escolhidos no auto-ritmo devem respeitar a faixa mínima/máxima. Valores fora dela são erros explícitos, nunca ajustados silenciosamente.

### `update`

`update` altera somente a política, nunca o objetivo. Os novos limites são absolutos:

- em `50/50`, `--max-iterations 100` resulta em `50/100`;
- ampliar `--for` move o prazo absoluto para `startedAt + nova duração`, sem reiniciar o relógio;
- reduzir um limite para valor já consumido mantém o loop pausado pelo limite correspondente.

Um loop pausado por limite pode ser ampliado e retomado. `resume` depois do prazo vigente é recusado e produz `duration_limit`; para retomar a mesma identidade, o usuário precisa primeiro ampliar `--for` com `update`.

## Ritmo

### Ritmo fixo

O intervalo definido pelo usuário é invariável. `loop_control continue` não recebe nem pode alterar o atraso. Apenas `/loop update --every ...` muda o intervalo.

A próxima espera começa depois que a iteração termina. A duração da execução da iteração continua consumindo o horizonte.

### Auto-ritmo

Cada decisão `continue` deve fornecer um atraso concreto. A extensão valida esse atraso contra:

- `minDelay`;
- `maxDelay`;
- tempo restante no horizonte.

Se o atraso ultrapassar o tempo restante, a decisão é aceita sem nova iteração: a extensão agenda a pausa exatamente no fim do horizonte e emite `duration_limit`.

## Envelope de iteração

Cada despertar injeta uma mensagem de usuário legível e auditável contendo, no mínimo:

```text
[loop iteration]
Objetivo (verbatim): ...
Iteração: N de MAX
Tempo de calendário decorrido: ...
Tempo até o prazo: ...
Ritmo: fixed 10m | auto (faixa 1m..24h)
Última decisão/resumo/evidência: ...

Reavalie o objetivo com o estado atual. Não use polling do loop para um processo
background que o harness já observa. Termine chamando loop_control como sua última ação.
```

Se o agente estiver ocupado quando o timer vencer, a mensagem entra como `followUp`. Vários vencimentos durante o mesmo turno são coalescidos em uma única mensagem.

Mensagens normais do usuário não pausam nem reiniciam o timer.

## Tool `loop_control`

A tool oferece comandos no modo imperativo; os estados e eventos resultantes usam particípio:

- `continue`: objetivo ainda requer supervisão e retorna o loop ao estado `waiting`; não existe estado chamado `continue`;
- `complete`: objetivo alcançado, produz `completed`;
- `fail`: resultado externo negativo observado, produz `failed`;
- `block`: supervisão incapaz de prosseguir sem intervenção humana, produz `blocked`; o estado real do objetivo pode ser desconhecido.

Campos conceituais:

```ts
type LoopDecision =
  | { decision: "continue"; summary: string; evidence: string; nextDelay?: string }
  | { decision: "complete"; summary: string; evidence: string }
  | { decision: "fail"; summary: string; evidence: string }
  | { decision: "block"; summary: string; evidence: string };
```

Regras:

- `summary` e `evidence` são campos próprios e obrigatórios em todas as decisões.
- `nextDelay` é obrigatório apenas para `continue` em auto-ritmo.
- `nextDelay` é proibido para ritmo fixo.
- `complete` e `fail` encerram definitivamente como `completed` e `failed`.
- `block` pausa como `blocked` e permite `resume` depois da intervenção, desde que o prazo ainda não tenha vencido.
- a evidência do agente deve citar estado observado, comando executado ou referência consultada; a extensão não a verifica.

### Decisão ausente ou inválida

Ao assentar uma iteração sem decisão válida, a extensão envia uma única cobrança imediata como continuação da mesma iteração. O retry:

- não incrementa `iterationCount`;
- incrementa o `protocolRetryCount` cumulativo do loop;
- marca `protocolRetryUsed: true` apenas para a iteração corrente;
- é uma chamada paga e permanece visível na contabilidade.

O direito ao retry zera no início de cada nova iteração, inclusive a primeira após `resume`; o contador cumulativo nunca zera em pause, resume, reload ou persistência. Uma segunda omissão/invalidade na mesma iteração, ou atingir o teto cumulativo v0 de 5 retries, pausa o loop com `protocol_error`; assim, `maxIterations: 50` permite no máximo 55 chamadas de iteração/protocolo, não 100.

## Estados e transições

Estados principais:

- `waiting`: timer armado;
- `queued`: iteração coalescida aguardando o agente assentar;
- `running`: iteração em execução;
- `paused`: retomável;
- `completed`: encerrado com sucesso reportado;
- `failed`: encerrado por falha observada;
- `stopped`: encerrado pelo usuário.

Motivos de pausa que requerem atenção:

- `blocked` (produzido pela decisão `block`);
- `duration_limit`;
- `iteration_limit`;
- `protocol_error`.

Outros motivos de pausa:

- comando `/loop pause`;
- fechamento da sessão;
- fork/clone/navegação de árvore.

`pause` e `stop` pedidos durante uma iteração não abortam ferramentas/modelo em voo. A intenção é aplicada quando o agente assentar e prevalece sobre qualquer `continue` emitido pela rodada.

## Tempo e limites

O horizonte é um prazo de calendário calculado como `startedAt + forMs`:

- inclui `--after`, esperas, fila, execução das iterações e períodos pausados;
- continua correndo com a sessão fechada;
- não é reiniciado por `update` ou `resume`;
- pode ser ampliado por `update --for`, sempre em relação ao `startedAt` original.

Ao atingir `maxIterations`, o loop pausa com `iteration_limit`. Ao atingir o prazo enquanto a runtime está aberta — inclusive se o loop já estiver pausado — o timer de prazo materializa e avisa `duration_limit` imediatamente. Se o prazo vencer sem runtime, a próxima abertura da sessão materializa o evento antes de qualquer `resume`: `endedAt` registra o instante real do prazo (`startedAt + forMs`) e `observedAt`, o instante posterior em que a extensão pôde observá-lo e avisar.

## Persistência e ciclo de vida

O estado é versionado e persistido na própria sessão por entradas customizadas, permitindo reconstrução da branch atual.

- fechamento normal: persiste como pausado; reabrir nunca rearma automaticamente;
- `/reload`: reinício transparente; rearma os timers de despertar e prazo com o tempo restante; se o prazo já venceu durante o reload, segue a mesma materialização tardia da abertura de sessão, preservando `endedAt` no prazo real e usando o retorno da runtime como `observedAt`;
- estado de versão desconhecida após reload: pausa com motivo explícito, sem descartar dados;
- fork, clone ou navegação em `/tree`: pausa antes da bifurcação/navegação e informa o usuário; nenhum timer é duplicado.

## Eventos de atenção

Um único enum plano é usado pelo aviso e pelo hook:

```ts
type AttentionStatus =
  | "completed"
  | "failed"
  | "blocked"
  | "duration_limit"
  | "iteration_limit"
  | "protocol_error";
```

Envelope versionado:

```ts
interface LoopAttentionEventV1 {
  version: 1;
  status: AttentionStatus;
  summary: string;
  evidence: {
    source: "agent" | "extension";
    text: string;
  };
  objective: string;
  iterationCount: number;
  maxIterations: number;
  protocolRetryCount: number;
  calendarElapsedMs: number;
  deadlineRemainingMs: number;
  session: {
    id: string;
    file?: string;
    cwd: string;
  };
  endedAt: string; // instante semântico da transição; no duration_limit, o prazo real
  observedAt: string; // instante em que a runtime observou/materializou o evento
}
```

`completed`, `failed` e `blocked` usam evidência de origem `agent`. Limites e erro de protocolo usam evidência factual de origem `extension`.

Todo evento de atenção:

1. é registrado visivelmente na conversa/TUI;
2. atualiza o status da extensão;
3. tenta executar o hook externo, se configurado.

Pausa/stop voluntários, reload e fork não disparam o hook.

## Hook externo v0

Configuração global em `~/.pi/agent/loop.json`:

```json
{
  "onAttention": "/caminho/absoluto/para/script"
}
```

Contrato:

- executa um único arquivo diretamente, sem shell, argumentos ou interpolação;
- envia `LoopAttentionEventV1` como JSON no stdin;
- fica fora do caminho crítico;
- tem timeout curto;
- falha ou timeout do hook são mostrados na TUI, mas não alteram o estado do loop nem geram outro evento de atenção.

O timeout exato é detalhe calibrável na implementação.

## Casos conscientemente deferidos para implementação

A API/runtime do Pi deve informar a serialização concreta destes casos:

- `stop` chegando com um `followUp` já enfileirado;
- `update --every` durante um despertar em voo;
- transição terminal enquanto existe timer ou mensagem pendente;
- ponto exato em que uma iteração enfileirada passa a contar em `iterationCount`.

Regra guarda-chuva: intenção explícita do usuário e estados terminais prevalecem; qualquer transição terminal cancela o que ainda puder despertar ou reagendar o loop, e nenhum vencimento produz mais de uma iteração pendente.

## Fora de escopo da v0

- sobrevivência sem a sessão/runtime aberta;
- múltiplos loops na mesma sessão;
- alteração do objetivo de um loop existente;
- verificação automática da evidência;
- polling automático de processos background observáveis pelo harness;
- notificadores específicos de desktop, webhooks ou templates shell;
- garantia de entrega do hook.
