Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/piloto/protocolo.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ python3 -m venv .venv

Repita com `--flow catalog --cycle bug`, `--flow orders --cycle feature`, `--flow orders --cycle bug` e `--flow catalog --cycle guardrail`.

No G5, revise o PR no GitHub e faça o merge antes de responder `a` no terminal. Ao final, consolide os dados:
No G5, revise o PR no GitHub e faça o merge antes de responder `a` no terminal. Se responder antes do merge, o supervisor espera o merge sem registrar uma nova decisão. Ao final, consolide os dados:

```bash
.venv/bin/python -m orchestrator report
```

## Front de homologação

Durante o G7, a homologação fica no ar em http://localhost:8080 para o aprovador conferir o comportamento antes de autorizar a virada. O front está fora do escopo dos agentes; funcionalidades novas sem tela, como o cancelamento de pedido, são conferidas pelas APIs em `/docs`.
Durante o G7, a homologação fica no ar em http://localhost:8080 para o aprovador conferir o comportamento antes de autorizar a virada. Com a virada autorizada, o ambiente continua no ar com a versão liberada, simulando produção. No rollback, ele é derrubado. O front está fora do escopo dos agentes; funcionalidades novas sem tela, como o cancelamento de pedido, são conferidas pelas APIs em `/docs`.

## Papéis nos gates

Expand All @@ -106,7 +106,7 @@ Durante o G7, a homologação fica no ar em http://localhost:8080 para o aprovad
| G5 | Revisão e merge do PR | Desenvolvedor sênior; QA em bugs |
| G7 | Autorização da virada ou rollback | DevOps |

A virada para produção é simulada por uma tag `release/<execução>`. O sistema é fictício e não há produção real.
A virada para produção é simulada: o supervisor cria a tag `release/<execução>` e mantém o ambiente no ar com a versão liberada. O sistema é fictício e não há produção real.

## Ensaios

Expand Down
17 changes: 11 additions & 6 deletions orchestrator/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,11 @@ async def _review(self, pull_request: PullRequest) -> None:
self.platform.push(self.worktree, self.branch)
await self._wait_ci(pull_request)
self.platform.complete_review(self.worktree, pull_request)
if self.platform.is_merged(self.worktree, pull_request):
self.trace.emit("pr_merged", number=pull_request.number, rejections=rejections)
return
self._failure("processo", "G5 aprovado sem merge efetivo do PR", gate="G5")
print("O PR ainda não foi mesclado. Faça o merge e responda novamente.")
while not self.platform.is_merged(self.worktree, pull_request):
self.trace.emit("merge_pending", number=pull_request.number)
self.gates.wait(f"O PR {pull_request.url} ainda não foi mesclado. Faça o merge e pressione Enter.")
self.trace.emit("pr_merged", number=pull_request.number, rejections=rejections)
return

