teqo/scripts/lib/worktree.mjs
Francisco Solla c399b918c3 feat(worktree): comando plan — worktree dedicado ao /plan-issue
Novo subcomando `pnpm worktree plan [--go] [--no-migrate]` cria/reutiliza o
worktree de planejamento no branch fixo `plans/plan-issue`
(`PLAN_WORKTREE_BRANCH`), sem vínculo com a fila de claim: um `next` posterior
da próxima Issue nunca colide em branch nem em slot (slot derivado por hash do
branch, bumpado entre worktrees vivos). Mesmo provisionamento isolado do
`next` — porta 3100+slot, bancos teqo_wt<slot>/teqo_wt<slot>_test, migrations.

- scripts/worktree.mjs: cmdPlan + provision() com purpose/issue opcionais
- scripts/lib/worktree.mjs: constante PLAN_WORKTREE_BRANCH
- tests/unit/worktree.unit.spec.ts: derivação de slot do plano
- docs: .opencode/commands/worktree.md, shell, skills, AGENTS.md, changelog
2026-08-08 16:00:27 -03:00

51 lines
2 KiB
JavaScript

/**
* Pure helpers for `scripts/worktree.mjs` — branch naming derived from the
* claim-queue issue, shared with the unit tests. Naming: `<code>-<slug>`
* where `code` is the frontmatter `id` (ex. `C15`) and `slug` is the pt-BR
* title slugified with the repo's `src/lib/slug.ts` (accents stripped,
* non-alphanumeric → `-`).
*/
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.
*/
export const PLAN_WORKTREE_BRANCH = 'plans/plan-issue'
/** Strip the leading `<code> — ` (or any dash variant) off a title. */
const stripCodePrefix = (title, code) => {
const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return title.replace(new RegExp(`^${escaped}\\s*[—–:-]?\\s*`), '')
}
/**
* `code` = frontmatter `id` (falls back to the leading `ID — ` token of the
* title); `subject` = the title with that prefix removed.
*/
export const issueCodeAndSubject = (issue) => {
const title = issue.title ?? ''
const code = typeof issue.meta?.id === 'string' && issue.meta.id.length > 0 ? issue.meta.id : null
const subject = code
? stripCodePrefix(title, code)
: title.replace(/^[A-Za-z0-9+]+\s*[—–:-]?\s*/, '')
return { code, subject }
}
/**
* `<code>-<slug>` truncated to `maxLength` total characters (always keeping
* the code). Throws when the issue has no id — fail loudly, never invent.
*/
export const branchNameForIssue = (issue, maxLength = 60) => {
const { code, subject } = issueCodeAndSubject(issue)
if (!code) {
throw new Error(`Issue sem frontmatter id: #${issue.number} ${issue.title}`)
}
const slug = slugify(subject) || 'issue'
const full = `${code}-${slug}`
if (full.length <= maxLength) return full
const keep = Math.max(1, maxLength - code.length - 1)
return `${code}-${slug.slice(0, keep)}`
}