mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:35:21 +00:00
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.
131 lines
5.3 KiB
JavaScript
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'] })
|