Pipeline Roles Triage - axolotl-logic/project-buddy GitHub Wiki

Triage role: spec (draft)

Draft from integration c1797bd (2026-10-16). It describes today's code; adjust it as the split settles. Index: Pipeline roles.

Purpose

Triage decides whether the pipeline takes an issue on, before anyone reads the code closely. It answers one of four ways: take it (ready), ask its author questions (needs-info), take it but suggest the spec writer cut it into parts (split), or turn it down (reject). On ready it hands the spec writer short notes. On the role runner it opens no branch or PR, since the spec writer opens them (#334); the sweep's triage still opens the issue's draft PR. It never edits code, never writes a spec and never opens sub-issues.

Scope today

  • Stage: triage. The role lives in src/roles/triage/: reply.ts (the reply and its decision), task.ts (the task, comments, stop reasons and plan limit), locate.ts (where a run starts) and script.ts (its steps and end), registered in src/roles/registry.ts (#304). src/workflow/stages/triage.ts is a thin adapter over the same modules for workflow.js.
  • Ops: ask (src/ops/ask.ts). The sweep's triage also sends start-pr (src/ops/pr.ts); on the role runner start-pr has moved to the spec writer, and triage opens no branch or PR (spec-writer-opens-pr, #334).
  • Session: triage, agent issue-pipeline-reader, Opus, replies with TRIAGE_SCHEMA. It has read-only tools.
  • Job role: reader.

Session reply schema

From src/workflow/schemas.ts. The object is closed (additionalProperties: false) and every field is required. The role checks the reply by hand (src/roles/triage/reply.ts).

// TRIAGE_SCHEMA: the triage session's reply.
{
  decision: "ready" | "needs-info" | "split" | "reject";
  reason: string;      // posted in the taking, question or reject comment
  questions: string[]; // at most 5; asked when decision is "needs-info"
  plan: string;        // notes for the spec writer, clipped to 4,000 characters
}

For review:

  • questions is required even when unused, so every decision but needs-info sends [].
  • needs-info with an empty questions is read as reject, not as a malformed reply.
  • split from a sub-issue is read as reject, so the schema allows a decision the stage then refuses.

Lifecycle (built, #304)

Triage's own steps, in order. It is a linear script, not a state machine (role-state-machines), and runs on the shared runner (src/roles/triage/script.ts). Steps 1 to 3 and 7 are the job's (claim, checkout and issue.md, then the release through the end's save); the role's steps are triage (4 and 5) and announce (6), then the end, whose after op runs ask or stop. Only step 4 runs a model. Wiki access: read, in the job's docs/, at the issue's wiki branch, or at the wiki's base when that branch doesn't exist; code access: read, in repo/ (#338). See Shared for how the wiki is provisioned.

  1. Claim the issue (claim). A wrong stage, a stop label, or an author who isn't allowed releases it untouched.

  2. Check out the base read-only, or the prerequisite's branch when stacked, and the wiki at its base.

  3. Write inputs: issue.md (allowed users' text only), plus depth, triages and an earlier splitSuggested.

  4. Session: triage, read-only, replying with TRIAGE_SCHEMA.

  5. Read the reply (src/roles/triage/reply.ts, decisionOf). A depth-1 split, or needs-info with no questions, reads as reject.

  6. Act:

    • ready: announce posts the taking comment, and the end saves the state at spec with the plan, leaving pr as it was. It opens no PR: the spec writer does (#334). The sweep's triage still sends start-pr;
    • needs-info: the end releases the issue, and its after op runs ask on the saved state;
    • split: announce posts the taking comment, and the end saves the state at spec with splitSuggested;
    • reject: announce posts the reason, and the end's after op runs stop.

    announce posts its comment keyed triage (<!-- issue-pipeline:<n>:triage -->), so a rerun edits it rather than posting it again.

  7. Release the lease, and remove the checkout.

Locate (built, #304: every-role-resumes). Every start runs triage again from its first step (src/roles/triage/locate.ts), with the run counted and its session charged, or a stop when maxTriages or the budget rules the session out (Shared). The session reruns; the taking and rejection comments are updated in place by their marker, and labels are set as a set. The job refuses --stage triage --resume before it claims anything, and the orchestrator restarts a dead triage job without --resume.

No work (proposed: no-work-skips-stages). The claim in step 1 already releases an issue that is closed, carries a stop label, or is at another stage, without a session. Triage's reject covers an issue the pipeline shouldn't take on. Proposed: when the issue is already done on the base, triage still replies ready, and the spec writer, who reads the code, says no-work.

Inputs

  • An issue the sweep claimed at stage triage: new, answered after agent:needs-info, or restarted after a stop with no implementation (implemented: false) or a too-large stop.
  • issue.md in the run dir, holding the issue body and only allowed users' comments.
  • The checkout at the base branch and the wiki checkout, read-only.
  • depth (0 for an issue, 1 for a sub-issue), triages (runs so far), splitSuggested from an earlier run.
  • On a restart after too-large: the implementer's handoff.md and the commits on agent/issue-<n>.

Outputs

  • Ready: the state at spec with plan and a signed "Taking this issue" comment. splitSuggested cleared. On the role runner no draft PR (spec-writer-opens-pr, #334); the sweep's triage also saves the draft PR (pr, headSha) from start-pr.
  • Split suggested: the state at spec with plan and splitSuggested set to the reason, and a comment saying a spec follows that may split it.
  • Needs info: label agent:needs-info and a comment with up to 5 numbered questions. The issue leaves the pipeline until an allowed user answers.
  • Reject: a stop with agent:stopped-triage-rejected and the reason.
  • Other stops: agent:stopped-triage-limit or triage failed.

Requirements

  • Each run counts toward triages. After maxTriages (3) runs on one issue, triage stops it with agent:stopped-triage-limit and launches nothing.
  • One read-only session reads the issue and as much of the repo and wiki as it needs, and returns TRIAGE_SCHEMA. A session with no structured output, or one that doesn't fit the schema, stops the issue with triage failed.
  • On a restart, the task tells the session to read an earlier implementer's handoff and the branch's commits before deciding.
  • The task sizes an issue in implementer sessions, with the bound in tokens: twice contextLimits.soft (350,000 by default), read from the config, not a fixed number (#413, roles-6-10-scope). ready means two implementer sessions can implement and test it; split means it needs more than two. Before #413 the bound was one session, at sessionTokenCap.
  • A sub-issue's (depth 1) task says splitting is not allowed. A split reply from it reads as reject.
  • ready:
    • moves the issue to spec, keeps plan clipped to 4,000 characters, and clears splitSuggested;
    • posts a signed comment that it is taking the issue, with the reason;
    • in the sweep, runs start-pr when the issue has no PR yet: an empty commit "Start work on #N" on agent/issue-<n>, and a draft PR titled " (#N)" whose body starts with "Closes #N" and carries the plan. A failed start-pr is only logged, and open-pr creates the PR later.
    • On the role runner (spec-writer-opens-pr, #334): triage creates no branch and opens no PR. The spec writer does both once it has a spec (see Spec writer, Branch and PR).
  • split (depth 0) moves the issue to spec with the reason in splitSuggested, logs triage: split-suggested - <reason>, and opens no draft PR. Triage opens no sub-issues.
  • needs-info posts the questions through ask, labels agent:needs-info, and closes the issue's claims. With no questions it reads as reject. A failed ask aborts the sweep.
  • reject posts the reason and stops the issue with agent:stopped-triage-rejected.
  • Triage never rejects a change for the kind of file it touches (CI workflows, auth, dependencies, migrations).
  • Only issues and comments from allowed users reach triage:
    • a user is trusted only if they are on the config's allowedUsers and are still a collaborator, by the collaborators API;
    • a config without allowedUsers is refused, so a new repo never trusts everyone;
    • an issue from anyone else is skipped, including when it is run by number;
    • comments from anyone else are left out of issue.md, with a note giving how many were withheld;
    • the pipeline's own account is always allowed, so the sub-issues it opens pass the same check.
  • Only an allowed user's reply after the questions counts as an answer.
  • Triage has no harm check: harm is the security review's job, so triage never rejects for it, nor because an issue needs secrets.
  • Today: the draft PR's "Closes #N" puts it under the issue's Development section, and a suggested split gets no draft PR because the split op never closes one. Proposed: these rules move to the spec writer with start-pr.
  • Triage opens no sub-issues because it hasn't read enough of the code to cut along file boundaries. The spec writer has.

Done looks like

  • Every requirement above has a test at one of the interfaces below, and mise //apps/issue-pipeline:check passes.
  • The role's stage, reply reader and prompt live in modules the other roles don't import, apart from the shared parts the index lists.
  • A triage run ends in exactly one of: the issue at spec with plan set, agent:needs-info with questions posted, or a stop with its label. It never launches a writer session or opens a sub-issue.
  • An issue that leaves the role runner's triage has no branch and no PR from triage (spec-writer-opens-pr, #334).
  • The wiki's triage section describes the role as built, and linked-draft-prs names the spec writer as the role that opens the PR.

Test interfaces

  • Step machine: next from stages/triage.ts with workflow-triage-test-kit.mjs (stateWith, contextWith, replyOf, SPLIT_ALLOWED, SPLIT_FORBIDDEN). Assert the launch (task, schema, agent), state.st (stage, plan, splitSuggested, triages), the comments and the stop.
  • Built sweep: runWorkflow with an issue claimed at triage, scripting the triage session and the ask reply (and start-pr today). Read the outcome with opsRequests, requestFor and releasedState.
  • Ops CLI: runOps for ask (and start-pr, until it moves) against ops-test-kit.mjs and the fake gh. Assert the gh writes (comments, labels, claims closed, the draft PR's title and body).
  • Restart routing: the shared start-state rules (start-state.test.mjs, start-state-too-large.test.mjs) decide which stopped issues come back to triage.
  • Stage job: runJob with --stage triage for the reader boundary: no gh token in pi's env and no push on exit.
  • Role runner: triage-role-test-kit.mjs over runner-test-kit.mjs: the role's run with the mock session, fake ops and the fake gh, killed before or after any effect call and started again.

Test plan

Requirement Interface Case Today
Launch step machine the first call counts the run, charges it and launches the session workflow-stages-triage-next.test.mjs
Sub-issue task step machine a depth-1 task forbids splitting workflow-stages-triage-next.test.mjs
Sub-issue split reads as reject step machine, role runner, pure module a depth-1 split reply stops with triage-rejected and the reject comment; decisionOf for every decision at depths 0 and 1 roles-triage.test.mjs, roles-triage-reply.test.mjs, workflow-stages-triage-next.test.mjs
Restart step machine the task names the handoff and branch (#283) workflow-stages-triage-next.test.mjs
Ready step machine, built sweep moves to spec, posts the taking comment, sends start-pr in the sweep, none on the role runner (#334); plan clipped to 4,000; an issue with a PR gets none; a failed start-pr is only logged workflow-stages-triage-next.test.mjs, workflow-stages-triage.test.mjs
Draft PR (proposed: moves to the spec writer) ops CLI a new branch gets a draft with "(#N)", "Closes #N" and the plan; an existing PR is only labelled pr-ops.test.mjs
No PR from triage runner, fake gh a ready triage creates no branch and opens no PR roles-triage.test.mjs (#334)
Split suggested step machine, built sweep depth 0 moves to spec with splitSuggested, no draft PR and no sub-issues; a later ready clears it workflow-stages-triage-next.test.mjs, workflow-stages-triage.test.mjs
Needs info step machine, ops CLI questions are asked; none reads as reject; a failed ask aborts; ask numbers the questions, labels and closes claims workflow-stages-triage-next.test.mjs, ops-issue-ops.test.mjs
Reject step machine, built sweep the reason is posted, then the stop workflow-stages-triage-next.test.mjs, workflow-stages-triage.test.mjs
Failures and limits step machine no structured output stops; a fourth run stops with triage-limit; a run the budget can't fit stops workflow-stages-triage-next.test.mjs
Allowed input ops CLI a named issue from an author not allowed is refused ops-sweep.test.mjs
Restart routing pure module a too-large stop with work restarts at triage; stopped with no work restarts fresh start-state.test.mjs, start-state-too-large.test.mjs
Reader boundary stage job triage gives pi no gh token and never pushes; --resume is refused job-roles.test.mjs, job-boundary.test.mjs, job-role-triage.test.mjs
Role isolation unit the role's modules import nothing from the other roles' modules roles-triage.test.mjs
Kill anywhere locate, role runner, fake gh killed before and after each effect call and started again, triage ends as an uninterrupted run, with at most one triage comment and the same labels roles-triage.test.mjs

Open questions

  • Triage and spec as one session: keep triage a cheap, separate first look, or let the spec writer also reject and ask? Splitting it into its own role assumes the first.
  • Re-triage after answers: today an answer to the spec writer's questions sends the issue back through triage, which re-decides from scratch and counts toward maxTriages. Should it go straight back to the spec writer?

Sources

The requirements above take in the wiki decisions this role follows, as of wiki f1b2944 (2026-10-16): allow-listed-input, triage, linked-draft-prs. From this split, not on the wiki yet: triage-is-own-role, spec-writer-opens-pr (decisions in the owner's notes, not public). Once the specs are approved, prune-wiki-decisions deletes or trims the wiki sections the specs make redundant.

⚠️ **GitHub.com Fallback** ⚠️