Repository navigation
Pipeline Roles Triage
Draft from integration c1797bd (2026-10-16). It describes today's code; adjust it as the split settles. Index: Pipeline roles.
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.
-
Stage:
triage. The role lives insrc/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) andscript.ts(its steps and end), registered insrc/roles/registry.ts(#304).src/workflow/stages/triage.tsis a thin adapter over the same modules for workflow.js. -
Ops:
ask(src/ops/ask.ts). The sweep's triage also sendsstart-pr(src/ops/pr.ts); on the role runnerstart-prhas 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 withTRIAGE_SCHEMA. It has read-only tools. -
Job role:
reader.
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:
-
questionsis required even when unused, so every decision butneeds-infosends[]. -
needs-infowith an emptyquestionsis read asreject, not as a malformed reply. -
splitfrom a sub-issue is read asreject, so the schema allows a decision the stage then refuses.
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.
-
Claim the issue (
claim). A wrong stage, a stop label, or an author who isn't allowed releases it untouched. -
Check out the base read-only, or the prerequisite's branch when stacked, and the wiki at its base.
-
Write inputs:
issue.md(allowed users' text only), plusdepth,triagesand an earliersplitSuggested. -
Session: triage, read-only, replying with
TRIAGE_SCHEMA. -
Read the reply (
src/roles/triage/reply.ts,decisionOf). A depth-1split, orneeds-infowith no questions, reads asreject. -
Act:
-
ready:announceposts the taking comment, and the end saves the state atspecwith the plan, leavingpras it was. It opens no PR: the spec writer does (#334). The sweep's triage still sendsstart-pr; -
needs-info: the end releases the issue, and itsafterop runsaskon the saved state; -
split:announceposts the taking comment, and the end saves the state atspecwithsplitSuggested; -
reject:announceposts the reason, and the end'safterop runsstop.
announceposts its comment keyedtriage(<!-- issue-pipeline:<n>:triage -->), so a rerun edits it rather than posting it again. -
-
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.
- An issue the sweep claimed at stage
triage: new, answered afteragent:needs-info, or restarted after a stop with no implementation (implemented: false) or atoo-largestop. -
issue.mdin 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),splitSuggestedfrom an earlier run. - On a restart after
too-large: the implementer'shandoff.mdand the commits onagent/issue-<n>.
-
Ready: the state at
specwithplanand a signed "Taking this issue" comment.splitSuggestedcleared. On the role runner no draft PR (spec-writer-opens-pr, #334); the sweep's triage also saves the draft PR (pr,headSha) fromstart-pr. -
Split suggested: the state at
specwithplanandsplitSuggestedset to the reason, and a comment saying a spec follows that may split it. -
Needs info: label
agent:needs-infoand 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-rejectedand the reason. -
Other stops:
agent:stopped-triage-limitortriage failed.
- Each run counts toward
triages. AftermaxTriages(3) runs on one issue, triage stops it withagent:stopped-triage-limitand 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 withtriage 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).readymeans two implementer sessions can implement and test it;splitmeans it needs more than two. Before #413 the bound was one session, atsessionTokenCap. - A sub-issue's (depth 1) task says splitting is not allowed. A
splitreply from it reads asreject. -
ready:- moves the issue to
spec, keepsplanclipped to 4,000 characters, and clearssplitSuggested; - posts a signed comment that it is taking the issue, with the reason;
- in the sweep, runs
start-prwhen the issue has no PR yet: an empty commit "Start work on #N" onagent/issue-<n>, and a draft PR titled " (#N)" whose body starts with "Closes #N" and carries the plan. A failedstart-pris only logged, andopen-prcreates 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).
- moves the issue to
-
split(depth 0) moves the issue tospecwith the reason insplitSuggested, logstriage: split-suggested - <reason>, and opens no draft PR. Triage opens no sub-issues. -
needs-infoposts the questions throughask, labelsagent:needs-info, and closes the issue's claims. With no questions it reads asreject. A failedaskaborts the sweep. -
rejectposts the reason and stops the issue withagent: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
allowedUsersand are still a collaborator, by the collaborators API; - a config without
allowedUsersis 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.
- a user is trusted only if they are on the config's
- 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
splitop never closes one. Proposed: these rules move to the spec writer withstart-pr. - Triage opens no sub-issues because it hasn't read enough of the code to cut along file boundaries. The spec writer has.
- Every requirement above has a test at one of the interfaces below, and
mise //apps/issue-pipeline:checkpasses. - 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
specwithplanset,agent:needs-infowith 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
triagesection describes the role as built, andlinked-draft-prsnames the spec writer as the role that opens the PR.
-
Step machine:
nextfromstages/triage.tswithworkflow-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:
runWorkflowwith an issue claimed attriage, scripting the triage session and theaskreply (andstart-prtoday). Read the outcome withopsRequests,requestForandreleasedState. -
Ops CLI:
runOpsforask(andstart-pr, until it moves) againstops-test-kit.mjsand the fakegh. 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:
runJobwith--stage triagefor the reader boundary: no gh token in pi's env and no push on exit. -
Role runner:
triage-role-test-kit.mjsoverrunner-test-kit.mjs: the role's run with the mock session, fake ops and the fakegh, killed before or after any effect call and started again.
| 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 |
- 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?
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.