mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
fix(worktree): plan cria um worktree diferente por invocação (paralelismo /plan-issue)
Cada 'pnpm worktree plan' agora deriva um branch próprio: plans/plan-issue-<bag> quando um bag é passado (sufixo -2/-3 em colisão) ou o próximo sequencial plans/plan-issue-<n> quando não. O prefixo minúsculo plans/… nunca colide com o branch <Code>-<slug> de 'next'. planBranchName (scripts/lib/worktree.mjs) unit-testado; docs e shell/command atualizados.
This commit is contained in:
parent
819a015911
commit
6c8c466b40
8 changed files with 173 additions and 61 deletions
|
|
@ -11,9 +11,11 @@
|
|||
# Uso (terminal interativo):
|
||||
# worktree next [--go] cria/reutiliza o worktree da próxima Issue claimável;
|
||||
# com --go, cd para dentro dele
|
||||
# worktree plan [--go] cria/reutiliza o worktree de planejamento do /plan-issue
|
||||
# (branch fixo plans/plan-issue, sem vínculo com a fila);
|
||||
# com --go, cd para dentro dele
|
||||
# worktree plan [bag] [--go] cria um worktree de planejamento do /plan-issue
|
||||
# DIFERENTE a cada chamada (sessões paralelas): com bag,
|
||||
# branch plans/plan-issue-<bag> (sufixo -2/-3 se o nome
|
||||
# já existir), sem bag o próximo plans/plan-issue-<n>
|
||||
# sequencial; com --go, cd para dentro dele
|
||||
# worktree kill [--force]
|
||||
#
|
||||
# Read-only no GitHub — claim continua sendo `pnpm agent:claim`.
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ A lógica determinística (fila → código+slug → branch → worktree) vive e
|
|||
|
||||
```bash
|
||||
pnpm worktree next [--go] [--no-migrate] # cria worktree de origin/main + provisiona o ambiente
|
||||
pnpm worktree plan [--go] [--no-migrate] # worktree de PLANEJAMENTO (/plan-issue), branch fixo plans/plan-issue
|
||||
pnpm worktree plan [bag] [--go] [--no-migrate] # worktree de PLANEJAMENTO (/plan-issue): 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** (branch fixo `plans/plan-issue`) para rodar a skill `/plan-issue` sem ocupar o main. Não é nomeado por Issue nenhuma — proposital, para um `next` posterior da próxima Issue nunca colidir em branch nem em slot. 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). O fluxo desta skill (fila → próxima Issue → implementação) é só `next`.
|
||||
|
||||
No terminal interativo, para `--go` trocar de diretório de verdade, use a função `worktree()` de **`.agents/shell/worktree.sh`** (uma linha de `source` no profile): o script imprime `cd <dir>` e a função o aplica no shell que te chamou — node não consegue mudar o cwd do shell pai.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
description: Cria o worktree da próxima Issue claimável (next [--go]), o worktree de planejamento do /plan-issue (plan [--go]) ou destrói o worktree atual (kill [--force])
|
||||
description: Cria o worktree da próxima Issue claimável (next [--go]), um worktree novo de planejamento do /plan-issue (plan [bag] [--go]) 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 [--go]`: cria (ou reutiliza) o worktree da próxima Issue claimável. Com `--go`, o script imprime `cd <dir>` na última linha — aplique-o na sessão de shell atual (`cd <dir>`) e reporte o novo diretório.
|
||||
- `plan [--go]`: cria (ou reutiliza) o worktree de planejamento (branch fixo `plans/plan-issue`, sem vínculo com a fila) para rodar `/plan-issue` sem ocupar o main; nunca conflita com `next`.
|
||||
- `plan [bag] [--go]`: 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`.
|
||||
- `kill [--force]`: destrói o worktree em que você está.
|
||||
|
||||
!`node scripts/worktree.mjs $ARGUMENTS`
|
||||
|
|
|
|||
|
|
@ -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):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--go]` 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. `worktree plan [--go]` (2026-08-08) is the sibling for the `/plan-issue` skill: same provisioning on the fixed branch `plans/plan-issue` (`PLAN_WORKTREE_BRANCH`, `scripts/lib/worktree.mjs`), deliberately **not** named after any Issue 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):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--go]` 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. `worktree plan [bag] [--go]` (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.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
---
|
||||
|
||||
**Recently resolved (2026-08-08):** **`pnpm worktree plan [--go] [--no-migrate]`** — worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main: branch fixo `plans/plan-issue` (`PLAN_WORKTREE_BRANCH`, `scripts/lib/worktree.mjs`), deliberadamente **não** nomeado por Issue nenhuma — um `next` posterior da próxima Issue nunca colide em branch nem em slot (slot do plano = hash do branch, registrado via marcador e bumpado pelos vivos). Mesmo provisionamento isolado do `next` (porta `3100+slot`, bancos `teqo_wt<slot>`/`teqo_wt<slot>_test`, migrations; sem Docker degrada ao compartilhado). `kill` cobre; comment das envs distingue `gerado por pnpm worktree plan`. Sem migration. Constant unit-testada.
|
||||
**Recently resolved (2026-08-08):** **`pnpm worktree plan [bag] [--go] [--no-migrate]`** — worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main: **cada invocação cria UM DIFERENTE**, para sessões de planejamento paralelas — com `bag`, branch `plans/plan-issue-<bag>` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag`, o próximo sequencial `plans/plan-issue-<n>` livre (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`). Prefixo minúsculo `plans/…` nunca colide com um `next` posterior em branch nem em slot (slot do plano = hash do branch, registrado via marcador e bumpado pelos vivos). Mesmo provisionamento isolado do `next` (porta `3100+slot`, bancos `teqo_wt<slot>`/`teqo_wt<slot>_test`, migrations; sem Docker degrada ao compartilhado). `kill` cobre; comment das envs distingue `gerado por pnpm worktree plan`. Sem migration. Naming unit-testada.
|
||||
|
||||
**Recently resolved (2026-08-08):** **B168** (Issue #422) — desfeito o **encadeamento automático** das ações rápidas do wizard (B98 era o precedente que este item inverte; evidência de campo: o coordenador rejeitou a tela avançando sozinha). Ao salvar **qualquer** ação de ajuste (votos, tendência, atualização, liderança), o usuário volta à origem (`from` allowlist B110 mantido; Início sem `from`) com toast — nunca avança para outro wizard. Removidos a fila/pipeline `wizardActionChain.ts` (fundido `wizardReturnHref` + `wizardPreviousHref` só com passos internos em `campaignActionRoutes.ts`), o param `entry`, os skip resolvers/`Pular` (`WizardSkipTrailing`/`WizardTrendSkipTrailing` deletados, chrome mobile sempre “X”) e o prefill de nota sem produtor (`notePrefill`/`voteFrom`/`voteTo`). Tendência: pós-save o RSC refresha com status já aplicado e o **client** é o único dono da navegação (toast + retorno) — o redirect servidor engolia o toast (B97 corrigido no caminho). Liderança: botão flutuante pós-save passou de “Continuar” (avançava) para **“Concluir”** (retorna à origem). Sem migration. Intenção [`desfazer-encadeamento-acoes-wizard.md`](plans/desfazer-encadeamento-acoes-wizard.md); impl [`desfazer-encadeamento-acoes-wizard-impl.md`](plans/desfazer-encadeamento-acoes-wizard-impl.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@
|
|||
import { slugify } from '../../src/lib/slug.ts'
|
||||
|
||||
/**
|
||||
* Generic branch of the /plan-issue planning worktree (`pnpm worktree plan`).
|
||||
* Deliberately NOT tied to any Issue: it must never collide with the
|
||||
* `<code>-<slug>` branch `worktree next` derives for the next claimable Issue.
|
||||
* Prefix of every `/plan-issue` planning-worktree branch (`pnpm worktree
|
||||
* plan`). Lowercase-led `plans/…`, so it can never collide with a `next`
|
||||
* branch — `<Code>-<slug>` is always uppercase-led — neither in branch name
|
||||
* nor, structurally, in the port/slot space derived from the branch.
|
||||
*/
|
||||
export const PLAN_WORKTREE_BRANCH = 'plans/plan-issue'
|
||||
export const PLAN_BRANCH_PREFIX = 'plans/plan-issue'
|
||||
|
||||
/** Total branch-name budget for plan branches — mirrors `branchNameForIssue`. */
|
||||
const PLAN_BRANCH_MAX_LENGTH = 60
|
||||
|
||||
/** Strip the leading `<code> — ` (or any dash variant) off a title. */
|
||||
const stripCodePrefix = (title, code) => {
|
||||
|
|
@ -49,3 +53,39 @@ export const branchNameForIssue = (issue, maxLength = 60) => {
|
|||
const keep = Math.max(1, maxLength - code.length - 1)
|
||||
return `${code}-${slug.slice(0, keep)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const planBranchName = ({ bag = '', taken = new Set() }) => {
|
||||
const hasBag = typeof bag === 'string' && bag.trim().length > 0
|
||||
|
||||
if (!hasBag) {
|
||||
for (let n = 1; ; n += 1) {
|
||||
const candidate = `${PLAN_BRANCH_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,
|
||||
)}`
|
||||
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}`
|
||||
if (!taken.has(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,17 @@
|
|||
* paralelo não disputarem porta 3000 nem o
|
||||
* `teqo_test` compartilhado. `--no-migrate` pula a
|
||||
* aplicação das migrations nos bancos novos.
|
||||
* pnpm worktree plan [--go] [--no-migrate]
|
||||
* cria/reutiliza o worktree de PLANEJAMENTO
|
||||
* (branch fixo `plans/plan-issue`) para rodar a
|
||||
* skill /plan-issue sem ocupar o main; NÃO é
|
||||
* nomeado nem derivado de Issue nenhuma, então um
|
||||
* `next` posterior da próxima Issue nunca colide
|
||||
* (nem no branch, nem no slot). Mesmo provisionamento
|
||||
* isolado do `next`.
|
||||
* pnpm worktree plan [bag] [--go] [--no-migrate]
|
||||
* cria um worktree de PLANEJAMENTO novo para rodar
|
||||
* a skill /plan-issue sem ocupar o main — cada
|
||||
* invocação cria UM DIFERENTE, para sessões de
|
||||
* planejamento paralelas: com `bag`, branch
|
||||
* `plans/plan-issue-<bag>` (e `-2`, `-3`, … se o
|
||||
* nome já estiver vivo); sem `bag`, o próximo
|
||||
* 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`.
|
||||
* 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)
|
||||
|
|
@ -49,7 +52,7 @@ import {
|
|||
isGeneratedDatabaseName,
|
||||
worktreeEnvironment,
|
||||
} from './lib/worktree-env.mjs'
|
||||
import { branchNameForIssue, PLAN_WORKTREE_BRANCH } from './lib/worktree.mjs'
|
||||
import { branchNameForIssue, planBranchName } from './lib/worktree.mjs'
|
||||
|
||||
const die = dieAgent('worktree')
|
||||
const WORKTREES_ROOT = join(homedir(), '.cursor', 'worktrees', 'teqo')
|
||||
|
|
@ -335,37 +338,60 @@ const cmdNext = async (go, skipMigrate) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* `plan` — the /plan-issue planning worktree. Deliberately NOT tied to the
|
||||
* claim queue and NOT named after any Issue: `PLAN_WORKTREE_BRANCH` is a fixed
|
||||
* generic branch, so a later `pnpm worktree next` for the next claimable Issue
|
||||
* never collides with it (branch name nor slot — the plan env registers its
|
||||
* hashed slot via the marker and `next` bumps around it). Same isolated env
|
||||
* provisioning as `next`.
|
||||
* `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 (go, skipMigrate) => {
|
||||
const branch = PLAN_WORKTREE_BRANCH
|
||||
const cmdPlan = async (go, skipMigrate, bag) => {
|
||||
git(['fetch', 'origin'])
|
||||
|
||||
const entries = parseWorktreeList(git(['worktree', 'list', '--porcelain']))
|
||||
|
||||
const taken = new Set()
|
||||
for (const scope of [
|
||||
(
|
||||
git(['for-each-ref', '--format=%(refname:short)', 'refs/heads'], {
|
||||
okIfFails: true,
|
||||
}) ?? ''
|
||||
).split('\n'),
|
||||
(
|
||||
git(['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin'], {
|
||||
okIfFails: true,
|
||||
}) ?? ''
|
||||
)
|
||||
.split('\n')
|
||||
.map((name) => name.replace(/^origin\//, '')),
|
||||
]) {
|
||||
for (const name of scope) if (name) taken.add(name)
|
||||
}
|
||||
|
||||
const branch = planBranchName({ bag, taken })
|
||||
if (git(['check-ref-format', '--allow-onelevel', branch], { okIfFails: true }) === null) {
|
||||
die(`Branch plan inválido para refname: ${branch}`)
|
||||
}
|
||||
const dir = join(WORKTREES_ROOT, branch)
|
||||
|
||||
git(['fetch', 'origin'])
|
||||
|
||||
const entries = parseWorktreeList(git(['worktree', 'list', '--porcelain']))
|
||||
if (entries.some((entry) => resolve(entry.path) === resolve(dir))) {
|
||||
console.log(`Worktree de planejamento já existe em ${dir} — reutilizando, sem duplicar.`)
|
||||
} else {
|
||||
const branchExists =
|
||||
git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { okIfFails: true }) !== null
|
||||
const flag = branchExists ? '-B' : '-b'
|
||||
git(['worktree', 'add', flag, branch, dir, 'origin/main'])
|
||||
|
||||
console.log('Worktree de planejamento criado:')
|
||||
console.log(` branch: ${branch}`)
|
||||
console.log(` path: ${dir}`)
|
||||
console.log(' origem: origin/main')
|
||||
die(
|
||||
`Já existe um worktree de planejamento 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(` branch: ${branch}`)
|
||||
console.log(` path: ${dir}`)
|
||||
console.log(' origem: origin/main')
|
||||
|
||||
const mainRoot = entries[0]?.path
|
||||
const env = worktreeEnvironment({
|
||||
branch,
|
||||
|
|
@ -467,19 +493,20 @@ const subcommand = positional[0]
|
|||
|
||||
if (!subcommand) {
|
||||
console.log(
|
||||
'Uso: pnpm worktree next [--go] [--no-migrate] | plan [--go] [--no-migrate] | kill [--force]',
|
||||
'Uso: pnpm worktree next [--go] [--no-migrate] | plan [bag] [--go] [--no-migrate] | kill [--force]',
|
||||
)
|
||||
console.log(' next [--go] [--no-migrate]')
|
||||
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(' com --go imprime `cd <dir>` no fim (quem aplica o cd: opencode command, ou a')
|
||||
console.log(' função `worktree()` de .agents/shell/worktree.sh); --no-migrate pula migrations')
|
||||
console.log(' plan [--go] [--no-migrate]')
|
||||
console.log(`\n plan [bag] [--go] [--no-migrate]`)
|
||||
console.log(
|
||||
' cria/reutiliza o worktree de planejamento (branch plans/plan-issue) para rodar a',
|
||||
' cria um worktree de planejamento DIFERENTE a cada invocação (sessões /plan-issue',
|
||||
)
|
||||
console.log(' skill /plan-issue sem ocupar o main nem travar na fila; provisiona o mesmo')
|
||||
console.log(' ambiente isolado de `next`; nunca colide com o branch <code>-<slug> de `next`')
|
||||
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(' kill [--force] destrói o worktree em que você está (recusa sujo sem --force) e')
|
||||
console.log(' remove os bancos gerados do worktree (best-effort)')
|
||||
process.exit(1)
|
||||
|
|
@ -487,7 +514,8 @@ if (!subcommand) {
|
|||
|
||||
try {
|
||||
if (subcommand === 'next') await cmdNext(Boolean(flags.go), Boolean(flags['no-migrate']))
|
||||
else if (subcommand === 'plan') await cmdPlan(Boolean(flags.go), Boolean(flags['no-migrate']))
|
||||
else if (subcommand === 'plan')
|
||||
await cmdPlan(Boolean(flags.go), Boolean(flags['no-migrate']), positional[1])
|
||||
else if (subcommand === 'kill') {
|
||||
if (flags.go) die('`--go` só faz sentido com `next`.')
|
||||
await cmdKill(Boolean(flags.force))
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import {
|
|||
import {
|
||||
branchNameForIssue,
|
||||
issueCodeAndSubject,
|
||||
PLAN_WORKTREE_BRANCH,
|
||||
PLAN_BRANCH_PREFIX,
|
||||
planBranchName,
|
||||
} from '../../scripts/lib/worktree.mjs'
|
||||
|
||||
type TestIssue = {
|
||||
|
|
@ -82,6 +83,50 @@ describe('branchNameForIssue', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('planBranchName (per-invocation planning worktrees)', () => {
|
||||
it('no bag → first free sequential plans/plan-issue-<n>', () => {
|
||||
expect(planBranchName({})).toBe('plans/plan-issue-1')
|
||||
expect(planBranchName({ bag: '' })).toBe('plans/plan-issue-1')
|
||||
expect(planBranchName({ bag: ' ' })).toBe('plans/plan-issue-1')
|
||||
})
|
||||
|
||||
it('no bag → skips taken sequential names (parallel sessions)', () => {
|
||||
const taken = new Set(['plans/plan-issue-1', 'plans/plan-issue-2'])
|
||||
expect(planBranchName({ taken })).toBe('plans/plan-issue-3')
|
||||
})
|
||||
|
||||
it('named bag → plans/plan-issue-<slug> when free', () => {
|
||||
expect(planBranchName({ bag: 'Agenda eleitoral' })).toBe('plans/plan-issue-agenda-eleitoral')
|
||||
})
|
||||
|
||||
it('named bag whose name is taken → suffixed -2, -3, …', () => {
|
||||
const taken = new Set(['plans/plan-issue-agenda'])
|
||||
expect(planBranchName({ bag: 'agenda', taken })).toBe('plans/plan-issue-agenda-2')
|
||||
|
||||
taken.add('plans/plan-issue-agenda-2')
|
||||
expect(planBranchName({ bag: 'agenda', taken })).toBe('plans/plan-issue-agenda-3')
|
||||
})
|
||||
|
||||
it('each invocation returns a different branch for the same bag (live name)', () => {
|
||||
const first = planBranchName({ bag: 'municipios' })
|
||||
const second = planBranchName({ bag: 'municipios', taken: new Set([first]) })
|
||||
expect(second).toBe(`${first}-2`)
|
||||
})
|
||||
|
||||
it('never collides with a `next` branch (uppercase-led <Code>-<slug>)', () => {
|
||||
const taken = new Set(['C15-fullcalendar-em-campanha-agenda'])
|
||||
for (const bag of ['agenda', 'municipios', 'C15']) {
|
||||
const branch = planBranchName({ bag, taken })
|
||||
expect(branch).toMatch(/^plans\/plan-issue/)
|
||||
expect(branch).not.toMatch(/^[A-Z][A-Za-z0-9]*-/)
|
||||
}
|
||||
})
|
||||
|
||||
it('a numeric bag shares the sequential namespace (uniform)', () => {
|
||||
expect(planBranchName({ bag: '3' })).toBe('plans/plan-issue-3')
|
||||
})
|
||||
})
|
||||
|
||||
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' })
|
||||
|
|
@ -130,17 +175,14 @@ describe('worktreeEnvironment (per-worktree ports and databases)', () => {
|
|||
})
|
||||
|
||||
it('plan worktree derives a stable hashed slot and never collides with `next`', () => {
|
||||
expect(PLAN_WORKTREE_BRANCH).toBe('plans/plan-issue')
|
||||
expect(PLAN_WORKTREE_BRANCH).not.toMatch(/^[A-Z][A-Za-z0-9]*-/)
|
||||
const env = worktreeEnvironment({ branch: PLAN_WORKTREE_BRANCH, code: null })
|
||||
expect(env.slot).toBe(hashSlotOf(PLAN_WORKTREE_BRANCH))
|
||||
const branch = planBranchName({ bag: 'agenda' })
|
||||
expect(branch).toBe(`${PLAN_BRANCH_PREFIX}-agenda`)
|
||||
expect(PLAN_BRANCH_PREFIX).not.toMatch(/^[A-Z][A-Za-z0-9]*-/)
|
||||
const env = worktreeEnvironment({ branch, code: null })
|
||||
expect(env.slot).toBe(hashSlotOf(branch))
|
||||
expect(numericSlotOfCode('')).toBeNull()
|
||||
expect(env).toEqual(worktreeEnvironment({ branch: PLAN_WORKTREE_BRANCH, code: null }))
|
||||
const bumped = worktreeEnvironment({
|
||||
branch: PLAN_WORKTREE_BRANCH,
|
||||
code: null,
|
||||
takenSlots: new Set([env.slot]),
|
||||
})
|
||||
expect(env).toEqual(worktreeEnvironment({ branch, code: null }))
|
||||
const bumped = worktreeEnvironment({ branch, code: null, takenSlots: new Set([env.slot]) })
|
||||
expect(bumped.slot).toBe(env.slot + 1)
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue