Merge pull request #583 from fsolla/OPS26-worktree-go-abre-o-opencode-com-pre-selecoes-deepseek-

OPS26: worktree next/plan no terminal abrem o opencode com presets (DeepSeek V4 Flash, auto-approve, skill inicial)
This commit is contained in:
Francisco Solla 2026-08-10 11:11:56 -03:00 committed by GitHub
commit 4551a4e997
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 237 additions and 12 deletions

View file

@ -6,7 +6,16 @@
# na última linha, porque um processo filho (node) não consegue mudar o cwd do
# shell que o chamou.
#
# Instalação (uma linha no profile):
# No terminal (esta função), o script também imprime a diretiva `launch
# opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt …]` (OPS26):
# a função executa o cd e então a linha, e o TUI do opencode abre no worktree —
# `next` com `/work-issue` já enviado, `plan` sem prompt (autocomplete completa
# `/plan-`). Sem `exec` de propósito: ao sair do opencode, o terminal volta ao
# shell dentro do worktree. Presets são constantes em scripts/lib/worktree.mjs;
# o marcador TEQO_WORKTREE_TERMINAL=1 é o que separa esta superfície da do comando
# `/worktree` do opencode (que nunca lança TUI). `--stay` suprime cd e launch.
#
# Instalação (uma linha no profile; requer bash ou zsh — usa BASH_SOURCE, arrays e here-strings):
# source <repo>/.agents/shell/worktree.sh
#
# Uso (terminal interativo):
@ -37,7 +46,7 @@ worktree() {
done
local out
out="$(node "$root/scripts/worktree.mjs" "$@")" || return $?
out="$(TEQO_WORKTREE_TERMINAL=1 node "$root/scripts/worktree.mjs" "$@")" || return $?
printf '%s\n' "$out"
if [ "$stay" -eq 1 ]; then
@ -53,4 +62,16 @@ worktree() {
printf 'worktree: não consegui aplicar o `cd` da saída do script.\n' >&2
return 1
fi
# Diretiva `launch` (só existe quando TEQO_WORKTREE_TERMINAL=1 e sem --stay): o
# script a gera a partir de constantes + dir slugificado (sem espaços), então o
# split por IFS=' ' abaixo é intencional e seguro — nunca eval. Falha do launch
# (ex.: opencode fora do PATH) só avisa: o worktree já está pronto e utilizável.
local launch
launch="$(printf '%s\n' "$out" | sed -n 's/^launch //p' | tail -n 1)"
if [ -n "$launch" ]; then
local -a launch_args
IFS=' ' read -r -a launch_args <<< "$launch"
"${launch_args[@]}" || printf 'worktree: launch do opencode falhou (código %s) — o worktree segue pronto em %s.\n' "$?" "$PWD" >&2
fi
}

View file

@ -30,6 +30,8 @@ No opencode, isso é o comando **`/worktree next`** / **`/worktree kill`** (`.op
**Default-go:** `next` e `plan` imprimem `cd <dir>` na última linha **por padrão**; `--stay` suprime; `--go` explícito continua aceito como no-op. No terminal interativo, use a função `worktree()` de **`.agents/shell/worktree.sh`** (uma linha de `source` no profile) para o `cd` ser aplicado de verdade no shell que te chamou — node não consegue mudar o cwd do shell pai. `kill` também imprime `cd <main>` no fim (a sessão nunca fica num diretório destruído).
**Launch do opencode (OPS26):** só a função shell marca `TEQO_WORKTREE_TERMINAL=1`; com o marcador, `next`/`plan` imprimem também a diretiva `launch opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt /work-issue]` **antes** do `cd`, e a função a executa depois do cd — `next` abre o TUI com `/work-issue` já enviado; `plan` abre sem prompt (`/plan-` + autocomplete do TUI). Sem `exec`: sair do opencode volta ao shell no worktree. Presets são constantes em `scripts/lib/worktree.mjs`. **O comando `/worktree` do opencode (e qualquer automação sem o marcador) nunca lança o TUI.**
## Fluxo quando invocado como skill (agentes que não têm opencode)
1. Rode `pnpm worktree next` e leia a saída inteira. Se a fila estiver vazia, ele para sozinho — não crie worktree sem Issue.

View file

@ -8,4 +8,6 @@ Rode o script determinístico abaixo (não improvise a lógica) e reporte o resu
- `plan [bag] [--stay]`: cria um worktree de planejamento **diferente a cada invocação** (branch `plans/plan-issue-<bag>`, ou `plans/plan-issue-<n>` sequencial sem bag; sufixo `-2`/`-3` em colisão) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`. Mesmo contrato de `cd` do `next`: aplique a última linha `cd <dir>` por padrão; `--stay` suprime.
- `kill [--force]`: destrói o worktree em que você está e remove seus bancos. O script imprime `cd <main>` na última linha — aplique-o (`cd <main>`) para a sessão sair do diretório destruído.
> **Este comando roda SEM o marcador `TEQO_WORKTREE_TERMINAL`, então nunca recebe a diretiva `launch`** — só o `cd`. O launch do opencode (OPS26) acontece apenas na função `worktree()` de `.agents/shell/worktree.sh` (terminal interativo); nunca abra um TUI aninhado a partir daqui.
!`node scripts/worktree.mjs $ARGUMENTS`

View file

@ -34,7 +34,7 @@ These are product/architecture constraints — not a ban on editing existing cod
- `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit.
- To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data).
- `pnpm db:doctor` diagnoses local database connectivity for the dev AND test targets: it names who holds the port, points at exited/crash-looping containers, and flags Postgres containers sharing one data volume (two postmasters on one PGDATA corrupt the WAL — this destroyed the local cluster's checkpoint on 2026-07-25; the canonical setup is ONE compose container on 5432 hosting `teqo` + `teqo_test`). `pnpm dev` runs the same preflight via `guard-dev-db` and refuses to start with a named remedy instead of a later ECONNREFUSED. `db:start`/`db:stop` pin the compose project with `-p teqo`, so they always target that ONE container even from a worktree directory.
- **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests) created on the shared container with the full migration chain applied; writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. **`next`/`plan` print `cd <dir>` as the last line BY DEFAULT** — the caller applies it (shell function `worktree()` in `.agents/shell/worktree.sh`, or the `/worktree` opencode command); `--stay` suppresses the cd, `--go` explicit stays accepted as a no-op (it was the old default), and `kill` ends with `cd <main>` so the shell never sits in a destroyed directory. `worktree plan [bag] [--stay]` (2026-08-08) is the sibling for the `/plan-issue` skill: **each invocation creates a DIFFERENT planning worktree** so parallel `/plan-issue` sessions never share one — branch `plans/plan-issue-<bag>` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-<n>` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `<Code>-<slug>`, so a later `next` never collides in branch or slot. `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo_<worktree>_test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning.
- **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests) created on the shared container with the full migration chain applied; writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. **`next`/`plan` print `cd <dir>` as the last line BY DEFAULT** — the caller applies it (shell function `worktree()` in `.agents/shell/worktree.sh`, or the `/worktree` opencode command); `--stay` suppresses the cd, `--go` explicit stays accepted as a no-op (it was the old default), and `kill` ends with `cd <main>` so the shell never sits in a destroyed directory. **In the interactive terminal the shell function sets `TEQO_WORKTREE_TERMINAL=1` and `next`/`plan` additionally print a `launch opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt /work-issue]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10): `next` opens the opencode TUI in the worktree with `/work-issue` already sent (the OPS25 command runs the whole cycle), `plan` opens with the same presets and NO prompt (`/plan-` + TUI autocomplete — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; gap registered for the opencode repo). The launch is without `exec` (exiting the TUI returns to the shell in the worktree, no auto-`kill`), `--stay` suppresses both the cd and the launch, the presets are constants in `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, unit-tested), and **the `/worktree` opencode command never launches a TUI** (no marker — no nested TUI). `worktree plan [bag] [--stay]` (2026-08-08) is the sibling for the `/plan-issue` skill: **each invocation creates a DIFFERENT planning worktree\*\* so parallel `/plan-issue` sessions never share one — branch `plans/plan-issue-<bag>` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-<n>` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `<Code>-<slug>`, so a later `next` never collides in branch or slot. `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo_<worktree>_test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning.
- To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below.
- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data (TSE 2022)" below.

View file

@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
---
**Recently resolved (2026-08-10):** **OPS26** (Issue #571) — `worktree next`/`plan` **no terminal** terminam com o TUI do opencode aberto no worktree: a função `worktree()` de `.agents/shell/worktree.sh` marca `TEQO_WORKTREE_TERMINAL=1` e o script imprime a diretiva `launch opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt /work-issue]` **antes** do `cd` (que segue sendo a última linha — contrato OPS24 intacto); a função aplica o cd e executa a linha (sem `exec`: sair do TUI volta ao shell no worktree; sem auto-`kill`). `next` envia `/work-issue` (o comando OPS25 roda o ciclo completo); `plan` abre **sem prompt** — a CLI do opencode não tem pre-fill sem submit (`--prompt` sempre auto-envia; fallback acordado: `/plan-` + autocomplete; gap registrado para o repo do opencode). Presets são constantes em `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, `opencodeLaunchDirective` — pura, unit-testada; trocar de modelo é editar a constante). **Sem o marcador (comando `/worktree` do opencode, automação) a diretiva não é impressa** — nunca abre TUI aninhado; `--stay` suprime cd e launch; `kill` intocado. Docs: AGENTS.md ("Per-worktree environments"), header/uso do script, `.opencode/commands/worktree.md`, skill `worktree-next-issue`. Sem migration/dados/access/UI. Intenção [`worktree-go-abre-opencode-com-presets.md`](plans/worktree-go-abre-opencode-com-presets.md); impl [`worktree-go-abre-opencode-com-presets-impl.md`](plans/worktree-go-abre-opencode-com-presets-impl.md).
**Recently resolved (2026-08-10):** **OPS24** (Issue #569) — `--go` virou o **padrão** do worktree CLI e `kill` volta ao main: `worktree next`/`plan` imprimem `cd <dir>` na última linha **por padrão** (antes exigiam `--go`); `--stay` suprime; `--go` explícito continua aceito como no-op (não quebra hábito/automação). `kill` troca a mensagem "Volte ao main: cd …" (que nunca casava com o sed `s/^cd //p` da função shell) pela **linha máquina `cd <main>` como última linha** — depois do drop dos DBs, que rodava antes — e **não aceita `--stay`** (ficar em cwd destruído é exatamente o bug corrigido; `--go` no kill é no-op). Vale nas duas superfícies que aplicam o cd: função `worktree()` de `.agents/shell/worktree.sh` (agora sempre captura/applica, `--stay` retorna cedo) e comando `/worktree` do opencode (agente aplica o `cd` da última linha sempre, exceto `--stay`). Docs: header/uso do script, AGENTS.md ("Per-worktree environments"), skills `worktree-next-issue`/`local-database`. Sem migration/dados/access; derivação pura (`worktree-env.mjs`/`worktree.mjs` libs) intocada. Intenção [`worktree-go-default-e-kill-volta-ao-main.md`](plans/worktree-go-default-e-kill-volta-ao-main.md); impl [`worktree-go-default-e-kill-volta-ao-main-impl.md`](plans/worktree-go-default-e-kill-volta-ao-main-impl.md).
**Recently resolved (2026-08-10):** **OPS25** (Issue #570) — atalhos diretos no TUI do opencode: `.opencode/commands/work-issue.md` e `plan-issue.md` (mecanismo nativo de commands, mesmo padrão de `issue.md`/`worktree.md`). O frontmatter leva `description` pt-BR (autocomplete do TUI) + `model: deepseek/deepseek-v4-flash` (pin do modelo de preferência, alinhado ao launch da OPS26 com `--model`); o corpo instrui carregar a skill de **mesmo nome** pela ferramenta de skills e segui-la ponta a ponta, apontando a fonte canônica `.agents/skills/<nome>/SKILL.md` — **sem transcrever** (anti-goal da intenção) — com `$ARGUMENTS` repassado no fim. **Guard de acoplamento** `tests/unit/opencodeCommands.unit.spec.ts`: comando↔skill presos por nome exato (renome de skill → build falha em vez de comando quebrado silencioso). Sem migration/access/Consent/UI. Intenção [`opencode-comandos-work-issue-e-plan-issue.md`](plans/opencode-comandos-work-issue-e-plan-issue.md); impl [`opencode-comandos-work-issue-e-plan-issue-impl.md`](plans/opencode-comandos-work-issue-e-plan-issue-impl.md).

View file

@ -27,7 +27,7 @@ Toda sessão de trabalho no repo começa invocando `/work-issue` (executar uma I
- O template instrui o agente a carregar a skill pelo nome exato e seguir o fluxo dela ponta a ponta, sem reescrever o conteúdo.
- `$ARGUMENTS` é repassado (ex.: `/plan-issue <lote>`).
- Cada comando leva `model: deepseek/deepseek-v4-flash` no frontmatter — sessão manual já abre no modelo de preferência (ver questão em aberto).
- Abertura automática a partir de `worktree next/plan` (OPS26) usa estes comandos como primeira mensagem.
- Abertura automática a partir de `worktree next/plan` (OPS26) usa estes comandos como primeira mensagem — `next` envia `/work-issue`; `plan` abre sem prompt (fallback acordado na OPS26: a CLI do opencode não tem pre-fill sem submit).
## Dados (intenção)

View file

@ -0,0 +1,79 @@
# Impl: Worktree `go` abre o opencode com pré-seleções (DeepSeek V4 Flash, auto-approve, skill inicial)
Status: aprovado
Atualizado em: 2026-08-10
Issue: #571
Intenção: docs/plans/worktree-go-abre-opencode-com-presets.md
Appetite restante: ~0,5–1 dia eng (herdado; cabe folgado)
## Leitura da intenção
- **Outcome:** `worktree next` no terminal termina com o TUI do opencode aberto no worktree (modelo `deepseek/deepseek-v4-flash`, `--auto`, `/work-issue` **enviado**); `worktree plan` abre com os mesmos presets e **sem** prompt (fallback acordado: `/plan-` + autocomplete; pre-fill sem submit quando o CLI do opencode ganhar a flag); sem marcador de terminal → **nenhum** launch (só `cd`); `--stay` suprime cd e launch; presets são constantes no script.
- **O que NÃO negociar:** sem TUI aninhado (`/worktree` do opencode não lança); sem alterar provisionamento; sem presets por-worktree via config commitada; launch sem `exec` (sair do TUI volta ao shell no worktree); sem auto-`kill` ao sair.
- **O que reavaliar:** a hipótese de "env/flag que só a função shell passa" — validada como **marcador de env** (aceite #3 exige que o script distinga a superfície); a localização das constantes (script vs função shell) — aceite #6 exige script.
## Abordagem recomendada
```mermaid
flowchart LR
S[worktree.sh função worktree] -->|TEQO_WORKTREE_TERMINAL=1 + args| N[node scripts/worktree.mjs]
N -->|print| CD["launch opencode <dir> --model … --auto [--prompt /work-issue]<br/>cd <dir>"]
S -->|sed 's/^launch //p'| L[executa launch sem exec<br/>foreground, TUI assume o terminal]
S -->|sed 's/^cd //p'| D[cd <dir> primeiro]
C[.opencode/commands/worktree.md] -->|sem marcador| N2[node scripts/worktree.mjs]
N2 -->|print| CD2["cd <dir> (sem launch — superfície nunca lança TUI)"]
C -->|aplica| D2[cd <dir>]
```
**Opções consideradas:** A (marcador de env `TEQO_WORKTREE_TERMINAL=1` — linha `launch` só quando marcado) | B (script sempre imprime `launch`; cada chamador decide o que executar) | C (flag `--launch` no argv)
**Recomendação:** A — o aceite #3 exige que o launch **não aconteça** quando o script roda sem marcador (o `/worktree` do opencode, automação): o marcador é o que separa as duas superfícies; a superfície opencode nunca recebe a linha, então seu texto não precisa ensinar a ignorá-la.
**Rejeitadas:** B — inverte o aceite (a linha sairia para o `/worktree` também; o comando precisaria aprender a descartá-la, e automação futura ingênua executaria TUI aninhado); C — o shell repassa `"$@"` intacto; uma flag extra exigiria strip antes do repasse e poderia vazar para a superfície opencode.
### Componentes / mudanças
- **`opencodeLaunchDirective`** (`scripts/lib/worktree.mjs`): função pura `({ dir, purpose, terminal }) → string | null` — devolve `null` sem marcador; linha `launch opencode <dir> --model <model> --auto` + `--prompt /work-issue` só para `purpose: 'next'` (plan **sem** prompt — fallback acordado). Constantes exportadas no lib (fonte única, unit-testável): `OPENCODE_PRESET_MODEL = 'deepseek/deepseek-v4-flash'`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE = { next: '/work-issue', plan: null }`, `WORKTREE_TERMINAL_ENV = 'TEQO_WORKTREE_TERMINAL'`. Invariante documentada: dir é sempre `<root sem espaço>/<branch slugificado>` — sem espaços, word-splitting seguro.
- **`scripts/worktree.mjs`**: lê `process.env[WORKTREE_TERMINAL_ENV] === '1'`; em `cmdNext`/`cmdPlan`, no bloco `!stay`, imprime a linha `launch …` **antes** do `cd <dir>` (a linha `cd` continua sendo a **última** — contrato OPS24 preservado para todos os consumidores). Header do script + bloco de uso ganham uma linha sobre o launch.
- **`.agents/shell/worktree.sh`**: chama `TEQO_WORKTREE_TERMINAL=1 node …`; depois do cd aplicado, extrai a última linha `launch …` (mesmo padrão sed do cd) e executa word-split (`$launch`, sem `eval` — formato fixo gerado pelo script, sem globs) **sem `exec`** (aceite #5: ao sair do TUI, volta ao shell no worktree); falha do launch (ex.: `opencode` ausente) imprime aviso e o worktree segue utilizável.
- **Migration:** sem migration (nenhuma mudança de schema/dados).
- **Access / Consent:** N/A (CLI de dev).
- **UI:** Impeccable A — N/A (sem UI de produto; TUI de terceiros).
### Ordem das linhas na saída
```text
… (bloco "Ambiente isolado") …
launch opencode /home/<user>/.cursor/worktrees/teqo/OPS26-x --model deepseek/deepseek-v4-flash --auto --prompt /work-issue
cd /home/<user>/.cursor/worktrees/teqo/OPS26-x
```
`cd` continua última linha (contrato documentado em AGENTS.md/skill/comando intacto); a função shell aplica cd primeiro e só então executa o launch (foreground).
## Fases verificáveis
1. **Derivação pura** — lib (`opencodeLaunchDirective` + constantes) + unit tests novos em `tests/unit/worktree.unit.spec.ts` (next → prompt presente; plan → sem prompt; terminal=false → null; constantes pinadas). Quota: pequena.
2. **Script + shell** — `cmdNext`/`cmdPlan` imprimem a diretiva; função shell aplica marcador, cd e launch; textos (header/uso do script, header do .sh). Verificação manual: `TEQO_WORKTREE_TERMINAL=1 node scripts/worktree.mjs …` (output shape) e simulação do parse sed com saída sintética (sem criar worktree/DBs reais).
3. **Docs** — AGENTS.md (parágrafo Per-worktree), `.opencode/commands/worktree.md` (nota: comando nunca lança TUI), `.agents/skills/worktree-next-issue/SKILL.md` (linha default-go), `docs/CHANGELOG-AGENTS.md` (entrada única, padrão das entregas).
4. **Gates** — `pnpm gate:fast` na iteração; `pnpm push` na entrega (gate:ci + push).
## Rabbit holes / Não escopo (engenharia)
- `opencode.json` por worktree / config global `~/.config/opencode` (presets são flags por invocação — rabbit hole cortado na intenção).
- Flag de pre-fill sem submit no CLI do opencode (gap registrado em `~/Code/propositions/opencode/prefill-prompt-sem-submit.md`; se ganhar a flag, `plan` passa a preencher — gatilho documentado na intenção).
- Modelo dinâmico por Issue (`model:` da Issue claimada vs constante) — constante fixa, fora de escopo da intenção.
- `worktree new` (OPS27, herda launch sem skill inicial) — item próprio.
- Execução via `eval` na função shell — desnecessária; word-splitting de formato fixo basta.
- Testes da função shell (bats etc.) — repo não tem suíte de shell; verificação via simulação do parse.
## Riscos e mitigação
- **TUI aninhado por engano** (marcador esquecido em automação futura): o default é **sem** launch — falha segura no sentido bom (nenhum TUI surpresa). Aceite #3 cobre.
- **Launch falho** (`opencode` fora do PATH, TUI com erro): cd já aplicado; aviso + worktree utilizável — nada destrutivo.
- **Word-splitting da linha launch** quebra se dir tiver espaços: impossível hoje (root fixo sem espaço + branch slugificado); invariante documentada no lib e na função shell.
- **Contrato "cd última linha"** quebrado por engano: diretiva impressa **antes** do cd; unit test do lib não cobre a ordem — coberto pela verificação manual de fase 2 e pelo texto do header.
- **Doc drift** (AGENTS.md/skill dizendo "cd última linha por padrão"): atualizações na fase 3 no MESMO PR (padrão do repo: docs e código juntos).
## Aceite de engenharia
- [x] Aceite de produto da intenção ainda coberto (6 critérios: next envia `/work-issue`; plan sem prompt; sem marcador → sem launch; `--stay` suprime; sem `exec` + sem auto-kill; constantes no script)
- [x] Invariantes AGENTS/engineering-standards (nenhuma mudança em src/, migrations, access, Consent, transações)
- [x] Testes de domínio previstos (unit da derivação pura; sem int/e2e — nenhum caminho de escrita/access muda)

View file

@ -30,7 +30,7 @@ flowchart LR
**Rejeitadas:**
- **B — inverter só na função shell** (shell decide aplicar por default e passa `--stay` ao script): deixaria o opencode command e consumidores diretos do script com a semântica velha; a superfície `/worktree` também precisa do default-go. Rejeitada por inconsistência.
- **C — detectar "terminal vs opencode" no script** (ex.: env `WORKTREE_TERMINAL`): anti-goal explícito da intenção; o contrato continua sendo "script imprime `cd`; quem aplica decide". Rejeitada.
- **C — detectar "terminal vs opencode" no script** (ex.: env `WORKTREE_TERMINAL`): anti-goal explícito da intenção; o contrato continua sendo "script imprime `cd`; quem aplica decide". Rejeitada. _(Reavaliado na OPS26: o marcador `TEQO_WORKTREE_TERMINAL` foi adotado lá para a diretiva `launch` — só a função shell marca, e a superfície `/worktree` nunca recebe a linha.)_
- **D — `kill --stay`** (ficar no cwd morto): anti-goal de produto (é exatamente o bug que se corrige). Rejeitada — `kill` com `--stay` morre com mensagem clara.
### Componentes / mudanças

View file

@ -16,6 +16,22 @@ import { slugify } from '../../src/lib/slug.ts'
*/
export const PLAN_BRANCH_PREFIX = 'plans/plan-issue'
/** Env var the interactive-terminal `worktree()` function sets to request the opencode launch. */
export const WORKTREE_TERMINAL_ENV = 'TEQO_WORKTREE_TERMINAL'
/** Preset model for the opencode launch — change the preset by editing this constant. */
export const OPENCODE_PRESET_MODEL = 'deepseek/deepseek-v4-flash'
/**
* Skill command sent as the launch's initial message per purpose. `next`
* sends `/work-issue` (the OPS25 command executes the full cycle); `plan`
* sends nothing — the opencode CLI's `--prompt` always auto-submits, there is
* no prefill-without-submit flag, and the TUI autocomplete completes `/plan-`
* instead (gap registered for the opencode repo). Switch `plan` to
* `/plan-issue` here if the CLI ever gains prefill-without-submit.
*/
export const OPENCODE_SKILL_COMMAND_BY_PURPOSE = { next: '/work-issue', plan: null }
/** Total branch-name budget for plan branches — mirrors `branchNameForIssue`. */
const PLAN_BRANCH_MAX_LENGTH = 60
@ -89,3 +105,21 @@ export const planBranchName = ({ bag = '', taken = new Set() }) => {
if (!taken.has(candidate)) return candidate
}
}
/**
* Launch directive for the opencode TUI, printed by `worktree next`/`plan`
* right before the `cd <dir>` line when called from the interactive terminal
* (`WORKTREE_TERMINAL=1`): `launch opencode <dir> --model <preset> --auto
* [--prompt <command>]`. The shell function (`.agents/shell/worktree.sh`)
* applies the `cd` first, then word-splits and executes this line — the dir
* is always `<root without spaces>/<slugified branch>`, so the line never
* needs quoting. Returns `null` outside the terminal so the `/worktree`
* opencode command never launches a nested TUI.
*/
export const opencodeLaunchDirective = ({ dir, purpose, terminal = false }) => {
if (!terminal) return null
const prompt = OPENCODE_SKILL_COMMAND_BY_PURPOSE[purpose]
const args = [dir, '--model', OPENCODE_PRESET_MODEL, '--auto']
if (prompt) args.push('--prompt', prompt)
return `launch opencode ${args.join(' ')}`
}

View file

@ -12,6 +12,17 @@
* `.agents/shell/worktree.sh`, sourced no profile).
* `--stay` suprime a linha `cd`; `--go` explícito
* continua aceito como no-op (era o antigo padrão).
* Chamado do terminal interativo (com
* `TEQO_WORKTREE_TERMINAL=1`, que só a função shell
* seta), imprime também a diretiva `launch
* opencode <dir> --model deepseek/deepseek-v4-flash
* --auto --prompt /work-issue` ANTES do `cd` — a
* função shell executa o cd e então a linha, e o
* TUI do opencode abre no worktree com `/work-issue`
* já enviado (OPS26). Presets em
* scripts/lib/worktree.mjs. Sem o marcador (comando
* `/worktree` do opencode), a diretiva não é
* impressa — nunca abre TUI aninhado.
* Também PROVISIONA o ambiente isolado do worktree:
* porta do dev server + bancos próprios derivados
* deterministicamente do branch (ver
@ -29,7 +40,12 @@
* sequencial `plans/plan-issue-<n>` livre. Nenhum
* deles (branch nem slot) colide com um `next`
* posterior (prefixo minúsculo `plans/…`).
* Mesmo provisionamento isolado do `next`.
* Mesmo provisionamento isolado do `next`; no
* terminal, mesma diretiva `launch` — porém SEM
* `--prompt` (a CLI do opencode não tem pre-fill
* sem submit; `/plan-` + autocomplete do TUI).
* `--stay` suprime a linha `cd`; `--go` explícito
* continua aceito como no-op (era o antigo padrão).
* pnpm worktree kill [--force] destrói o worktree em que o shell atual está
* (recusa worktree sujo sem `--force`) e remove os
* bancos gerados do worktree (best-effort); por
@ -59,11 +75,30 @@ import {
worktreeEnvFileContents,
worktreeEnvironment,
} from './lib/worktree-env.mjs'
import { branchNameForIssue, planBranchName } from './lib/worktree.mjs'
import {
branchNameForIssue,
opencodeLaunchDirective,
planBranchName,
WORKTREE_TERMINAL_ENV,
} from './lib/worktree.mjs'
const die = dieAgent('worktree')
const WORKTREES_ROOT = join(homedir(), '.cursor', 'worktrees', 'teqo')
/**
* True when the interactive terminal shell function (`.agents/shell/worktree.sh`)
* calls us: it sets `TEQO_WORKTREE_TERMINAL=1` and executes the `launch`
* directive we print. Without the marker (the `/worktree` opencode command,
* automation) the launch line is never printed — no nested TUI.
*/
const terminalShell = process.env[WORKTREE_TERMINAL_ENV] === '1'
/** Print the `launch` directive (only exists from the terminal shell); the `cd` line stays last. */
const printLaunchDirective = ({ dir, purpose }) => {
const line = opencodeLaunchDirective({ dir, purpose, terminal: terminalShell })
if (line) console.log(line)
}
/** Fixed compose project so `db:start`/provisioning always target ONE container. */
const SHARED_COMPOSE_PROJECT = 'teqo'
/** Maintenance database on the shared local container — only ever localhost. */
@ -332,7 +367,10 @@ const cmdNext = async (stay, skipMigrate) => {
console.log(` banco dev: postgresql://teqo:teqo@localhost:5432/${env.devDatabase}`)
console.log(` banco test: postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`)
if (!stay) console.log(`cd ${dir}`)
if (!stay) {
printLaunchDirective({ dir, purpose: 'next' })
console.log(`cd ${dir}`)
}
}
/**
@ -403,7 +441,10 @@ const cmdPlan = async (stay, skipMigrate, bag) => {
console.log(` banco dev: postgresql://teqo:teqo@localhost:5432/${env.devDatabase}`)
console.log(` banco test: postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`)
if (!stay) console.log(`cd ${dir}`)
if (!stay) {
printLaunchDirective({ dir, purpose: 'plan' })
console.log(`cd ${dir}`)
}
}
/** Generated database names referenced by a worktree's own env files. */
@ -498,15 +539,22 @@ if (!subcommand) {
console.log(' cria worktree da próxima Issue claimável (branch <code>-<slug>) e provisiona o')
console.log(' ambiente isolado: porta de dev + bancos próprios (determinístico do branch);')
console.log(' por padrão imprime `cd <dir>` no fim (quem aplica o cd: opencode command, ou a')
console.log(' função `worktree()` de .agents/shell/worktree.sh); --stay suprime o cd; --go')
console.log(' explícito continua aceito como no-op; --no-migrate pula migrations')
console.log(
' função `worktree()` de .agents/shell/worktree.sh); no terminal (TEQO_WORKTREE_TERMINAL=1)',
)
console.log(' imprime também a diretiva `launch opencode …` (OPS26: abre o TUI com')
console.log(' deepseek/deepseek-v4-flash + auto + /work-issue enviado); --stay suprime cd e')
console.log(' launch; --go explícito continua aceito como no-op; --no-migrate pula migrations')
console.log(`\n plan [bag] [--stay] [--no-migrate]`)
console.log(
' cria um worktree de planejamento DIFERENTE a cada invocação (sessões /plan-issue',
)
console.log(' paralelas): com bag, branch plans/plan-issue-<bag> (sufixo -2/-3 se o nome já')
console.log(' existir); sem bag, o próximo plans/plan-issue-<n> sequencial livre; o prefixo')
console.log(' minúsculo plans/… nunca colide com o branch <code>-<slug> de `next`')
console.log(
' minúsculo plans/… nunca colide com o branch <code>-<slug> de `next`; no terminal,',
)
console.log(' mesma diretiva `launch` porém sem --prompt (autocomplete completa /plan-)')
console.log(' kill [--force] destrói o worktree em que você está (recusa sujo sem --force),')
console.log(' remove os bancos gerados do worktree (best-effort) e imprime')
console.log(' `cd <main>` no fim — o shell sempre volta ao worktree principal')

View file

@ -16,8 +16,12 @@ import {
import {
branchNameForIssue,
issueCodeAndSubject,
OPENCODE_PRESET_MODEL,
OPENCODE_SKILL_COMMAND_BY_PURPOSE,
opencodeLaunchDirective,
PLAN_BRANCH_PREFIX,
planBranchName,
WORKTREE_TERMINAL_ENV,
} from '../../scripts/lib/worktree.mjs'
type TestIssue = {
@ -128,6 +132,39 @@ describe('planBranchName (per-invocation planning worktrees)', () => {
})
})
describe('opencodeLaunchDirective (terminal-only opencode launch, OPS26)', () => {
const dir = '/home/fsolla/.cursor/worktrees/teqo/OPS26-foo'
it('returns null outside the terminal — the /worktree command never launches a TUI', () => {
expect(opencodeLaunchDirective({ dir, purpose: 'next' })).toBeNull()
expect(opencodeLaunchDirective({ dir, purpose: 'plan', terminal: false })).toBeNull()
})
it('next launches with the preset model, --auto and the /work-issue command sent', () => {
expect(opencodeLaunchDirective({ dir, purpose: 'next', terminal: true })).toBe(
`launch opencode ${dir} --model deepseek/deepseek-v4-flash --auto --prompt /work-issue`,
)
})
it('plan launches with the same presets but NO prompt (no prefill-without-submit)', () => {
expect(opencodeLaunchDirective({ dir, purpose: 'plan', terminal: true })).toBe(
`launch opencode ${dir} --model deepseek/deepseek-v4-flash --auto`,
)
})
it('pins the preset constants — changing the model is editing a constant', () => {
expect(OPENCODE_PRESET_MODEL).toBe('deepseek/deepseek-v4-flash')
expect(WORKTREE_TERMINAL_ENV).toBe('TEQO_WORKTREE_TERMINAL')
expect(OPENCODE_SKILL_COMMAND_BY_PURPOSE).toEqual({ next: '/work-issue', plan: null })
})
it('an unknown purpose degrades to a prompt-less launch (fail-safe direction)', () => {
expect(opencodeLaunchDirective({ dir, purpose: 'bogus', terminal: true })).toBe(
`launch opencode ${dir} --model deepseek/deepseek-v4-flash --auto`,
)
})
})
describe('worktreeEnvironment (per-worktree ports and databases)', () => {
it('derives slot, port and database names deterministically from the branch', () => {
const env = worktreeEnvironment({ branch: 'C15-foo', code: 'C15' })