async def _rework(self, pull_request: PullRequest, decision: GateDecision) -> None:
comments = self.platform.review_comments(self.worktree, pull_request)
Expand All @@ -275,6 +275,8 @@ async def _rework(self, pull_request: PullRequest, decision: GateDecision) -> No
async def _deploy_and_release(self) -> str:
self.platform.sync_merged(self.worktree)
self._record_coverage("after")
(self.worktree / DEPLOY_REPORT).parent.mkdir(exist_ok=True)
released = False
try:
await self._agent(
DEPLOY,
Expand All @@ -298,9 +300,12 @@ async def _deploy_and_release(self) -> str:
tag = f"release/{self.run_id}"
self.platform.tag_release(self.worktree, tag)
self.trace.emit("release", tag=tag)
released = True
return "completed"
finally:
run(["bash", "scripts/deploy_homolog.sh", "down"], self.worktree, check=False)
# A versão liberada no G7 continua no ar como produção simulada; nos demais casos o ambiente é derrubado.
if not released:
run(["bash", "scripts/deploy_homolog.sh", "down"], self.worktree, check=False)

def _archive_deploy_report(self) -> str:
source = self.worktree / DEPLOY_REPORT
Expand Down
8 changes: 8 additions & 0 deletions orchestrator/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class GateDecision:
class Gates(Protocol):
def decide(self, gate: str, summary: str, can_edit: bool = True) -> GateDecision: ...

def wait(self, message: str) -> None: ...


class TerminalGates:
"""Aprovação humana real; o tempo informado alimenta a métrica de esforço."""
Expand Down Expand Up @@ -65,6 +67,9 @@ def decide(self, gate: str, summary: str, can_edit: bool = True) -> GateDecision
answer = input(f"Minutos de trabalho humano [{wall / 60:.1f}]: ").strip().replace(",", ".")
return GateDecision(decision, comment, approver, wall, float(answer) if answer else wall / 60, category, target)

def wait(self, message: str) -> None:
input(message)


class ScriptedGates:
"""Somente para ensaios e testes: as execuções geradas ficam fora do relatório oficial."""
Expand All @@ -76,3 +81,6 @@ def decide(self, gate: str, summary: str, can_edit: bool = True) -> GateDecision
if self.decisions:
return self.decisions.pop(0)
return GateDecision("approved", "aprovação automática de ensaio", "ensaio", 0.0, 0.0)

def wait(self, message: str) -> None:
pass
13 changes: 13 additions & 0 deletions orchestrator/platform.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
Expand Down Expand Up @@ -52,6 +53,7 @@ def tag_release(self, worktree: Path, tag: str) -> None: ...
class GitHubPlatform:
name = "github"
base_ref = "origin/main"
CHECKS_TIMEOUT_S = 300

def prepare_base(self) -> None:
run(["git", "fetch", "origin", "main", "--tags"], REPO_ROOT)
Expand All @@ -69,6 +71,7 @@ def push(self, worktree: Path, branch: str) -> None:
run(["git", "push", "origin", branch], worktree)

def wait_ci(self, worktree: Path, pr: PullRequest, service: str) -> CiResult:
self._wait_checks_registered(worktree, pr)
watched = run(["gh", "pr", "checks", str(pr.number), "--watch", "--interval", "15"], worktree, check=False)
if watched.returncode == 0:
return CiResult(True, watched.stdout)
Expand All @@ -80,6 +83,16 @@ def wait_ci(self, worktree: Path, pr: PullRequest, service: str) -> CiResult:
logs = run(["gh", "run", "view", str(run_ids[0]["databaseId"]), "--log-failed"], worktree, check=False)
return CiResult(False, logs.stdout[-12000:])

def _wait_checks_registered(self, worktree: Path, pr: PullRequest) -> None:
"""Logo após o push o GitHub ainda não listou os checks, e o `--watch` sairia como falha."""
deadline = time.monotonic() + self.CHECKS_TIMEOUT_S
while time.monotonic() < deadline:
listed = run(["gh", "pr", "checks", str(pr.number), "--json", "name"], worktree, check=False)
if listed.stdout.strip() not in ("", "[]"):
return
time.sleep(5)
raise RuntimeError(f"Nenhum check de CI foi registrado no PR #{pr.number} em {self.CHECKS_TIMEOUT_S}s")

def is_merged(self, worktree: Path, pr: PullRequest) -> bool:
view = run(["gh", "pr", "view", str(pr.number), "--json", "state"], worktree)
return json.loads(view.stdout)["state"] == "MERGED"
Expand Down
29 changes: 26 additions & 3 deletions orchestrator/tests/test_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def repo(tmp_path, monkeypatch):
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "t@t")
(tmp_path / "scripts").mkdir()
(tmp_path / "scripts/healthcheck.py").write_text("print('ok')\n")
(tmp_path / "scripts/deploy_homolog.sh").write_text("exit 0\n")
(tmp_path / "scripts/deploy_homolog.sh").write_text('mkdir -p .pilot && echo "$1" >> .pilot/deploy.log\n')
(tmp_path / "services/catalog/tests").mkdir(parents=True)
(tmp_path / "services/catalog/mod.py").write_text("VALUE = 1\n")
(tmp_path / "docs/backlog").mkdir(parents=True)
Expand All @@ -44,15 +44,16 @@ class FakePlatform:
name = "fake"
base_ref = "main"

def __init__(self, ci=(True,)):
def __init__(self, ci=(True,), merged=(True,)):
self.ci = list(ci)
self.merged = list(merged)
self.tags = []

def prepare_base(self): ...
def open_pr(self, worktree, branch, title, body): return PullRequest(1, "url", branch)
def push(self, worktree, branch): ...
def wait_ci(self, worktree, pr, service): return CiResult(self.ci.pop(0) if self.ci else True, "log")
def is_merged(self, worktree, pr): return True
def is_merged(self, worktree, pr): return self.merged.pop(0) if self.merged else True
def complete_review(self, worktree, pr): ...
def review_comments(self, worktree, pr): return ""
def sync_merged(self, worktree): ...
Expand Down Expand Up @@ -122,6 +123,27 @@ def test_pr_body_closes_demand_issue(repo, tmp_path_factory):
assert "Closes #7" in supervisor._pr_body()


def deploy_calls(repo):
log = repo / ".pilot/deploy.log"
return log.read_text().split() if log.exists() else []


