From de8962457de0a9301c07136830c0620cf6804962 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 17 Aug 2026 19:32:10 -0300 Subject: [PATCH] fix(worktree): copia .opencode/secrets para o worktree provisionado MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O opencode.json referencia .opencode/secrets/penpot-token via {file:...}, mas o arquivo é gitignored e git worktree não leva ignored files — o launch do opencode no worktree morria com 'bad file reference'. provision agora copia .opencode/secrets/* do worktree principal (sem sobrescrever cópia manual). --- AGENTS.md | 2 +- scripts/worktree.mjs | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bcb64206..c4de1d23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,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. **`db:start` is idempotent (OPS37):** with `teqo-postgres-1` up and healthy it is a no-op — a plain `compose up` from another worktree would recreate the shared container and kill every live dev/e2e connection (the compose file's relative `./docker/postgres/init` bind mount makes the config hash differ per worktree); apply a real config/image change on purpose with `pnpm db:start --force-recreate`. -- **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. **`next` claims BEFORE creating the worktree (OPS33, 2026-08-10):** the same queue/order and optimistic lock as `pnpm agent:claim` (shared `claimIssue` in `scripts/lib/agent-github.mjs`); a failed claim prints the reason and stops with no orphan worktree; `--issue ` claims the directed issue (`ready`) or **reopens** an already-claimed one (`in-progress`, no re-claim, worktree reuse stays idempotent); the claim brief is printed and the output warns "já claimada — não rodar `pnpm agent:claim`" (`plan`/`new`/`kill` never touch Issues; the pool's coordinated `claimIssueForPool` is untouched). `worktree next [--issue N] [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, capped at 999 with hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt` (dev) and `teqo_wt_test` (tests) created on the shared container with the full migration chain AND the minimal seed applied (`pnpm migrate` + `pnpm db:seed:minimal` per database — OPS28 parity: CI runs the same pair before e2e, so a worktree's `pnpm test:e2e` mirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and `--no-migrate` skips both); 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 ` 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
` 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 --model deepseek/deepseek-v4-flash --auto [--prompt "/work-issue --issue "]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10; OPS33: `next` sends `/work-issue --issue ` — the claimed issue — with the prompt value quoted, and the shell tokenizes the line via xargs, quote-aware, never eval; OPS31 for `plan`): `next` opens the opencode TUI in the worktree with `/work-issue --issue ` already sent (the OPS25 command runs the whole cycle; the skill reads the rest from the Issue), `plan` opens with `/plan-issue` already sent (the TUI lands straight in the planning flow), `new` opens with no prompt ("apenas conversar", no skill — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; that stays an upstream-only feature request, annotated in the opencode proposition). 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 claim still happens), 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-` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `-`, 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/` (suffixed `-2`/`-3` on collision) or the next free sequential `work/` (`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__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 — and still runs migrate + seed mínimo on those shared databases (same parity; the seed re-pins campaignGoals and 5 municipalities there, overwriting manual tweaks). +- **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. **`next` claims BEFORE creating the worktree (OPS33, 2026-08-10):** the same queue/order and optimistic lock as `pnpm agent:claim` (shared `claimIssue` in `scripts/lib/agent-github.mjs`); a failed claim prints the reason and stops with no orphan worktree; `--issue ` claims the directed issue (`ready`) or **reopens** an already-claimed one (`in-progress`, no re-claim, worktree reuse stays idempotent); the claim brief is printed and the output warns "já claimada — não rodar `pnpm agent:claim`" (`plan`/`new`/`kill` never touch Issues; the pool's coordinated `claimIssueForPool` is untouched). `worktree next [--issue N] [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, capped at 999 with hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt` (dev) and `teqo_wt_test` (tests) created on the shared container with the full migration chain AND the minimal seed applied (`pnpm migrate` + `pnpm db:seed:minimal` per database — OPS28 parity: CI runs the same pair before e2e, so a worktree's `pnpm test:e2e` mirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and `--no-migrate` skips both); 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 — plus the gitignored `.opencode/secrets/*` (e.g. `penpot-token`, referenced by the worktree's `opencode.json` MCP config), without which opencode refuses to launch ("bad file reference" for the dangling `{file:…}`). **`next`/`plan`/`new` print `cd ` 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
` 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 --model deepseek/deepseek-v4-flash --auto [--prompt "/work-issue --issue "]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10; OPS33: `next` sends `/work-issue --issue ` — the claimed issue — with the prompt value quoted, and the shell tokenizes the line via xargs, quote-aware, never eval; OPS31 for `plan`): `next` opens the opencode TUI in the worktree with `/work-issue --issue ` already sent (the OPS25 command runs the whole cycle; the skill reads the rest from the Issue), `plan` opens with `/plan-issue` already sent (the TUI lands straight in the planning flow), `new` opens with no prompt ("apenas conversar", no skill — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; that stays an upstream-only feature request, annotated in the opencode proposition). 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 claim still happens), 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-` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `-`, 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/` (suffixed `-2`/`-3` on collision) or the next free sequential `work/` (`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__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 — and still runs migrate + seed mínimo on those shared databases (same parity; the seed re-pins campaignGoals and 5 municipalities there, overwriting manual tweaks). - 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. diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index 9b077f82..89b0dfed 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -43,6 +43,11 @@ * mínimo (`db:seed:minimal`, OPS28) são aplicados * aos dois bancos — paridade com a CI, que roda * `migrate → seed:minimal` antes do e2e. + * Também copia do worktree principal os secretos + * gitignored de `.opencode/secrets/` (ex. + * `penpot-token` do MCP Penpot) — git não leva + * ignored files para o worktree e o opencode se + * recusa a abrir com `{file:…}` pendurado. * `--no-migrate` pula migrations E o seed (que * depende do catálogo migrado). * pnpm worktree plan [bag] [--stay] [--no-migrate] @@ -93,7 +98,15 @@ import { execFileSync } from 'node:child_process' import { randomBytes } from 'node:crypto' -import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + statSync, + writeFileSync, +} from 'node:fs' import { homedir } from 'node:os' import { join, resolve } from 'node:path' @@ -220,6 +233,28 @@ const envFileIsGenerated = (dir, name) => { } } +/** + * Copies the main repo's gitignored `.opencode/secrets/*` into the worktree — + * e.g. `penpot-token`, referenced by the root `opencode.json` MCP config via + * `{file:…}`. Git worktrees don't carry ignored files, and opencode refuses to + * start with a dangling `{file:…}` reference ("bad file reference"), so the + * TUI launch directive would fail inside a fresh worktree. Never overwrites an + * existing worktree file (a manual copy wins). + */ +const copyOpendevSecrets = (mainRoot, dir) => { + const sourceDir = join(mainRoot, '.opencode', 'secrets') + if (!existsSync(sourceDir)) return + const targetDir = join(dir, '.opencode', 'secrets') + mkdirSync(targetDir, { recursive: true }) + for (const name of readdirSync(sourceDir)) { + const source = join(sourceDir, name) + const target = join(targetDir, name) + if (!statSync(source).isFile() || existsSync(target)) continue + copyFileSync(source, target) + console.log(`[worktree] .opencode/secrets/${name} copiado do worktree principal.`) + } +} + /** `CREATE DATABASE` for each missing generated name on the shared container. */ const ensureDatabases = async (names) => { const client = new pg.Client({ @@ -305,6 +340,10 @@ const writeFallbackEnv = (dir, branch) => { * 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' }) => { + // Secreto gitignored que o opencode.json do worktree referencia (`{file:…}`) + // — sem ele o opencode se recusa a abrir; a cópia vale para todos os caminhos. + copyOpendevSecrets(mainRoot, dir) + const manual = (existsSync(join(dir, '.env.local')) && !envFileIsGenerated(dir, '.env.local')) || (existsSync(join(dir, '.env.test.local')) && !envFileIsGenerated(dir, '.env.test.local'))