OPS27: worktree new — worktree neutro sem função pré-definida (work/<bag>)
This commit is contained in:
parent
dc2f76e1a1
commit
06d34ed23b
10 changed files with 318 additions and 62 deletions
|
|
@ -27,6 +27,11 @@
|
|||
# já existir), sem bag o próximo plans/plan-issue-<n>
|
||||
# sequencial; cd para dentro dele por padrão; --stay não
|
||||
# troca
|
||||
# worktree new [bag] [--stay] cria um worktree NEUTRO (sem função pré-definida)
|
||||
# DIFERENTE a cada chamada: com bag, branch work/<bag>
|
||||
# (sufixo -2/-3 se o nome já existir), sem bag o próximo
|
||||
# work/<n> sequencial; cd para dentro dele por padrão;
|
||||
# --stay não troca
|
||||
# worktree kill [--force] destrói o worktree atual e cd para o main por padrão
|
||||
#
|
||||
# `--go` explícito continua aceito como no-op (era o antigo padrão).
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ VMs Cloud **não rodam** `docker compose` / `pnpm db:start`. Postgres nativo: `.
|
|||
|
||||
## Per-worktree environments (`pnpm worktree next`)
|
||||
|
||||
Parallel agents must not fight over port 3000, the shared `teqo_test`, or a second Postgres container. `pnpm worktree next [--stay]` provisions each worktree with a deterministic isolated env (`next`/`plan` print `cd <dir>` by default — apply it, or pass `--stay` to suppress; `--go` is still accepted as a no-op):
|
||||
Parallel agents must not fight over port 3000, the shared `teqo_test`, or a second Postgres container. `pnpm worktree next [--stay]` provisions each worktree with a deterministic isolated env (`next`/`plan`/`new` print `cd <dir>` by default — apply it, or pass `--stay` to suppress; `--go` is still accepted as a no-op):
|
||||
|
||||
- `slot` = numeric part of the issue code (`C15` → 15, `B164` → 164; hash fallback without digits; bumped on collision among live worktrees).
|
||||
- Dev server port `3100+slot`; databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests), created on the **one shared container** with the full migration chain applied (one-time, ~30 s each).
|
||||
- Writes `.env.local` (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and `.env.test.local` (test DATABASE_URL, NEXT_PUBLIC_SITE_URL, **PLAYWRIGHT_BASE_URL** — both URL vars derived from the same slot port so a parallel e2e run never falls back to :3000; see `worktreeEnvFileContents` in `scripts/lib/worktree-env.mjs`). Both carry the marker `# teqo worktree env (generated by pnpm worktree next)`; **manual env files are never overwritten** — remove them and re-run `pnpm worktree next` (or `pnpm worktree plan` for the /plan-issue worktree) to regenerate.
|
||||
- Writes `.env.local` (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and `.env.test.local` (test DATABASE_URL, NEXT_PUBLIC_SITE_URL, **PLAYWRIGHT_BASE_URL** — both URL vars derived from the same slot port so a parallel e2e run never falls back to :3000; see `worktreeEnvFileContents` in `scripts/lib/worktree-env.mjs`). Both carry the marker `# teqo worktree env (generated by pnpm worktree next)`; **manual env files are never overwritten** — remove them and re-run `pnpm worktree next` (or `pnpm worktree plan` for the /plan-issue worktree, `pnpm worktree new` for a neutral one) to regenerate.
|
||||
- `--no-migrate` skips the migration runs; `pnpm worktree kill` drops the worktree's generated databases (best-effort, `DROP DATABASE … WITH (FORCE)`).
|
||||
- Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning and no port override.
|
||||
- Pure derivation: `scripts/lib/worktree-env.mjs` (unit-tested in `tests/unit/worktree.unit.spec.ts`).
|
||||
|
|
|
|||
|
|
@ -21,14 +21,15 @@ A lógica determinística (fila → código+slug → branch → worktree) vive e
|
|||
```bash
|
||||
pnpm worktree next [--stay] [--no-migrate] # cria worktree de origin/main + provisiona o ambiente
|
||||
pnpm worktree plan [bag] [--stay] [--no-migrate] # worktree de PLANEJAMENTO (/plan-issue): um DIFERENTE por invocação
|
||||
pnpm worktree new [bag] [--stay] [--no-migrate] # worktree NEUTRO (sem função pré-definida): um DIFERENTE por invocação
|
||||
pnpm worktree kill [--force] # destrói o worktree atual + remove seus bancos gerados
|
||||
```
|
||||
|
||||
No opencode, isso é o comando **`/worktree next`** / **`/worktree kill`** (`.opencode/commands/worktree.md`), que só repassa `$ARGUMENTS` para o script.
|
||||
|
||||
`plan` é o primo do `next`: worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main. **Cada invocação cria um DIFERENTE** (sessões paralelas): com `bag` opcional → branch `plans/plan-issue-<bag>` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag` → próximo sequencial `plans/plan-issue-<n>` livre. Não é nomeado por Issue nenhuma — proposital, para um `next` posterior da próxima Issue nunca colidir em branch nem em slot (prefixo minúsculo `plans/…` vs branch `<Code>-<slug>` sempre uppercase-led). O fluxo desta skill (fila → próxima Issue → implementação) é só `next`.
|
||||
`plan` é o primo do `next`: worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main. **Cada invocação cria um DIFERENTE** (sessões paralelas): com `bag` opcional → branch `plans/plan-issue-<bag>` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag` → próximo sequencial `plans/plan-issue-<n>` livre. Não é nomeado por Issue nenhuma — proposital, para um `next` posterior da próxima Issue nunca colidir em branch nem em slot (prefixo minúsculo `plans/…` vs branch `<Code>-<slug>` sempre uppercase-led). `new` é o irmão **neutro** (sem função pré-definida — explorar ideia, conversar, planejar sem registrar): mesma semântica de "um DIFERENTE por invocação", branch `work/<bag>` (sufixo `-2`/`-3` em colisão) ou `work/<n>` sequencial, prefixo `work/…` que não colide com `next` nem `plan`. O fluxo desta skill (fila → próxima Issue → implementação) é só `next`.
|
||||
|
||||
**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).
|
||||
**Default-go:** `next`, `plan` e `new` 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.**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
---
|
||||
description: Cria o worktree da próxima Issue claimável (next [--stay]), um worktree novo de planejamento do /plan-issue (plan [bag] [--stay]) ou destrói o worktree atual (kill [--force])
|
||||
description: Cria o worktree da próxima Issue claimável (next [--stay]), um worktree novo de planejamento do /plan-issue (plan [bag] [--stay]), um worktree neutro sem função pré-definida (new [bag] [--stay]) ou destrói o worktree atual (kill [--force])
|
||||
---
|
||||
|
||||
Rode o script determinístico abaixo (não improvise a lógica) e reporte o resultado de forma concisa.
|
||||
|
||||
- `next [--stay]`: cria (ou reutiliza) o worktree da próxima Issue claimável. Por padrão o script imprime `cd <dir>` na última linha — aplique-o na sessão de shell atual (`cd <dir>`) e reporte o novo diretório. `--stay` suprime o `cd` (não troque de diretório). `--go` explícito continua aceito como no-op.
|
||||
- `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.
|
||||
- `new [bag] [--stay]`: cria um worktree **neutro** (sem função pré-definida — explorar ideia, conversar, planejar sem registrar nada) **diferente a cada invocação** (branch `work/<bag>`, ou `work/<n>` sequencial sem bag; sufixo `-2`/`-3` em colisão); o prefixo minúsculo `work/…` nunca conflita com `next` nem `plan`. Mesmo contrato de `cd` do `plan`: 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.
|
||||
|
|
|
|||
|
|
@ -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. **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.
|
||||
- **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`/`new` 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`/`new` 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` and `new` open with the same presets and NO prompt (`plan`: `/plan-` + TUI autocomplete; `new`: "apenas conversar", no skill — 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. `worktree new [bag] [--stay]` (OPS27, 2026-08-10) is the NEUTRAL sibling: a worktree with **no pre-defined function** (explore an idea, chat, plan without registering anything) — also a DIFFERENT one per invocation, branch `work/<bag>` (suffixed `-2`/`-3` on collision) or the next free sequential `work/<n>` (`WORK_BRANCH_PREFIX`/`workBranchName`, same `namespaceBranchName` core as `plan`); the lowercase `work/…` prefix collides with neither `next` nor `plan`, and the provisioning is identical with `purpose: 'new'` (env comment `gerado por pnpm worktree new`; the frozen env marker keeps its historical `…next` text). `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.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,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):** **OPS27** (Issue #572) — `pnpm worktree new [bag] [--stay] [--no-migrate]`: worktree **neutro**, sem função pré-definida (explorar ideia, conversar, planejar sem registrar nada) — cada invocação cria UM DIFERENTE, como o `plan`: com `bag`, branch `work/<bag>` (sufixo `-2`/`-3` em colisão); sem `bag`, o próximo sequencial `work/<n>` livre (`WORK_BRANCH_PREFIX`/`workBranchName`, `scripts/lib/worktree.mjs`). Provisionamento idêntico ao `plan` (`purpose: 'new'`; comentário `gerado por pnpm worktree new`; porta `3100+slot`, bancos próprios, migrations, `--no-migrate` respeitado); herda o default-go da OPS24 (`cd <dir>` na última linha, `--stay` suprime) e `kill` funciona de dentro; **no terminal, herda o launch da OPS26 prompt-less** (`new: null` no `OPENCODE_SKILL_COMMAND_BY_PURPOSE` — "apenas conversar", nenhuma skill enviada). **Decisões de engenharia:** o molde `cmdPlan` virou o runner compartilhado `cmdNamespaceBranch` (`scripts/worktree.mjs` — fetch → taken-set → add → provision → launch+cd; `cmdPlan` refatorado sem mudança de comportamento, `printLaunchDirective` parametrizado por `purpose`); a nomeação de branch generalizou num helper interno `namespaceBranchName` (a matemática de sufixo/sequencial/truncamento do `plan` é idêntica à do `new` — `planBranchName` vira wrapper fino, contrato e testes intactos); **`GENERATED_ENV_MARKER` continua congelado** no texto histórico `…worktree next` (unit-pinado e usado por `readLiveSlots`/`worktreeDatabaseNamesOf` para detectar envs vivas — o propósito real sai na linha `generatedBy` do header). Superfícies: `worktree.sh` + comando `/worktree` do opencode (genéricos, só ganharam o subcomando no help), AGENTS.md, skills `worktree-next-issue`/`local-database`. Unit novos de `workBranchName` (10 casos: sequencial, bag, colisão, disjunção com next/plan, fallback, truncamento, budget do sufixo) + `new` prompt-less no launch + verificação manual do caminho feliz (criação e kill de um `work/…` descartável). Sem migration/dados/access; libs puras e marker intocados. Intenção [`worktree-new-worktree-generico.md`](plans/worktree-new-worktree-generico.md); impl [`worktree-new-worktree-generico-impl.md`](plans/worktree-new-worktree-generico-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).
|
||||
|
|
|
|||
81
docs/plans/worktree-new-worktree-generico-impl.md
Normal file
81
docs/plans/worktree-new-worktree-generico-impl.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Impl: Worktree `new`: worktree genérico sem função pré-definida
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-10
|
||||
Issue: #572
|
||||
Intenção: docs/plans/worktree-new-worktree-generico.md
|
||||
Appetite restante: ~0,5–1 dia eng (herdado — scripts/docs/testes, sem runtime nem DB de dados)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** `worktree new [bag]` cria um worktree **neutro** a partir de `origin/main` com branch `work/<bag>` (colisão → sufixo `-2`/`-3`) ou `work/<n>` sequencial livre, provisionado de forma idêntica ao `plan` (slot/porta/bancos próprios, migrations aplicadas, `--no-migrate` respeitado, comentário `gerado por pnpm worktree new`), cada invocação cria UM DIFERENTE, herda o default-go da OPS24 (`--stay` suprime, `--go` no-op), `kill` funciona de dentro dele, help/uso + AGENTS.md atualizados.
|
||||
- **O que NÃO negociar:** prefixo minúsculo `work/…` que nunca colide com `<Code>-<slug>` do `next` nem com `plans/plan-issue-…` do `plan`; provisionamento idêntico ao `plan`; `new` **não** é um segundo `plan` (sem função de planejamento embutida); OPS26 (launch do opencode) é dependência **opcional e não está em main** — o `new` nasce só com o default-go, sem duplicar o launch.
|
||||
- **O que reavaliar:**
|
||||
- A hipótese "generalizar `planBranchName` se valer a pena" — **vale**: a matemática de sufixo de colisão (`-2`/`-3`), sequencial livre e truncamento é idêntica e sutil entre os dois namespaces; um helper único impede drift (mesma família de guarda que o repo já aplica nos prefixos next/plan).
|
||||
- **`GENERATED_ENV_MARKER` é congelado:** o unit test pina o texto exato `generated by pnpm worktree next` e `readLiveSlots`/`worktreeDatabaseNamesOf`/`envFileIsGenerated` dependem dele para detectar envs geradas (inclusive de worktrees `next` vivos). O `new` **não** parametriza o marker — o propósito real aparece na linha `generatedBy` do header (`gerado por pnpm worktree new`), que o `provision` já deriva de `purpose`.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[pnpm worktree new [bag]] --> B[namespaceBranchName: work/<bag> ou work/<n>]
|
||||
A2[pnpm worktree plan [bag]] --> B2[planBranchName: plano existente]
|
||||
B --> C[runner compartilhado cmdNamespaceBranch: fetch → taken-set → add → provision → cd]
|
||||
B2 --> C
|
||||
C --> D[worktree.mjs: purpose 'new'/'plan' + linhas cd por padrão]
|
||||
D --> E[superfícies: worktree.sh + /worktree opencode aplicam cd]
|
||||
```
|
||||
|
||||
**Decisão 1 — nomeação de branch (`scripts/lib/worktree.mjs`).**
|
||||
Opções: A) extrair helper interno `namespaceBranchName({ prefix, bag, taken, maxLength, fallback })`; `planBranchName` vira wrapper fino (contrato + testes intactos) e novo `workBranchName` com `WORK_BRANCH_PREFIX = 'work'`; B) parametrizar `planBranchName` com prefixo opcional; C) duplicar a lógica em `workBranchName`.
|
||||
**Recomendação: A** — a matemática de colisão/sequencial/truncamento é idêntica e sutil; um único lugar evita drift (os dois namespaces vivem no mesmo arquivo há uma semana de evolução ativa). **Rejeitadas:** B — `planBranchName` produzindo `work/…` mente no nome e obriga default paramétrico; C — duplica ~25 linhas de truncamento delicado (budget 60, `keep` mínimo 1).
|
||||
Detalhes do contrato `workBranchName`: sem bag → próximo `work/<n>` sequencial livre (começa em `work/1`); com bag → `work/<slug>`; colisão → `work/<slug>-2`, `-3`, …; slug vazio → fallback `'work'` (o `plan` preserva o fallback `'plano'`); bag numérico (`work new 3`) compartilha o namespace sequencial (precedente uniforme do plan); budget total 60 (espelha `branchNameForIssue`/`PLAN_BRANCH_MAX_LENGTH`); o prefixo `work/` é lowercase-led → estruturalmente disjunto do `<Code>-<slug>` uppercase-led e do `plans/plan-issue-…`.
|
||||
|
||||
**Decisão 2 — `cmdNew` (`scripts/worktree.mjs`).**
|
||||
Opções: A) extrair de `cmdPlan` o runner compartilhado `cmdNamespaceBranch({ purpose, branchName, label })` (fetch → taken-set → `check-ref-format` → `worktree add` → `provision` → summary do ambiente → `cd`), com `cmdPlan` refatorado para o runner e `cmdNew` como segundo call site; B) clonar `cmdPlan` em `cmdNew` (~60 linhas); C) mode flag dentro de `cmdPlan`.
|
||||
**Recomendação: A** — o CLI está em evolução ativa (OPS24/OPS26/OPS27 na mesma semana); duplicar ~60 linhas de git/provisionamento drifta. **Rejeitadas:** B — duplicação cara de manter; C — embaralha propósito em flag e cresce o teste mental do dispatch.
|
||||
|
||||
**Decisão 3 — marker de env.**
|
||||
Opções: A) `GENERATED_ENV_MARKER` inalterado (texto histórico `…next`) para todos os purposes; B) parametrizar o marker por purpose.
|
||||
**Recomendação: A** — o marker é contrato congelado e pinado: `readLiveSlots`, `worktreeDatabaseNamesOf`, `envFileIsGenerated` e o unit test dependem do texto exato; mudar quebraria a detecção de envs de worktrees `next` vivos e exigiria "migração" de marker. O comentário real por-worktree já diz o propósito (`generatedBy`). **Rejeitada:** B.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`scripts/lib/worktree.mjs`**: helper interno `namespaceBranchName`; exports `WORK_BRANCH_PREFIX = 'work'` e `workBranchName`; `planBranchName` refatorado para wrapper (contrato idêntico, testes intactos).
|
||||
- **`scripts/worktree.mjs`**: runner `cmdNamespaceBranch` extraído de `cmdPlan` (taken-set, branch, add, provision, summary, cd); `cmdNew(stay, skipMigrate, bag)` com `purpose: 'new'`; dispatch com `new`; bloco de uso (`pnpm worktree` sem args) e docblock do header atualizados (`new [bag] [--stay] [--no-migrate]`).
|
||||
- **`tests/unit/worktree.unit.spec.ts`**: describe novo `workBranchName` — sequencial livre (skip de taken), bag → `work/<slug>`, colisão `-2`/`-3`, slug vazio → fallback, bag numérico compartilha o namespace, truncamento ≤60, nunca colide com `next` nem `plan` (regex de lowercase-led), invocações paralelas → branches diferentes.
|
||||
- **`.agents/shell/worktree.sh`**: header de uso com `new [bag] [--stay]` (a função já é genérica — repassa args e aplica o `cd`).
|
||||
- **`.opencode/commands/worktree.md`**: frontmatter `description` + corpo com `new [bag] [--stay]` (mesmo contrato de `cd` do `plan`).
|
||||
- **`AGENTS.md`** (seção "Per-worktree environments"): subcomando `new [bag]` + namespace `work/…` + `purpose: 'new'`.
|
||||
- **`.agents/skills/worktree-next-issue/SKILL.md`**: linha dos subcomandos com `new [bag] [--stay] [--no-migrate]`.
|
||||
- **`docs/CHANGELOG-AGENTS.md`**: entrada curta da entrega.
|
||||
- **Migration:** sem migration (nenhum schema). **Access / Consent:** N/A — CLI de dev fora do runtime. **UI:** Impeccable A — N/A (sem superfície de produto).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Lib pura + testes** — `namespaceBranchName`/`workBranchName`/`WORK_BRANCH_PREFIX` + `workBranchName.unit.spec.ts`; `pnpm test:unit` no arquivo.
|
||||
2. **Runner + comando** — `cmdNamespaceBranch` (refactor de `cmdPlan`) + `cmdNew` + dispatch + uso + docblock; superfícies (shell/opencode) + docs (AGENTS.md, skill, CHANGELOG).
|
||||
3. **Verificação manual do caminho feliz** — um `worktree new teste-gate` real (cria worktree + bancos + migrations no container local), conferir branch `work/teste-gate`, env com `gerado por pnpm worktree new` e porta própria, e derrubar com `pnpm worktree kill --force` de dentro (valida também o `kill` num branch `work/…`). `pnpm worktree` sem args mostra o `new` no uso.
|
||||
4. **Gates** — `pnpm gate:fast` na iteração; `pnpm push` na entrega (format/knip/cycles/test/build).
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Launch do opencode no `new`**: OPS26 é o dono e não está em main — o `new` nasce com default-go; a superfície é a mesma que a OPS26 estenderá (nada a construir).
|
||||
- Não tocar `scripts/lib/worktree-env.mjs` nem `GENERATED_ENV_MARKER` (pinados por teste).
|
||||
- Não criar terceira função embutida (`worktree new --issue` etc.) — para isso existem `next` e `plan`.
|
||||
- Não renomear/normalizar branches `work/…` manuais existentes nem "migrar" markers de envs vivas.
|
||||
- Sem e2e: CLI fora do runtime; cobertura = unit da lib pura + verificação manual descartável.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Refatorar `cmdPlan` introduz regressão no fluxo existente** — mitigação: extração com diff minimal (movimentação pura de corpo + parâmetros `purpose`/`label`/branch fn), mensagens de saída idênticas; conferir por diff e pelo caminho feliz do `new` (compartilha o runner).
|
||||
- **`work/…` colide com branch manual do usuário** — taken-set local+origin já cobre (sufixo `-2`/`-3`); comportamento idêntico ao `plan`, documentado.
|
||||
- **Slot do `new` colide com worktrees vivos** — herdado: `readLiveSlots` + bump no `worktreeEnvironment` (já testado).
|
||||
- **Texto do marker `next` em envs de `new` confundir leitura** — a linha `generatedBy` carrega o propósito real; registrado neste impl plan.
|
||||
- **`worktree new` sem Docker degrada para env compartilhado** — herdado do `provision` (mesmo aviso do `next`/`plan`).
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto (`new [bag]` de `origin/main`; branch `work/<bag>`/`work/<n>` sem colisão; provision igual ao `plan` com `purpose: 'new'` e `--no-migrate` respeitado; default-go/`--stay`; `kill` funciona; uso + AGENTS.md atualizados)
|
||||
- [ ] Invariantes AGENTS/engineering-standards (marker e `worktree-env.mjs` intocados; nenhum DB de prod; libs puras unit-testadas)
|
||||
- [ ] Testes de domínio previstos: `workBranchName` (unit) + caminho feliz manual descartável
|
||||
|
|
@ -25,15 +25,23 @@ 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.
|
||||
* and `new` send nothing — the opencode CLI's `--prompt` always auto-submits,
|
||||
* there is no prefill-without-submit flag, and the TUI autocomplete completes
|
||||
* `/plan-` instead (`new` is just "apenas conversar", no skill at all; 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 }
|
||||
export const OPENCODE_SKILL_COMMAND_BY_PURPOSE = { next: '/work-issue', plan: null, new: null }
|
||||
|
||||
/** Total branch-name budget for plan branches — mirrors `branchNameForIssue`. */
|
||||
const PLAN_BRANCH_MAX_LENGTH = 60
|
||||
/**
|
||||
* Prefix of every neutral-worktree branch (`pnpm worktree new`). Lowercase-led
|
||||
* `work/…` — structurally disjoint from `next`'s uppercase-led `<Code>-<slug>`
|
||||
* and from `plan`'s `plans/plan-issue-…`, in branch name and slot space alike.
|
||||
*/
|
||||
export const WORK_BRANCH_PREFIX = 'work'
|
||||
|
||||
/** Total branch-name budget — mirrors `branchNameForIssue` (60). */
|
||||
const NAMESPACE_BRANCH_MAX_LENGTH = 60
|
||||
|
||||
/** Strip the leading `<code> — ` (or any dash variant) off a title. */
|
||||
const stripCodePrefix = (title, code) => {
|
||||
|
|
@ -71,37 +79,38 @@ export const branchNameForIssue = (issue, maxLength = 60) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Branch for a `/plan-issue` planning worktree. Every invocation must land on
|
||||
* a DIFFERENT branch so parallel planning sessions coexist:
|
||||
* - `bag` given → `plans/plan-issue-<bag-slug>`; if that name is already
|
||||
* taken, `plans/plan-issue-<bag-slug>-2`, `-3`, …
|
||||
* - no `bag` → the next free sequential `plans/plan-issue-1`, `-2`, `-3`, …
|
||||
* `taken` = branch short-names already alive (local refs + origin); when a
|
||||
* name is free it is reused only as a name-free slot — the branch is created
|
||||
* fresh from `origin/main` each time. The lowercase `plans/…` prefix cannot
|
||||
* collide with `next`'s uppercase-led `<Code>-<slug>` branches.
|
||||
* Shared branch naming for namespace worktrees NOT tied to the claim queue
|
||||
* (`plan`, `new`). Every invocation must land on a DIFFERENT branch so
|
||||
* parallel sessions coexist:
|
||||
* - `bag` given → `<prefix><slug>`; if that name is already taken,
|
||||
* `<prefix><slug>-2`, `-3`, …
|
||||
* - no `bag` → the next free sequential `<prefix>1`, `<prefix>2`, …
|
||||
* `prefix` carries the namespace AND its separator (`plans/plan-issue-` for
|
||||
* plan, `work/` for new), so the sequential and bag forms share one spelling
|
||||
* per namespace. `taken` = branch short-names already alive (local refs +
|
||||
* origin); when a name is free it is reused only as a name-free slot — the
|
||||
* branch is created fresh from `origin/main` each time. Both namespaces are
|
||||
* lowercase-led, so neither can collide with `next`'s uppercase-led
|
||||
* `<Code>-<slug>` branches.
|
||||
*/
|
||||
export const planBranchName = ({ bag = '', taken = new Set() }) => {
|
||||
const namespaceBranchName = ({ prefix, bag = '', taken = new Set(), fallback }) => {
|
||||
const hasBag = typeof bag === 'string' && bag.trim().length > 0
|
||||
|
||||
if (!hasBag) {
|
||||
for (let n = 1; ; n += 1) {
|
||||
const candidate = `${PLAN_BRANCH_PREFIX}-${n}`
|
||||
const candidate = `${prefix}${n}`
|
||||
if (!taken.has(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
||||
const slug = slugify(bag) || 'plano'
|
||||
const base = `${PLAN_BRANCH_PREFIX}-${slug.slice(
|
||||
0,
|
||||
PLAN_BRANCH_MAX_LENGTH - PLAN_BRANCH_PREFIX.length - 1,
|
||||
)}`
|
||||
const slug = slugify(bag) || fallback
|
||||
const base = `${prefix}${slug.slice(0, NAMESPACE_BRANCH_MAX_LENGTH - prefix.length)}`
|
||||
if (!taken.has(base)) return base
|
||||
|
||||
for (let n = 2; ; n += 1) {
|
||||
const suffix = `-${n}`
|
||||
const keep = Math.max(1, PLAN_BRANCH_MAX_LENGTH - PLAN_BRANCH_PREFIX.length - 1 - suffix.length)
|
||||
const candidate = `${PLAN_BRANCH_PREFIX}-${slug.slice(0, keep)}${suffix}`
|
||||
const keep = Math.max(1, NAMESPACE_BRANCH_MAX_LENGTH - prefix.length - suffix.length)
|
||||
const candidate = `${prefix}${slug.slice(0, keep)}${suffix}`
|
||||
if (!taken.has(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
|
@ -123,3 +132,17 @@ export const opencodeLaunchDirective = ({ dir, purpose, terminal = false }) => {
|
|||
if (prompt) args.push('--prompt', prompt)
|
||||
return `launch opencode ${args.join(' ')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Branch for a `/plan-issue` planning worktree — see `namespaceBranchName`
|
||||
* (namespace `plans/plan-issue-…`, fallback label `plano`).
|
||||
*/
|
||||
export const planBranchName = ({ bag = '', taken = new Set() }) =>
|
||||
namespaceBranchName({ prefix: `${PLAN_BRANCH_PREFIX}-`, bag, taken, fallback: 'plano' })
|
||||
|
||||
/**
|
||||
* Branch for a neutral worktree (`pnpm worktree new`) — see
|
||||
* `namespaceBranchName` (namespace `work/…`, fallback label `work`).
|
||||
*/
|
||||
export const workBranchName = ({ bag = '', taken = new Set() }) =>
|
||||
namespaceBranchName({ prefix: `${WORK_BRANCH_PREFIX}/`, bag, taken, fallback: 'work' })
|
||||
|
|
|
|||
|
|
@ -44,8 +44,21 @@
|
|||
* 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 new [bag] [--stay] [--no-migrate]
|
||||
* cria um worktree NEUTRO novo — sem função
|
||||
* pré-definida (explorar ideia, conversar, ou
|
||||
* planejar sem registrar nada): cada invocação
|
||||
* cria UM DIFERENTE; com `bag`, branch
|
||||
* `work/<bag>` (e `-2`, `-3`, … se o nome já
|
||||
* estiver vivo); sem `bag`, o próximo sequencial
|
||||
* `work/<n>` livre. Prefixo minúsculo `work/…` —
|
||||
* nunca colide com `<code>-<slug>` de `next` nem
|
||||
* com `plans/plan-issue-…` de `plan`.
|
||||
* Mesmo provisionamento isolado do `next`/`plan`;
|
||||
* no terminal, mesma diretiva `launch` — SEM
|
||||
* `--prompt` (apenas conversar, nenhuma skill).
|
||||
* `--stay` suprime a linha `cd` e a diretiva;
|
||||
* `--go` explícito continua aceito como no-op.
|
||||
* 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
|
||||
|
|
@ -79,6 +92,7 @@ import {
|
|||
branchNameForIssue,
|
||||
opencodeLaunchDirective,
|
||||
planBranchName,
|
||||
workBranchName,
|
||||
WORKTREE_TERMINAL_ENV,
|
||||
} from './lib/worktree.mjs'
|
||||
|
||||
|
|
@ -250,7 +264,7 @@ const writeFallbackEnv = (dir, branch) => {
|
|||
* Deterministic per-worktree environment: shared container + own databases +
|
||||
* own dev-server port + env files + migrations. Skips everything when a
|
||||
* manual `.env.local`/`.env.test.local` (no marker) exists — never clobbers.
|
||||
* `purpose` labels the env comment (`next` | `plan`); `issue` is optional and
|
||||
* `purpose` labels the env comment (`next` | `plan` | `new`); `issue` is optional and
|
||||
* fills the `issue #N` part of the comment only when present.
|
||||
*/
|
||||
const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpose = 'next' }) => {
|
||||
|
|
@ -373,22 +387,8 @@ const cmdNext = async (stay, skipMigrate) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `plan` — a `/plan-issue` planning worktree. Deliberately NOT tied to the
|
||||
* claim queue and NOT named after any Issue: the branch is derived either from
|
||||
* an optional `bag` slug (`plans/plan-issue-<bag>`, suffixed `-2`, `-3`, … on
|
||||
* collision) or from the next free sequential `plans/plan-issue-<n>`, so a
|
||||
* later `pnpm worktree next` for the next claimable Issue never collides with
|
||||
* it (branch name nor slot — each plan env registers its hashed slot via the
|
||||
* marker and `next` bumps around it). Every invocation creates a DIFFERENT
|
||||
* worktree — parallel `/plan-issue` sessions never share one. Same isolated
|
||||
* env provisioning as `next`.
|
||||
*/
|
||||
const cmdPlan = async (stay, skipMigrate, bag) => {
|
||||
git(['fetch', 'origin'])
|
||||
|
||||
const entries = parseWorktreeList(git(['worktree', 'list', '--porcelain']))
|
||||
|
||||
/** Branch short-names already alive — local refs plus origin (shortened). */
|
||||
const buildTakenBranchNames = () => {
|
||||
const taken = new Set()
|
||||
for (const scope of [
|
||||
(
|
||||
|
|
@ -406,24 +406,38 @@ const cmdPlan = async (stay, skipMigrate, bag) => {
|
|||
]) {
|
||||
for (const name of scope) if (name) taken.add(name)
|
||||
}
|
||||
return taken
|
||||
}
|
||||
|
||||
const branch = planBranchName({ bag, taken })
|
||||
/**
|
||||
* Shared runner for namespace worktrees NOT tied to the claim queue (`plan`
|
||||
* and `new`): fetches origin, picks a FRESH branch in the namespace (via
|
||||
* `branchName`), provisions the same isolated env as `next` (with `purpose`)
|
||||
* and prints the `cd <dir>` line by default (`--stay` suppresses). Every
|
||||
* invocation creates a DIFFERENT worktree — parallel sessions never share one.
|
||||
*/
|
||||
const cmdNamespaceBranch = async ({ stay, skipMigrate, purpose, noun, sessionLabel, branchName }) => {
|
||||
git(['fetch', 'origin'])
|
||||
|
||||
const entries = parseWorktreeList(git(['worktree', 'list', '--porcelain']))
|
||||
const taken = buildTakenBranchNames()
|
||||
|
||||
const branch = branchName(taken)
|
||||
if (git(['check-ref-format', '--allow-onelevel', branch], { okIfFails: true }) === null) {
|
||||
die(`Branch plan inválido para refname: ${branch}`)
|
||||
die(`Branch ${purpose} inválido para refname: ${branch}`)
|
||||
}
|
||||
const dir = join(WORKTREES_ROOT, branch)
|
||||
|
||||
if (entries.some((entry) => resolve(entry.path) === resolve(dir))) {
|
||||
die(
|
||||
`Já existe um worktree de planejamento em ${dir} (branch ${branch} não detectado nos refs). Rode \`pnpm worktree kill\` de dentro dele.`,
|
||||
`Já existe um worktree ${noun} em ${dir} (branch ${branch} não detectado nos refs). Rode \`pnpm worktree kill\` de dentro dele.`,
|
||||
)
|
||||
}
|
||||
|
||||
const label = bag && bag.trim() ? `lote "${bag}"` : 'sequencial'
|
||||
git(['worktree', 'add', '-b', branch, dir, 'origin/main'])
|
||||
|
||||
console.log('Worktree de planejamento criado:')
|
||||
console.log(` sessão: ${label}`)
|
||||
console.log(`Worktree ${noun} criado:`)
|
||||
console.log(` sessão: ${sessionLabel}`)
|
||||
console.log(` branch: ${branch}`)
|
||||
console.log(` path: ${dir}`)
|
||||
console.log(' origem: origin/main')
|
||||
|
|
@ -434,19 +448,58 @@ const cmdPlan = async (stay, skipMigrate, bag) => {
|
|||
code: null,
|
||||
takenSlots: readLiveSlots(entries, dir),
|
||||
})
|
||||
await provision({ dir, branch, env, skipMigrate, mainRoot, purpose: 'plan' })
|
||||
await provision({ dir, branch, env, skipMigrate, mainRoot, purpose })
|
||||
|
||||
console.log(`\nAmbiente isolado do worktree de planejamento (slot ${env.slot}):`)
|
||||
console.log(`\nAmbiente isolado do worktree ${noun} (slot ${env.slot}):`)
|
||||
console.log(` dev server: http://localhost:${env.devPort} (pnpm dev)`)
|
||||
console.log(` banco dev: postgresql://teqo:teqo@localhost:5432/${env.devDatabase}`)
|
||||
console.log(` banco test: postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`)
|
||||
|
||||
if (!stay) {
|
||||
printLaunchDirective({ dir, purpose: 'plan' })
|
||||
printLaunchDirective({ dir, purpose })
|
||||
console.log(`cd ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `plan` — a `/plan-issue` planning worktree. Deliberately NOT tied to the
|
||||
* claim queue and NOT named after any Issue: the branch is derived either from
|
||||
* an optional `bag` slug (`plans/plan-issue-<bag>`, suffixed `-2`, `-3`, … on
|
||||
* collision) or from the next free sequential `plans/plan-issue-<n>`, so a
|
||||
* later `pnpm worktree next` for the next claimable Issue never collides with
|
||||
* it (branch name nor slot — each plan env registers its hashed slot via the
|
||||
* marker and `next` bumps around it). Same isolated env provisioning as
|
||||
* `next`.
|
||||
*/
|
||||
const cmdPlan = async (stay, skipMigrate, bag) =>
|
||||
cmdNamespaceBranch({
|
||||
stay,
|
||||
skipMigrate,
|
||||
purpose: 'plan',
|
||||
noun: 'de planejamento',
|
||||
sessionLabel: bag && bag.trim() ? `lote "${bag}"` : 'sequencial',
|
||||
branchName: (taken) => planBranchName({ bag, taken }),
|
||||
})
|
||||
|
||||
/**
|
||||
* `new` — a NEUTRAL worktree with no pre-defined function (explore an idea,
|
||||
* chat, or plan without registering anything). Deliberately NOT tied to the
|
||||
* claim queue nor to `/plan-issue`: with `bag`, branch `work/<bag>` (suffixed
|
||||
* `-2`, `-3`, … on collision); without, the next free sequential `work/<n>`.
|
||||
* The lowercase `work/…` prefix can never collide with a `next` branch
|
||||
* (uppercase-led `<code>-<slug>`) nor with a `plan` branch (`plans/…`), in
|
||||
* branch name or slot space. Same isolated env provisioning as `plan`.
|
||||
*/
|
||||
const cmdNew = async (stay, skipMigrate, bag) =>
|
||||
cmdNamespaceBranch({
|
||||
stay,
|
||||
skipMigrate,
|
||||
purpose: 'new',
|
||||
noun: 'neutro',
|
||||
sessionLabel: bag && bag.trim() ? `bag "${bag}"` : 'sequencial',
|
||||
branchName: (taken) => workBranchName({ bag, taken }),
|
||||
})
|
||||
|
||||
/** Generated database names referenced by a worktree's own env files. */
|
||||
const worktreeDatabaseNamesOf = (dir) => {
|
||||
const names = []
|
||||
|
|
@ -533,7 +586,7 @@ const subcommand = positional[0]
|
|||
|
||||
if (!subcommand) {
|
||||
console.log(
|
||||
'Uso: pnpm worktree next [--stay] [--no-migrate] | plan [bag] [--stay] [--no-migrate] | kill [--force]',
|
||||
'Uso: pnpm worktree next [--stay] [--no-migrate] | plan [bag] [--stay] [--no-migrate] | new [bag] [--stay] [--no-migrate] | kill [--force]',
|
||||
)
|
||||
console.log(' next [--stay] [--no-migrate]')
|
||||
console.log(' cria worktree da próxima Issue claimável (branch <code>-<slug>) e provisiona o')
|
||||
|
|
@ -555,6 +608,12 @@ if (!subcommand) {
|
|||
' 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(`\n new [bag] [--stay] [--no-migrate]`)
|
||||
console.log(' cria um worktree NEUTRO (sem função pré-definida) DIFERENTE a cada invocação:')
|
||||
console.log(' com bag, branch work/<bag> (sufixo -2/-3 se o nome já existir); sem bag, o')
|
||||
console.log(' próximo work/<n> sequencial livre; o prefixo minúsculo work/… nunca colide com')
|
||||
console.log(' o branch <code>-<slug> de `next` nem com plans/plan-issue-… de `plan`; no')
|
||||
console.log(' terminal, mesma diretiva `launch` porém sem --prompt (apenas conversar)')
|
||||
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')
|
||||
|
|
@ -565,10 +624,12 @@ try {
|
|||
if (subcommand === 'next') await cmdNext(Boolean(flags.stay), Boolean(flags['no-migrate']))
|
||||
else if (subcommand === 'plan')
|
||||
await cmdPlan(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1])
|
||||
else if (subcommand === 'new')
|
||||
await cmdNew(Boolean(flags.stay), Boolean(flags['no-migrate']), positional[1])
|
||||
else if (subcommand === 'kill') {
|
||||
if (flags.stay) die('`--stay` não se aplica a `kill` — ele sempre volta ao main.')
|
||||
await cmdKill(Boolean(flags.force))
|
||||
} else die(`subcomando desconhecido: ${subcommand} (esperado: next | plan | kill)`)
|
||||
} else die(`subcomando desconhecido: ${subcommand} (esperado: next | plan | new | kill)`)
|
||||
} catch (error) {
|
||||
if (error?.stderr) die(error.stderr.toString().trim())
|
||||
die(error?.message ?? String(error))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import {
|
|||
opencodeLaunchDirective,
|
||||
PLAN_BRANCH_PREFIX,
|
||||
planBranchName,
|
||||
WORK_BRANCH_PREFIX,
|
||||
workBranchName,
|
||||
WORKTREE_TERMINAL_ENV,
|
||||
} from '../../scripts/lib/worktree.mjs'
|
||||
|
||||
|
|
@ -152,10 +154,16 @@ describe('opencodeLaunchDirective (terminal-only opencode launch, OPS26)', () =>
|
|||
)
|
||||
})
|
||||
|
||||
it('new launches prompt-less too — "apenas conversar", no skill sent', () => {
|
||||
expect(opencodeLaunchDirective({ dir, purpose: 'new', 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 })
|
||||
expect(OPENCODE_SKILL_COMMAND_BY_PURPOSE).toEqual({ next: '/work-issue', plan: null, new: null })
|
||||
})
|
||||
|
||||
it('an unknown purpose degrades to a prompt-less launch (fail-safe direction)', () => {
|
||||
|
|
@ -165,6 +173,80 @@ describe('opencodeLaunchDirective (terminal-only opencode launch, OPS26)', () =>
|
|||
})
|
||||
})
|
||||
|
||||
describe('workBranchName (per-invocation neutral worktrees)', () => {
|
||||
it('no bag → first free sequential work/<n>', () => {
|
||||
expect(workBranchName({})).toBe('work/1')
|
||||
expect(workBranchName({ bag: '' })).toBe('work/1')
|
||||
expect(workBranchName({ bag: ' ' })).toBe('work/1')
|
||||
})
|
||||
|
||||
it('no bag → skips taken sequential names (parallel sessions)', () => {
|
||||
const taken = new Set(['work/1', 'work/2'])
|
||||
expect(workBranchName({ taken })).toBe('work/3')
|
||||
})
|
||||
|
||||
it('named bag → work/<slug> when free', () => {
|
||||
expect(workBranchName({ bag: 'Ideia solta' })).toBe('work/ideia-solta')
|
||||
})
|
||||
|
||||
it('named bag whose name is taken → suffixed -2, -3, …', () => {
|
||||
const taken = new Set(['work/ideia'])
|
||||
expect(workBranchName({ bag: 'ideia', taken })).toBe('work/ideia-2')
|
||||
|
||||
taken.add('work/ideia-2')
|
||||
expect(workBranchName({ bag: 'ideia', taken })).toBe('work/ideia-3')
|
||||
})
|
||||
|
||||
it('each invocation returns a different branch for the same bag (live name)', () => {
|
||||
const first = workBranchName({ bag: 'teste' })
|
||||
expect(workBranchName({ bag: 'teste', taken: new Set([first]) })).toBe('work/teste-2')
|
||||
})
|
||||
|
||||
it('never collides with a `next` branch nor with a `plan` branch', () => {
|
||||
const taken = new Set(['C15-fullcalendar-em-campanha-agenda', 'plans/plan-issue-agenda'])
|
||||
expect(WORK_BRANCH_PREFIX).toBe('work')
|
||||
expect(`${WORK_BRANCH_PREFIX}/`).not.toMatch(/^[A-Z][A-Za-z0-9]*-/)
|
||||
for (const bag of ['agenda', 'municipios', 'C15']) {
|
||||
const branch = workBranchName({ bag, taken })
|
||||
expect(branch).toMatch(/^work\//)
|
||||
expect(branch).not.toMatch(/^[A-Z][A-Za-z0-9]*-/)
|
||||
expect(branch).not.toMatch(/^plans\//)
|
||||
}
|
||||
})
|
||||
|
||||
it('a numeric bag shares the sequential namespace (uniform)', () => {
|
||||
expect(workBranchName({ bag: '3' })).toBe('work/3')
|
||||
})
|
||||
|
||||
it('unslugifiable bag falls back to the namespace label', () => {
|
||||
expect(workBranchName({ bag: '!!!' })).toBe('work/work')
|
||||
})
|
||||
|
||||
it('truncates long slugs within the branch budget', () => {
|
||||
const branch = workBranchName({ bag: 'palavra '.repeat(20).trim() })
|
||||
expect(branch.startsWith('work/')).toBe(true)
|
||||
expect(branch.length).toBeLessThanOrEqual(60)
|
||||
expect(branch).toBe(`work/${'palavra-'.repeat(6)}palavra`)
|
||||
})
|
||||
|
||||
it('keeps the -2 suffix inside the branch budget when the base is taken', () => {
|
||||
const bag = 'palavra '.repeat(20).trim()
|
||||
const base = workBranchName({ bag })
|
||||
expect(base.length).toBe(60)
|
||||
const suffixed = workBranchName({ bag, taken: new Set([base]) })
|
||||
expect(suffixed.length).toBe(60)
|
||||
expect(suffixed.endsWith('-2')).toBe(true)
|
||||
expect(suffixed).toBe(`work/${'palavra-'.repeat(6)}palav-2`)
|
||||
})
|
||||
|
||||
it('planBranchName keeps its own namespace contract untouched', () => {
|
||||
expect(planBranchName({})).toBe('plans/plan-issue-1')
|
||||
expect(planBranchName({ bag: 'agenda', taken: new Set(['plans/plan-issue-agenda']) })).toBe(
|
||||
'plans/plan-issue-agenda-2',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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' })
|
||||
|
|
|
|||
Loading…
Reference in a new issue