Motor Loop¶
Daemon que executa ciclos autônomos em série — Modo B (24/7 com kill switch) ou Modo A (fila curada pelo Felipe) — agora com fail-fast em falha de ambiente.
Path: _core/motor_loop.py Issues: DEV-1132, DEV-1134/1135/1136/1137, DEV-1300 (Modo A), DEV-1345/1346 (endurecimento a falha sistêmica)
Por que foi construído assim¶
O loop é serial por design (um ciclo por vez) — reduz colisão de worktree, claim e budget enquanto a confiança do motor amadurece. PR em review não bloqueia a próxima issue (aprovação assíncrona), e o motor nunca mergeia em produção — entrega PR ou branch conforme a matriz de alçada.
O endurecimento de 2026-07-16 nasceu de incidente real: com o 1Password rate-limitado, o Modo A queimou uma fila de 18 issues em 5 segundos, todas com o mesmo erro de credencial disfarçado de "LINEAR_API_KEY não encontrada". Dois problemas distintos: o diagnóstico mentia (DEV-1345) e o loop tratava falha de ambiente como falha de issue, insistindo e agravando o rate-limit (DEV-1346).
Stack¶
| Camada | Tecnologia |
|---|---|
| Linguagem | Python 3.12, stdlib-first |
| Credenciais | 1Password via _shared/secrets.py (cascata env → op.env → op CLI) |
| Fila/estado | Linear (GraphQL via _shared/linear_client.py) |
| Onde roda | VPS Dev — container pd-motor (Modo B) ou processo avulso (Modo A) |
Como funciona¶
flowchart TD
classDef warning fill:#FEF9C3,stroke:#EAB308,color:#111
classDef core fill:#FEE2E2,stroke:#EF4444,color:#111
classDef external fill:#FEF3C7,stroke:#F59E0B,color:#111
classDef component fill:#D1FAE5,stroke:#10B981,color:#111
classDef flow fill:#DBEAFE,stroke:#3B82F6,color:#111
classDef decision fill:#EDE9FE,stroke:#8B5CF6,color:#111
subgraph SG_component["Componentes"]
a_hdr["Componentes"]
a_loop[("_core/docs/motor-loop.md")]
a_reaper[("_core/docs/stale-claim-reaper.md")]
a_secrets["secrets.py (`_shared`)"]
a_linear_client["linearclient.py (`shared`)"]
a_cron["cron VPS Dev"]
end
class a_hdr,a_loop,a_reaper,a_secrets,a_linear_client,a_cron component
subgraph SG_flow["Fluxo do processo"]
b_hdr["Fluxo (Modo A)"]
b1["Felipe entrega uma fila curada de issues pro motor (Modo A)"]
b2["Motor pega a próxima issue e pede as credenciais ao cofre (…"]
b3["Cofre respondeu → motor trabalha a issue e segue pra próxima"]
b6["Em paralelo, de hora em hora, o vigia de claims (reaper) pi…"]
end
class b_hdr,b1,b2,b3,b6 flow
subgraph SG_decision["Decisões"]
c_hdr["Decisões & ramificações"]
c1["O erro é do ambiente ou da issue?"]
c2["Reaper — 2 estágios, nunca punitivo de primeira"]
c3["Guard rails do reaper"]
c4["Por que o diagnóstico importa"]
end
class c_hdr,c1,c2,c3,c4 decision
t["Motor Autônomo — endurecimento a falha sistêmica"]
class t core
subgraph SG_external["Integrações externas"]
a_1p["1Password (op CLI)"]
a_linear["Linear API"]
end
class a_1p,a_linear external
subgraph SG_warning["Gotchas / Erros"]
b4["Cofre falhou (ex.: excesso de requisições) → o erro chega c…"]
b5["Motor reconhece que o problema é do AMBIENTE, não da issue …"]
end
class b4,b5 warning
b1 --> b2
b2 -->|"cofre OK"| b3
b2 -->|"cofre falhou"| b4
b4 --> b5
b5 --> c1
b6 --> c2 Modo B (daemon): a cada iteração consulta o kill switch (motor.is_enabled(fresh=True)). OFF → idle; ON → motor_run.run_cycle() (seleciona issue own:motor, claima, abre worktree, roda harness, abre PR ou libera). Loop infinito até kill switch OFF ou SIGTERM.
Modo A (curadoria explícita): motor_loop.py run --issues DEV-X DEV-Y — processa só a lista fornecida, na ordem, e encerra ao zerar. Bypassa o kill switch: a presença de --issues é a permissão explícita do Felipe.
Fail-fast (DEV-1346): todo erro de ciclo é classificado em sistêmico (credencial, rede — derruba qualquer issue) ou da issue (ex.: worker declarou bloqueio). Sistêmico em Modo A aborta a fila imediatamente com exit code 3 e reporta quantas issues nem foram tentadas; em Modo B vira idle estendido em vez de martelar a causa. Erro da issue mantém o comportamento antigo: registra e segue pra próxima.
Diagnóstico honesto (DEV-1345): quando o op CLI do 1Password falha, o stderr real (rate-limit, vault sem acesso, token expirado) viaja na exceção até o log — "credencial não configurada" e "1Password fora do ar" agora são mensagens diferentes.
Decisões técnicas¶
- Erro de ambiente ≠ erro de issue — insistir na fila com o ambiente quebrado só queima ciclos e agrava a causa (o resolve de credencial re-tenta o
oppor issue). Abortar cedo com relato claro é mais barato que 18 erros idênticos mudos. - Exit code 3 dedicado — "abortado por infra" é distinguível de "houve erros por issue" (exit 1) pra qualquer orquestrador em cima do loop.
- Falha operacional nunca vira mensagem genérica — regra do
_shareddesde os incidentes de 04/07 (credencial falhando mudo), agora aplicada também ao caminho do op CLI. - Modo A bypassa kill switch intencionalmente — curadoria explícita = permissão explícita.
Gotchas & armadilhas¶
- Heurística de erro sistêmico é por tipo + substring (
API_KEY,rate-limit,credencial...) — mensagem de erro de issue contendo essas palavras dispara falso-positivo; o efeito é conservador (aborta a fila), mas ajuste a heurística em vez de removê-la. run --oncepode demorar minutos com o harness trabalhando — timeout de shell não é necessariamente falha.- Logs do Modo A vão pra
motor-a.out.logno container, não prodocker logs— a skill/logar-motordetecta o modo e traz o log certo. - Modo A não exige label
own:motor— o--issuesclaima qualquer issue da lista.
Como operar¶
# Modo A — fila curada
python _core/motor_loop.py run --issues DEV-477 DEV-496
# Modo B — daemon (container pd-motor)
python _core/motor_loop.py run
# Self-test (inclui os cenários de fail-fast F/G/H)
python _core/motor_loop.py --self-test
Skills de operação: /ligar-motor, /desligar-motor, /logar-motor.
FAQ¶
O motor parou no meio da fila do Modo A — perdi as issues restantes? Não. O abort sistêmico só não tenta as restantes — elas continuam na fila do Linear intocadas. Resolva a causa (ex.: esperar o rate-limit do 1P passar) e rode o Modo A de novo com a lista restante (o stats do run abortado lista quais foram puladas).
Como sei se o run falhou por infra ou pelas issues? Exit code: 3 = abortado por infra (aborted_systemic no stats); 1 = uma ou mais issues falharam individualmente; 0 = tudo ok.
Por que não re-tentar com backoff em vez de abortar? A spec do DEV-1346 aceitava as duas; abortar foi escolhido por ser mais simples e porque Modo A sempre tem um humano por perto (é curadoria explícita) — relatar rápido > insistir às cegas.
Refs¶
- Vigia de claims: Stale-Claim Reaper
- Docs repo:
_core/docs/motor-loop.md·_core/MOTOR-AUTONOMO.md·_core/PR-ESCALATION-MATRIX.md - Sessão do endurecimento:
sessions-log/2026-07-16/motor-fix-dev-1344-1345-1346-reaper-diagnostico-fail-fast_2026-07-16_0009.md
Fonte repo: pd-framework/_core/docs/motor-loop.md
Notas Relacionadas¶
Motor-Autonomo - Motor-Loop