teqo/scripts/lib/agent-pool-forgejo.mjs
Francisco Solla c90f70aab4 ops(OPS76): tracker de Issues migra do Forgejo para o GitHub — GitHub como host único
Decisão (2026-08-21): o GitHub passa a ser a fonte única — main, branches,
PRs, CI via Actions E tracker de Issues. O Forgejo deixa de ser canal ativo
(git.e issue tracker congelados pós-convergência).

Data: 17 issues migradas do Forgejo para o GitHub (frontmatter id/depends/
serializes/priority/model preservados, labels mapeados, comment de origem
em cada uma; tabela Forgejo→GitHub registrada).

Tooling: github-api.mjs estendido com a camada de issues/labels/comments/
close/workflow-dispatch/contents (mesmo shape normalizado do forgejo-api);
18 scripts trocam o transporte forgejo-api → github-api; forgejo-api.mjs e
seu teste (961 linhas) removidos; teste githubApi.unit.spec.ts novo (7 casos);
issue-transition-on-merge.mjs renomeado e reapontado.

Flips pós-merge: issue-done e plan-issue-ready agora usam o GITHUB_TOKEN com
issues:write no repo (sem FORGEJO_API_TOKEN) e fecham/flipam Issues no GitHub.

Docs: AGENTS.md, AGENT-OPS.md e skills do fluxo vivo (work-issue, plan-issue,
project-status, capture-review-debts, worktree-next-issue, agent-work-issue)
apontam o tracker para github.com/fsolla/teqo/issues.

Gates: tsc/lint/format/knip/cycles/unit 2416 verdes.
2026-08-21 08:19:06 -03:00

131 lines
5.3 KiB
JavaScript

/**
* Forgejo IO wrappers for the agent pool supervisor. Pure logic lives in
* agent-pool-state.mjs / agent-pool-eligibility.mjs; the Cursor REST client in
* agent-pool-cursor.mjs. Everything here goes through scripts/lib/forgejo-api.mjs
* — no GitHub/gh dependency, same contract the pool had against `gh`.
*
* Pool state (was: repo Actions variables) lives in `pool-state.json` on the
* dedicated branch `pool-state` (Forgejo 9 exposes variables read-only via API
* — creation is 405; the contents API gives us read/write without git).
*/
import { randomUUID } from 'node:crypto'
import { labelNames } from './agent-forgejo.mjs'
import { formatPoolEvent } from './agent-pool-state.mjs'
import { githubApi as api } from './github-api.mjs'
const POOL_STATE_FILE = 'pool-state.json'
const POOL_STATE_BRANCH = 'pool-state'
/**
* Pool variables as a plain map from `pool-state.json` on the `pool-state`
* branch. Non-strict mode degrades to `{}` + a warn: inspection commands
* (status, dry-run tick) still work and the resulting config is the disabled
* default — the fail-closed direction. The live tick stays strict: if the
* supervisor cannot read its own config it must stop loudly, never guess.
*/
export const readPoolVariables = async ({ strict = true } = {}) => {
try {
const { content } = await api.getFileContents(POOL_STATE_FILE, POOL_STATE_BRANCH)
const parsed = JSON.parse(content)
return parsed && typeof parsed === 'object' ? parsed : {}
} catch (error) {
if (strict) throw error
console.error(
'[agent:pool] aviso: pool-state.json ilegível (branch pool-state ausente?) — usando defaults (pool desligado).',
)
return {}
}
}
/** Create or update one pool variable in pool-state.json (POST create, PUT update). */
export const writePoolVariable = async (name, value) => {
const current = await readPoolVariables()
const next = { ...current, [name]: value }
const message = `[agent:pool] ${name}=${value}`
try {
await api.updateFile(POOL_STATE_FILE, {
message,
content: JSON.stringify(next, null, 2) + '\n',
branch: POOL_STATE_BRANCH,
sha: (await api.getFileContents(POOL_STATE_FILE, POOL_STATE_BRANCH)).sha,
})
} catch (error) {
if (/404|object does not exist/i.test(error.message)) {
await api.createFile(POOL_STATE_FILE, {
message,
content: JSON.stringify(next, null, 2) + '\n',
branch: POOL_STATE_BRANCH,
})
} else {
throw error
}
}
}
export const listInProgressIssues = () =>
api.listIssues({ state: 'open', labels: 'in-progress', limit: 200 })
export const listIssueComments = (number) => api.listIssueComments(number)
export const listOpenPrs = () => api.listPullRequests({ state: 'open', limit: 100 })
/** The open PR whose body closes this issue (`Closes #N` / `Fixes #N`), if any. */
export const prClosingIssue = (prs, issueNumber) =>
prs.find((pr) => new RegExp(`(?:closes|fixes) #${issueNumber}\\b`, 'i').test(pr.body ?? '')) ??
null
// Path predicate for "schema-touching PR" — the pool serializes migration
// spawns while one is open (the CI migration-lock was removed 2026-08-12).
const SCHEMA_PR_PATH = /^(src\/migrations\/|src\/payload-types\.ts$|payload-types\.ts$)/
export const countOpenSchemaPrs = async () => {
const prs = await api.listPullRequests({ state: 'open', limit: 100 })
let count = 0
for (const pr of prs) {
const files = await api.listPullRequestFiles(pr.number).catch(() => null)
if ((files ?? []).some((file) => SCHEMA_PR_PATH.test(file.filename ?? ''))) count += 1
}
return count
}
/** Comment on an issue with human text plus the machine-readable pool marker. */
export const commentPoolEvent = (number, event, text) =>
api.addComment(number, `${text} ${formatPoolEvent(event)}`)
/**
* Coordinated claim (plan §4): the supervisor is the single allocator. Re-read
* the issue (same optimistic lock as agent:claim), flip ready→in-progress and
* leave the claim marker with the worker UUID the spawn will reuse as the
* idempotent `agentId`. Returns `{ ok, workerUuid }` or `{ ok: false }` when
* the race was lost — the tick just moves to the next queue entry.
*/
export const claimIssueForPool = async (number, { tickIso }) => {
const fresh = await api.getIssue(number)
const labels = labelNames(fresh)
if (fresh.state !== 'OPEN' || !labels.includes('ready') || labels.includes('in-progress')) {
return { ok: false, reason: 'lost-race' }
}
await api.setLabels(number, { add: ['in-progress'], remove: ['ready'] })
const workerUuid = randomUUID()
await commentPoolEvent(
number,
{ event: 'claim', tick: tickIso, worker: workerUuid },
`Claimed pelo pool-supervisor (tick ${tickIso}) — worker em spawn.`,
)
return { ok: true, workerUuid }
}
/** Undo the claim when the spawn itself failed — the issue goes back to ready. */
export const rollbackPoolClaim = async (number, note) => {
await api.setLabels(number, { add: ['ready'], remove: ['in-progress'] })
return api.addComment(number, `Pool-supervisor: claim revertido — ${note}`)
}
/**
* Failure path: a terminally failed worker's issue leaves the claim queue for
* human triage. The pool only ever blocks issues IT claimed (claim marker).
*/
export const blockIssueFromPool = (number) =>
api.setLabels(number, { add: ['blocked'], remove: ['in-progress'] })