29 KiB
Impl: OPS110 — Acompanhar runs longos de agente por attach/detach, sem manter terminal aberto
Status: rascunho Atualizado em: 2026-09-15 Issue: #1019 Intenção: docs/plans/ops110-attach-detach-runs-agent.md Appetite restante: ~1–2 dias eng (herdado) — cabe: 1 CLI + 1 lib pura + 2 specs unitários + docs/changelog; sem migration/UI/access/Consent.
Leitura da intenção
- Outcome (nas palavras do aceite): o run continua trabalhando sem nenhum cliente olhando (nenhum terminal/SSH aberto no workstation); (re)entrar de outro dispositivo retoma exatamente a mesma sessão, mesmo worktree, mesmo histórico; sair do cliente nunca é encerrar (encerrar é ato explícito); nada novo escutando além do loopback sem credencial (qualquer superfície além de
127.0.0.1exige auth, fail-closed semOPENCODE_SERVER_PASSWORD); worktree/branch segue o contrato de ambiente (sem nova forma de claim, sem sessão órfã); a sessão é endereçável a partir da Issue/worktree — contrato que o OPS109 (painel) consumirá. - O que NÃO negociar: não vira pool/supervisor (OPS65 removido — não ressuscitar); não cria caminho que burle gates humanos (PR/CI/approval); não cria segunda porta de entrada que ignore claim/worktree; não exige terminal aberto nem processo pendurado em SSH; não abre porta pública/internet; não é web UI como produto; sem notificações/frota/métricas; sem mudar claim/gates. Impeccable A — N/A (ferramenta/ops, sem UI).
- O que reavaliar (hipóteses da intenção/explorador):
- "Servidor nativo (a) vs multiplexador tmux (b)" → decidido neste plano: servidor + attach (D1/D2). O tmux cai porque não tem auto-approve fora do TUI, não dá session ID/URL endereçável para o OPS109 e adiciona dependência externa/preso a um pty.
- "o
opencode servesozinho sustenta o run" → falso: sessão só-servidor sem cliente fica pendurada na permissão de bash (fato verificado ao vivo; request fica em/permission). O design passa a exigir um driver destacado vivo pelo run inteiro — é o--autodele que aprova, como o--autodo launch de hoje. - "HTTP puro (
prompt_async/REST) resolve sem cliente" → falso pelo mesmo fato; rejeitado em D2. - "reusar o 4096 (Zed
opencode acp) ou o 4097 (opencode webdo usuário)" → rejeitado: processos de terceiros na máquina; servidor próprio em 4199 (TEQO_AGENT_SERVER_PORToverridável), fora da faixa de dev dos worktrees (3100+slot, slot ≤ 999 → até 4099). - "a sessão só existe quando o TUI abre" → falso:
POST /session?directory=Dcria a sessão antes do run; o ID já nasce endereçável e vai para o estado nostart. - "
opencode attach/TUI fecha o run" → falso (probes): SIGKILL no clienterun, fechar o TUI etmux kill-sessionnão abortaram o run server-side;POST /session/{id}/abortinterrompe e o driver sai. Ctrl+C no TUI pode limpar input em vez de sair (não aborta o run).
Abordagem recomendada
flowchart TD
W["worktree next/plan/new/fix (terminal)"] --> L["launch node scripts/agent-session.mjs start<br/>--purpose P --dir D --model M [--issue N] [--argument B]"]
L --> H{"GET 127.0.0.1:4199/global/health"}
H -->|"down / estranho"| SV["spawn detached: opencode serve<br/>--hostname 127.0.0.1 --port 4199 + log"]
H -->|"up"| ST
SV --> ST{"estado do branch existe<br/>e sessão viva?"}
ST -->|"sim"| AT["opencode attach URL -s SES (foreground)"]
ST -->|"não"| CS["POST /session?directory=D<br/>→ sessionID endereçável"]
CS --> DR["spawn detached: opencode run --attach URL -s SES<br/>--model M --auto --command CMD -- args + log"]
DR --> AT
AT -.->|"fechar terminal (SIGHUP) = detach"| RUN["run segue no servidor<br/>(o --auto do driver aprova)"]
RUN -.->|"laptop/celular"| AT2["agent-session attach<br/>(--session | --branch | --issue | branch do cwd)"]
AT2 -.-> RUN
AT2 -->|"ato explícito"| STOP["agent-session stop<br/>POST /session/id/abort + kill do driver + stoppedAt"]
STOP --> LJ["list --json (contrato OPS109)"]
RUN --> LJ
Opções consideradas: A) opencode serve compartilhado em loopback (127.0.0.1:4199) + driver destacado por run + opencode attach como cliente TUI; B) multiplexador de terminal (tmux) mantendo o TUI vivo entre anexações; C) servidor + HTTP puro (prompt_async/REST) sem cliente/driver vivo; D) um opencode serve por worktree.
Recomendação: A — é a única direção que preserva os três aceites ao mesmo tempo: o run sobrevive sem cliente (servidor dono da sessão + driver --auto destacado), o reattach é exato (-s <sessionID> no mesmo --dir) e a sessão é endereçável (sessionID + url no estado, base do contrato OPS109). Os probes ao vivo na 1.18.31 validaram cada peça: fechar/SIGKILL no cliente não aborta; o exit do driver é 0 ao concluir; abort interrompe e o driver sai; --command com separador -- entrega $ARGUMENTS corretamente (work-issue -- --issue 1019 passou "--issue 1019").
Alternativas rejeitadas: B — sem auto-approve fora do TUI (o run ficaria pendurado na permissão assim que o humano saísse), sem session ID/URL estável para o OPS109 e dependência externa (tmux) no caminho crítico; C — a permissão de bash pendura sem cliente vivo (fato verificado) e não há como "responder depois" sem superfície de permissão; D — N portas/processos para gerenciar, URLs múltiplas para o OPS109 e o mesmo custo de driver por run (nenhum ganho de isolamento, já que as sessões são escopadas por directory).
Decisões de engenharia (caras de reverter)
D1 — Topologia do servidor
- Opções: A) um
opencode servecompartilhado em127.0.0.1:4199(TEQO_AGENT_SERVER_PORT), sessões escopadas pordirectory(recomendada); B) um servidor por worktree; C) reusar 4096/4097 de terceiros; D) porta efêmera com registro em arquivo. - Recomendação: A — um único processo loopback, uma URL estável por workstation, endereçamento simples para o OPS109;
serveé idempotente (health check antes de subir). - Alternativas rejeitadas: B — N processos/portas e N URLs, sem ganho (sessões já isolam por diretório); C — são donos de terceiros (Zed ACP,
opencode webdo usuário), não nossos para gerenciar/derrubar; D — endereço instável entre reboots quebra o contrato de endereçamento do OPS109.
D2 — Quem sustenta o run (driver destacado)
- Opções: A) driver destacado por run (
opencode run --attach URL -s SES --dir DIR --model M --auto --command CMD -- <args>, spawndetached: true+unref()+ log em fd, precedentescripts/auto-unblock.mjs:344–374) (recomendada); B)prompt_async/HTTP puro sem cliente; C) tmux com o TUI vivo; D) confiar no servidor sozinho. - Recomendação: A — o driver é quem carrega o
--autodo run inteiro e auto-aprova permissões; verificado que ele sai com exit 0 ao concluir, que SIGKILL no cliente não aborta e quePOST /session/{id}/abortinterrompe o run e faz o driver sair. - Alternativas rejeitadas: B — permissão pendura sem cliente (verificado); C — sem
--autofora do TUI e sem session ID/URL estável; D — o servidor não aprova permissões por conta própria.
D3 — UX do start (TUI anexado em foreground)
- Opções: A)
starttermina comopencode attach <url> -s <id> --dir <dir>no foreground, stdio herdado (recomendada); B)startretorna sem abrir nada (só imprime URL/ID); C) abrir um TUI local antigo. - Recomendação: A — mantém a UX atual do launch (o chat abre no worktree, agora como cliente do servidor) e o detach explícito é fechar o terminal; sair do TUI não encerra o run.
- Alternativas rejeitadas: B — muda a UX do
worktree next/plan/...e adiciona um passo manual; C — TUI local não fala com o servidor e conflita com o endereçamento.
D4 — Estado por run e contrato de endereçamento (OPS109)
- Opções: A) JSON por run em
~/.local/state/teqo/agent-sessions/<slug-do-branch>.json(TEQO_AGENT_SESSION_DIRoverridável;XDG_STATE_HOMErespeitado), campos estáveisversion, code, issue, purpose, branch, dir, sessionID, url, model, driverPid, logPath, startedAt, stoppedAt(recomendada); B) sem arquivo, redescobrir tudo pela API do servidor; C) banco/daemon próprio de sessões. - Recomendação: A — o opencode persiste a sessão; o arquivo é só o índice de endereçamento (branch/dir/sessionID/url/driverPid/log). Sem segredo persistido: credencial só por env (
OPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAME), nunca no estado/log.codederivado do prefixo do branch (C15-…),nullnos namespacesplans/,work/,fix/. - Alternativas rejeitadas: B — a API do servidor não guarda issue/branch/purpose/driverPid; redescobrir exigiria heurística e quebraria o OPS109; C — rabbit hole de produto ("segundo dono da sessão").
D5 — Reuso de sessão, encerramento e worktree kill
- Opções: A)
startcom estado existente e sessão viva → apenas attach (não duplica driver/sessão);--newforça sessão nova; servidor/sessão inexistente (reboot) → nova sessão;stop= abort via API + kill do driver +stoppedAt;worktree killchama stop antes de remover e apaga o estado ao fim (recomendada); B) sempre criar sessão nova; C) nunca reusar/nunca parar. - Recomendação: A — cobre
worktree next --issue Nidempotente (worktree reaberto não pode ganhar dois runs no mesmo branch) e cumpre "sem sessão órfã". Estado vivo mas com scriptstartconcorrente é protegido por lock simples (ver R4). - Alternativas rejeitadas: B — dois runs no mesmo worktree/branch = corrupção de branch/PR; C — órfãos.
D6 — Dono do mapa purpose→comando/args e nova diretiva de launch
- Opções: A) mover
OPENCODE_SKILL_COMMAND_BY_PURPOSE(hoje emscripts/lib/worktree.mjs:91–96) e a montagem command/args parascripts/lib/agent-session.mjs;opencodeLaunchDirective(scripts/lib/worktree.mjs:207–233) passa a emitirlaunch node scripts/agent-session.mjs start --purpose <P> --dir <D> --model <M> [--issue N] [--argument "<B>"](recomendada); B) manter o mapa emworktree.mjse o CLI de sessão chamá-lo de volta; C) duplicar o mapa no script novo. - Recomendação: A — o launch segue sendo a única porta de entrada;
.agents/shell/worktree.shnão muda de mecânica (continua 1 linhalaunchtokenizada por xargs); a sanitização do bag (remover"/\) e a regra "issue só nonext" são preservadas no novo dono. - Alternativas rejeitadas: B — inverte dependência e acopla o ciclo de vida ao provisionador; C — drift garantido entre dois mapas.
D7 — Superfície de comando
- Opções: A) um script com verbos
serve|start|attach|stop|list [--json], exposto como"agent:session": "node scripts/agent-session.mjs"nopackage.json(recomendada; precedentepnpm issue/pnpm worktree); B) um script por verbo; C) pendurar os verbos noscripts/worktree.mjs. - Recomendação: A — cluster coeso do ciclo de vida;
scripts/*.mjsjá é entrada do knip (knip.jsonentry), então o script novo entra no grafo sem config nova.attach/stopresolvem por--session>--branch>--issue> branch do cwd (git rev-parse --abbrev-ref HEAD);list --jsoné o contrato de endereçamento do OPS109. - Alternativas rejeitadas: B — 5 entrypoints, help/parse duplicados; C — mistura claim/provisionamento (worktree) com ciclo de vida de run (sessão).
D8 — Verificação sem servidor real no CI
- Opções: A) lib pura com
fetch/spawn/fsinjetáveis + unit tests; smoke ao vivo discricionário (recomendada); B) integração/e2e subindoopencode serveno CI; C) só smoke manual. - Recomendação: A — o repo roda unit sem banco/rede de propósito; o que regride é o contrato (argv, paths de estado, validação fail-closed, mapa purpose→comando, shape do
list --json). Atualizartests/unit/worktree.unit.spec.ts(diretiva nova; o spec atual importaOPENCODE_SKILL_COMMAND_BY_PURPOSEda lib antiga — o import passa a vir do novo dono) e criartests/unit/agentSession.unit.spec.tssobtests/{unit,int,e2e}(guardcheck-test-locations). - Alternativas rejeitadas: B — daemon externo/binary local no runner, flake e fora do padrão (o opencode já é smoke, não teste); C — o contrato crítico (endereçamento OPS109, fail-closed de bind) ficaria sem pin.
D9 — Segurança do bind e da credencial
- Opções: A) default loopback sem auth (aceito pelo produto); qualquer
--hostnamenão-loopback exigeOPENCODE_SERVER_PASSWORDou falha fechado antes do spawn; nunca--mdns; credencial só por env (recomendada); B) espelhar o opencode (apenas avisa sem senha); C) bind na tailnet sem auth "porque a rede é confiável". - Recomendação: A —
validateServerBind({ hostname, password })na lib pura, testado para loopback (127.0.0.1,::1,localhost) e para o caminho que lança sem senha; a credencial nunca vai para o estado/log. Repasse ao filho por env (hipótese: o cliente lêOPENCODE_SERVER_PASSWORD; se não ler, fallback-pdocumentado — a validar no tracer). - Alternativas rejeitadas: B — viola o aceite "fail-closed sem credencial"; C — anti-goal explícito da intenção.
D10 — Daemon do servidor na v1
- Opções: A) sem parada automática:
serveidempotente + PID gravado no estado global (server.json) + parada manual documentada (recomendada); B)serve --stopcompleto com restart supervisionado; C) matar porpkill opencode. - Recomendação: A — loopback e idle custam pouco; manter o daemon evita matar o servidor no meio de um run. Registrar em
AGENT-OPS.md;serve --stopfica como gatilho barato se o smoke mostrar necessidade. - Alternativas rejeitadas: B — supervisão de daemon é outro item (fora do appetite); C — padrão perigoso: mataria os servidores de terceiros (4096/4097).
Componentes / mudanças
scripts/lib/agent-session.mjs(novo, lib pura, testável): dono dos símbolos —AGENT_SERVER_PORT(4199,TEQO_AGENT_SERVER_PORT),AGENT_SERVER_HOST(127.0.0.1),AGENT_SESSION_DIR(TEQO_AGENT_SESSION_DIR??$XDG_STATE_HOME/teqo/agent-sessions??~/.local/state/teqo/agent-sessions),STATE_VERSION,isLoopbackHost,validateServerBind,sessionSlug(reusaslugifydesrc/lib/slug.ts, já importado pela lib de worktree),sessionStatePath,serializeSessionState/parseSessionState,purposeInvocation({ purpose, issueNumber, argument })(mapa movido:next → { command: 'work-issue', arguments: '--issue N' },plan → { command: 'plan-issue', arguments: '' },fix → { command: 'bug-fix', arguments: bag },new → null; sanitização do bag idempotente também aqui),driverArgs/attachArgs/serverArgs(SEM o binário — o caller usaspawn('opencode', …); nenhum carrega-p, a credencial só vive no env),resolveSessionRef({ states, session, branch, issue, cwdBranch }),formatSessionList(states, probes)/sessionListPayload.scripts/agent-session.mjs(novo, CLI fina): verbosserve(health check → spawnopencode serve --hostname <H> --port <P>detached + log; PID emserver.json),start(reuso D5 → cria sessãoPOST /session?directory=D→ spawn do driver detached com log → attach foreground),attach(opencode attach <url> -s <id> [--dir <dir>]),stop(resolve ref →POST /session/{id}/abort→ SIGTERM/SIGKILL nodriverPid→ gravastoppedAt),list [--json](varre o state dir; probeGET /global/health+GET /session/status?directory=D; statusworking|idle|stopped|unknown). Aviso de drift: compararversiondo/global/healthcomopencode --versionse não for igual.worktree killdelega o encerramento ao próprio CLI (node scripts/agent-session.mjs stop --branch=<B>, best-effort; estado só é descartado quando o stop funciona) — sem segunda implementação de abort/kill.scripts/lib/worktree.mjs:opencodeLaunchDirectiveemite a linha nova (uma linhalaunch node scripts/agent-session.mjs start ..., dir sem espaços); removerOPENCODE_SKILL_COMMAND_BY_PURPOSE(dono passa a ser a lib nova); comentário/doc da função reescrito.resolveWorktreeModel/WORKTREE_MODEL_MAP/diretiva headless OPS106 intocados.scripts/worktree.mjs: caminho dokillchamastopSession(lib nova, best-effort: servidor inacessível → segue o kill do PID/estado) antes dogit worktree remove; ao fim da remoção, apaga o estado do branch. Help text e comentários realinhados.next/plan/new/fixinalterados fora da diretiva..agents/shell/worktree.sh: só comentários (l.9–26 e l.97–111) — mecânica intocada (1 linhalaunch, xargs,cdprimeiro). Hipótese a validar no smoke:launchpreserva o cwd (o caminhoscripts/agent-session.mjsé relativo); se não preservar, o fallback é oworktree.mjsemitir o caminho absoluto (node <repoRoot>/scripts/agent-session.mjs) — 1 linha.package.json:"agent:session": "node scripts/agent-session.mjs".tests/unit/worktree.unit.spec.ts: expectativas da diretiva nova; import do mapa vem da lib nova (ou o pin do mapa muda de casa). Demais pins (modelo, branches, env) intactos.tests/unit/agentSession.unit.spec.ts(novo): paths/override do state dir,sessionSlugde branch com/,validateServerBindfail-closed (não-loopback sem senha lança; loopback nunca lança;--mdnsnunca emitido),driverArgvcom--attach/-s/--auto/--command+ separador--e args (--issue 1019só nonext; bag só nofix, sanitizado),attachArgv,purposeInvocation,parseSessionState/roundtrip,formatSessionList/payload dolist --json.- Docs: seção nova em
docs/AGENT-OPS.md(ciclo de vida, comandos, detach, stop, estado, contrato OPS109, loopback/credencial, modos de falha: morte do driver → permissão pendura; reboot → histórico persiste, run não; Ctrl+C limpa input, sair = fechar terminal/keybindapp_exit); bullet do worktree emAGENTS-infra.md; changelogdocs/changelog/2026-09-15-ops110.md(não editardocs/CHANGELOG-AGENTS.mdnem oHISTORY). - Sem migration, sem UI, sem access/Consent/PII, sem e2e (diff é scripts + specs + docs; smoke ao vivo discricionário).
Dados → forma (N/A — sem dados)
Ferramenta/ops: os "dados" são o estado operacional do run (sessionID, status) exposto em list --json para o OPS109 — texto/JSON de máquina, sem métrica, gráfico ou dado eleitoral/contato. Nada a desenhar.
Fases verificáveis
- Tracer — lib pura + contrato da diretiva (RED → verde). Escrever primeiro
tests/unit/agentSession.unit.spec.ts(caminhos, fail-closed de bind, argv, mapa purpose→comando, estado, list) — RED contra o repo atual (módulo não existe; diretiva ainda não é a nova). Extrairscripts/lib/agent-session.mjs, mover o mapa e ajustaropencodeLaunchDirective; atualizartests/unit/worktree.unit.spec.ts. Gate: specs focados verdes +pnpm gate:fast. ~3–4h. - CLI
agent-session.mjs— serve/start/attach/stop/list. Fiar health check, spawn destacado do servidor e do driver (log em fd,unref), criação de sessão via API, attach foreground, stop/abort,list --json. Verificação: unit tests da lib via funções puras com fetch/spawn injetáveis (sem rede/no CI) +node scripts/agent-session.mjs list --jsoncom state dir vazio/inexistente retornando payload válido (fail-soft, sem stacktrace). Gate:pnpm gate:fast+pnpm knip. ~5–6h. - Integração no launcher + kill.
package.json, hook dokillnoscripts/worktree.mjs, comentários da shell. Verificação: specs de diretiva verdes;TEQO_WORKTREE_TERMINAL=1 node scripts/worktree.mjs plan --staynão emite launch (regra atual preservada) e o spec pina a linha emitida;worktree killcom estado de sessão chama stop antes do remove (coberto no smoke). Gate:pnpm gate:fast. ~2–3h. - Docs + changelog. Checklist da seção Componentes (
AGENT-OPS.md,AGENTS-infra.md,.agents/shell/worktree.sh,docs/changelog/2026-09-15-ops110.md;pnpm formatse o Prettier reclamar). Gate:pnpm push(ogate:cicobre docs-guards). ~1–2h. - Smoke ao vivo (discricionário) + entrega. Em dir/worktree de scratch (nunca um claim real):
agent-session startcom comando de scratch, fechar o cliente/SIGKILL no TUI, confirmar run vivo vialist//session/status, reattach,stopinterrompe o run e o driver sai;worktree killsem sessão órfã (PID/driver mortos, estado removido). Registrar o resultado no changelog;pnpm push; PR Ready. ~2h + observação pós-merge. Total ~13–17h (~2 dias eng).
Rabbit holes / Não escopo (engenharia)
- Painel de frota / web UI / pool — status de todos os runs, iniciar/parar por lista, notificações, métricas, supervisor (anti-goals; OPS109 consome só o
list --json). - Expor bind na tailnet sem auth ou "porque a rede é confiável" — qualquer superfície além do loopback exige credencial, fail-closed;
--mdnsnunca. - Persistência própria de sessão (banco/daemon guardando estado do run) — o opencode já persiste; aqui só o índice de endereçamento.
- Lock manager/scheduler completo — a corrida de
startresolve com lockfile simples por slug (R4), não vira fila de runs. - Gerenciar/derrubar os servidores de terceiros (4096/4097) ou "reusar um deles".
- Rotação de log, restart supervisionado do daemon, telemetria — v1 aceita o log append + daemon idle (R2/R10).
- Integração/e2e com servidor real no CI e migration/UI/access/Consent — sem superfície; smoke discricionário.
- Mudar claim/worktree/gates, criar segunda porta de entrada ou sessão sem branch/claim.
Riscos e mitigação
- R1 — Morte do driver → permissão pendurada (o run fica esperando um cliente que não existe). Mitigação: o attach pode responder a permissão;
stopaborta via API em modo tolerante (401/5xx não impedem o encerramento local do driver +stoppedAt); documentar emAGENT-OPS.md(sintoma + remédio). - R2 — Daemon do servidor sem parada na v1. Aceito (loopback, idle); PID no
server.jsonpara parada manual;serve --stopdeferido como gatilho barato (D10). - R3 — Drift de flags/API do opencode (servidor/attach/run). Mitigação:
/global/healthtrazversione ostartavisa se ≠opencode --version; unit tests pinam o argv; smoke ao vivo; os fatos deste plano são da 1.18.31. - R4 — Dois
startsimultâneos no mesmo branch/servidor (driver/sessão duplicados). Mitigação: lockfile por slug (fs.openSync(..., 'wx')com PID e reclaim se o PID morreu; sem dependência nova) com a decisão de reuso relida dentro do lock; segundostartsai com mensagem clara; caminho coberto pela lib (a corrida real não tem harness no repo — lacuna registrada no PR). - R5 — Ctrl+C no TUI anexado limpa input em vez de sair (probe; não aborta o run). Mitigação: documentar as chaves de saída (
app_exit: ctrl+c, ctrl+d, <leader>q; fechar terminal/SIGHUP = detach) e que encerrar éagent-session stop. - R6 — Bind não-loopback sem senha (violaria o aceite). Mitigação:
validateServerBindfalha antes do health/reuse (não só antes do spawn);isLoopbackHostestrito (não aceita127.0.0.1.evil); unit tests dedicados;--mdnsnunca emitido. - R7 — Hypotese do repasse de credencial por env (
opencode attach/runleremOPENCODE_SERVER_PASSWORDsem-p). Mitigação: fallback-pdocumentado (trade-off: visível empslocal) e validado no tracer; a credencial nunca entra no estado/log. - R8 — Worktrees reabertos/criados antes do merge não têm
scripts/agent-session.mjs. Resolvido em execução: a diretiva carrega o caminho absoluto do checkout que a emitiu (opencodeLaunchDirectiveganhousessionScript; o default relativo fica para os specs) — o relaunch de worktree antigo não quebra nem fica pinado no código do branch. - R9 — Contrato
list --json(OPS109) evoluir quebrando o painel. Mitigação:versionno estado e no payload; evolução additions-only; contrato documentado emAGENT-OPS.md. - R10 — Log do driver sem rotação. Aceito na v1 (arquivo append no state dir, caminho no estado); se crescer, truncar é trivial e não muda o contrato.
- R11 —
newsem run dirigido (sem driver enquanto o humano não conversar; permissões interativas dependem do cliente anexado). Mitigação:start --purpose newcria sessão + attach sem driver (hipótese:runsem mensagem não é confiável — validar no tracer); documentar a limitação; o aceite de detach vale para runs dirigidos (next/plan/fix).
Aceite de engenharia
- Aceite de produto da intenção coberto: (1) run continua sem nenhum cliente olhando; (2) reattach exato de outro dispositivo (mesma sessão
-s, mesmo--dir, mesmo histórico); (3) sair do cliente ≠ encerrar (stop é ato explícito); (4) nada além do loopback sem credencial —--hostnamenão-loopback semOPENCODE_SERVER_PASSWORDfalha fechado; (5) worktree/branch segue o contrato, sem nova forma de claim nem sessão órfã (worktree killpara a sessão antes de remover); (6) sessão endereçável por Issue/branch/worktree via estado +list --json(contrato OPS109). - Invariantes AGENTS/engineering-standards: sem migration/UI/access/Consent/PII; claim/gates intocados; a diretiva de launch continua a única porta (
opencodeLaunchDirective); shell function sem mudança de mecânica; sem segredo persistido/commitado;scripts/*.mjscomo entrada do knip e specs emtests/unit/(guardcheck-test-locations); sem dependência nova. - Testes previstos:
tests/unit/agentSession.unit.spec.tsnovo (paths/overrides, fail-closed de bind, argv do driver/attach com separador--, mapa purpose→comando+args, parse/serialize,list --json) com RED comprovado contra o repo atual;tests/unit/worktree.unit.spec.tsatualizado (diretiva nova) e demais pins verdes. - Gates:
pnpm gate:fastna iteração epnpm test:unit(full);pnpm push(ogate:cicobre format/lint/typecheck/unit/int/knip/cycles/docs-guards); e2e: declarar "sem superfície de runtime" (diff é scripts + specs + docs; OPS72 discricionário — o CI do PR aplica o conjunto curado por high-risk, poispackage.json/scripts/worktree.mjsestão no blast radius). Smoke ao vivo com dir/worktree de scratch (sem claim real): detach com run vivo, reattach,stop,worktree kill— resultado registrado no changelog; incluir o*-impl.mdno commit; PR Ready.
Self-score decision-quality (gate ≥4):
| Critério | Nota | Justificativa |
|---|---|---|
| 1. Decisões caras com rejeitadas | 1,0 | D1–D10 com opções e rejeitadas explícitas (topologia, driver vs tmux vs HTTP, estado, reuso/kill, dono do mapa, superfície, verificação, bind, daemon) |
| 2. Cabe no appetite | 1,0 | ~13–17h: 1 CLI + 1 lib pura + 2 specs unitários + hook de kill + docs; sem migration/UI/e2e |
| 3. Rabbit holes nomeados | 1,0 | frota/web UI/pool, bind sem auth/--mdns, persistência própria, lock manager, servidores de terceiros, rotação/supervisão, e2e com servidor real, mudar claim/gates |
| 4. Depth check (reuso) | 1,0 | reusa slugify, o spawn destacado do auto-unblock.mjs:344–374, a mecânica da shell (1 linha launch/xargs), o padrão de cluster pnpm issue/worktree, estado em XDG; edita os donos (diretiva/lib/worktree) sem twin |
| 5. Intenção preservada | 1,0 | os 6 aceites cobertos; direção (a) escolhida; anti-goals respeitados; endereçamento OPS109 explícito no list --json; produto decide "de onde acompanha" (SSH/TUI, já validado) |
| Total | 5,0/5 | ≥4 exigido |