async def test_approved_release_keeps_environment_running(repo, tmp_path_factory):
supervisor, _, _, _ = build(repo, tmp_path_factory)

assert await supervisor.run() == "completed"
assert "down" not in deploy_calls(repo)


async def test_g5_waits_for_merge_without_new_gate_decision(repo, tmp_path_factory):
supervisor, _, _, trace = build(repo, tmp_path_factory, platform=FakePlatform(merged=(False, False, True)))

assert await supervisor.run() == "completed"
assert [e["gate"] for e in events(trace, "gate_decision")] == ["G3", "G5", "G7"]
assert len(events(trace, "merge_pending")) == 2
assert not events(trace, "failure")


async def test_bug_cycle_skips_requirements(repo, tmp_path_factory):
supervisor, runner, _, trace = build(repo, tmp_path_factory, cycle="bug")

Expand Down Expand Up @@ -176,6 +198,7 @@ async def test_g7_rejection_does_not_release(repo, tmp_path_factory):
assert await supervisor.run() == "rolled_back"
assert platform.tags == []
assert events(trace, "rollback")
assert deploy_calls(repo) == ["down"]


async def test_budget_exhaustion_aborts_with_category(repo, tmp_path_factory):
Expand Down
2 changes: 2 additions & 0 deletions pilot/report.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run_id,demand,flow,cycle,status,wall_h,agent_min,human_min,cost_usd,tokens_in,tokens_out,tool_calls,interventions,rejections,edits,ci_failures,coverage_before,coverage_after,guardrail_blocks,failures,block_rules
20261110T234154-cat-bug-01,CAT-BUG-01,catalog,bug,completed,0.11617554401527778,1.8026666666666666,6.833226390750012,0.38498559999999993,458474,8345,34,4,0,0,1,64,77,3,"{'ci_quebrado': 1, 'processo': 2}","{'shell_metachar': 2, 'command_not_allowed': 1}"
29 changes: 29 additions & 0 deletions pilot/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Relatório do piloto

Gerado por `python -m orchestrator report` a partir de `pilot/traces/*.jsonl`. Ensaios ficam fora.

## Métricas por execução

| Demanda | Ciclo | Status | Tempo total (h) | Agentes (min) | Humano (min) | Custo (US$) | Intervenções | Reprovações | Edições | Falhas de CI | Cobertura antes → depois |
|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---|
| CAT-BUG-01 | bug | completed | 0.12 | 1.8 | 6.8 | 0.38 | 4 | 0 | 0 | 1 | 64% → 77% |

## Totais por fluxo

| Fluxo | Execuções | Custo (US$) | Humano (min) | Agentes (min) | Intervenções |
|---|---:|---:|---:|---:|---:|
| catalog | 1 | 0.38 | 6.8 | 1.8 | 4 |

## Falhas observadas

| Categoria | Ocorrências |
|---|---:|
| processo | 2 |
| ci_quebrado | 1 |

## Bloqueios de guarda-corpo

| Regra | Ocorrências |
|---|---:|
| shell_metachar | 2 |
| command_not_allowed | 1 |
30 changes: 30 additions & 0 deletions pilot/runs/20261110T234154-cat-bug-01/deploy_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Relatório de deploy em homologação

## Versão
`f9d7f51 Merge pull request #8 from mathcolombo:pilot/20261110T234154-cat-bug-01`

## Resultado dos passos

1. `bash scripts/deploy_homolog.sh up`: sucesso. As imagens de catalog, cart e orders foram construídas, quase todas a partir de cache. Os containers catalog e web foram recriados. rabbitmq, cart e orders já estavam em execução. Todos ficaram `Healthy`.
2. `python scripts/healthcheck.py`: sucesso.
- OK saúde http://localhost:8001
- OK saúde http://localhost:8002
- OK saúde http://localhost:8003
- OK front http://localhost:8080
- OK evento product.created
- OK saga de pedido confirmada

## Anomalias nos logs

Examinei apenas os logs do serviço `catalog` (últimas 100 linhas). Não há erros nem avisos: startup normal, `/health` e `/products` com 200 e `POST /products` com 201. Os logs de cart, orders, web e rabbitmq não foram examinados, porque o healthcheck passou e o procedimento só pede logs em caso de falha.

## Observações

- A imagem base é `python:3.14-slim`, enquanto o AGENTS.md fala em Python 3.12+. Não é uma falha, mas vale confirmar que a versão é intencional.
- Não foi feito nenhum rollback nem qualquer ação em produção.

## Recomendação ao aprovador

Homologação saudável e com a saga de pedido validada de ponta a ponta. Recomendo seguir para a decisão do gate G7.

STATUS: OK
Loading
Loading