Repository navigation
Agent Issue Pipeline
JNode's hands-off issue automation: receptionist triage, a per-ticket DEV/REVIEW/FEEDBACK loop, evidence-gated label and closure logic, and CI self-healing.
JNode runs a mostly autonomous pipeline from "issue opened" to "PR merged, issue closed". Five GitHub Actions workflows plus four JavaScript state machines cooperate through issue labels, hidden HTML state comments, and slash-command issue comments.
issue opened
-> auto-triage.yml posts "/oc triage issue #N" [issues: opened]
-> triage.yml Triage runs, read-only checkout, posts report ["/oc triage" comment]
-> ticket-runner.js auto-starts DEV [Triage workflow_run]
-> opencode.yml agent works the ticket, opens a PR ["/oc Please proceed…"]
-> ticket-runner.js DEV -> REVIEW, posts "/oc review" on the PR [opencode workflow_run]
-> agent review "Verdict: approve" | "Verdict: request-changes"
request-changes -> FEEDBACK -> "/oc fix Address review feedback." -> REVIEW
approve + !auto-merge-eligible -> HUMAN_REVIEW (native GitHub PR Review UI)
approve + eligible + safe diff + green CI -> MERGE -> DONE + close
-> ant.yml Java CI green re-reviews a deferred PR; red posts CI-heal
-> wiki-update.yml weekly documentation pass
orchestrator.yml / orchestrator.js is a parallel driver for batched multi-task master issues; it uses the same phases and the same gate in orchestrator-helpers.js.
| File | Role |
|---|---|
.github/workflows/auto-triage.yml |
Receptionist. Posts the triage request; re-triages on reporter replies. |
.github/workflows/triage.yml |
Triage runner. Read-only, no checkout writes, no branches, no PRs. |
.github/workflows/ticket-runner.yml |
Ticket Runner. Single job calling ticket-runner.js. |
.github/workflows/orchestrator.yml |
Task Orchestrator. Batch foreman for kind/orchestrator master issues. |
.github/workflows/opencode.yml |
The build agent itself: ISO build + QEMU, then the OpenCode agent. |
.github/scripts/ticket-runner.js |
Per-ticket state machine (DEV/REVIEW/FEEDBACK/HUMAN_REVIEW/MERGE/DONE/FAILED). |
.github/scripts/orchestrator.js |
Batch state machine over a master issue's queue/current_task. |
.github/scripts/orchestrator-helpers.js |
Shared REST helpers: trigger, verdict parsing, merge-safety gate, mergePR. |
.github/scripts/opencode-post-step.js |
Turns the agent's report into an agent/* label and (sometimes) a close. |
.github/scripts/sync-labels.js |
Label bootstrap CLI; owns the PRESERVE set. |
.github/scripts/tests/*.test.js |
105 node:test unit tests for the three state machines. |
.github/AGENTS.md |
274-line spec of the pipeline: triggers, labels, decision tree, state format. |
opencode.json |
Repo-level OpenCode config; grants the agent allow on /tmp/* external-directory access. |
.opencode/skills/jnode-triage-issue/SKILL.md |
v0.2.0 triage behaviour contract. |
.opencode/skills/jnode-issue-resolver/SKILL.md |
v0.1.0 worker behaviour contract. |
Added 2026-10-03 (a373767) so the agent can use /tmp for scratch files without a per-run prompt:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"external_directory": {
"/tmp/*": "allow"
}
}
}The pattern matters: it is "/tmp/*", so only paths under /tmp are allowed. /tmp itself and /etc are still gated. This is what makes the filesystem-debug and jnode-interact skills able to stage repro scripts and serial logs.
Three namespaces plus two bare labels. All are bootstrapped by sync-labels.js (which also PRESERVEs orchestrator-internal labels).
| Namespace | Values |
|---|---|
kind/* (blue #1d76db) |
bug, feature, investigate, wiki, review, chore, question, triage, test, plus orchestrator (not synced) |
agent/* (grey #cccccc) |
in-progress, needs-info, blocked, done, failed, skip, duplicate, investigated
|
area/* (green #0e8a16) |
core, fs, net, shell, gui, builder, docs, build, vm, test
|
| bare |
auto-merge (#0e8a16), no-auto (#d73a4a) |
| internal | orchestrator/locked |
Constants in the scripts:
| Constant | Location | Value |
|---|---|---|
ACTIONABLE_KINDS |
ticket-runner.js:114 |
kind/bug, kind/feature, kind/chore, kind/wiki, kind/test
|
AUTO_BLOCKING_LABELS |
ticket-runner.js:115-116 |
all 8 agent/* labels |
SAFE_AUTO_MERGE_KINDS |
orchestrator-helpers.js:69 |
kind/chore, kind/wiki, kind/test
|
COMPLETION_LABELS |
ticket-runner.js:210-213 |
agent/done, agent/investigated, agent/skip, agent/blocked, agent/needs-info, agent/duplicate
|
SHORT_CIRCUIT_LABELS |
ticket-runner.js:216-219 |
the same minus agent/done
|
CLOSE_KINDS |
opencode-post-step.js:16 |
kind/investigate, kind/question
|
AGENT_COMPLETION_RE |
opencode-post-step.js:15 |
/^agent\/(done|investigated|skip|needs-info|blocked|failed)$/ |
| Command | Consumer | Meaning |
|---|---|---|
/oc triage [issue #N] |
triage.yml only (excluded from opencode.yml) |
read-only classification |
/oc Please proceed with this task. |
opencode.yml |
bot-of-bots signature; starts the DEV phase |
/oc review |
opencode.yml |
code review; final line must be Verdict: approve or Verdict: request-changes
|
/oc fix [text] |
opencode.yml |
code fix; also the FEEDBACK phase trigger |
/oc investigate, /oc explain
|
opencode.yml |
investigation kinds |
/oc wiki |
opencode.yml |
run the update-wiki skill |
/oc chore, /oc test
|
opencode.yml |
chore / test kinds |
/run, /run --turns N, /run --max-turns N, /run --reset, /run --fresh
|
ticket-runner.yml |
per-ticket state machine |
/orchestrate |
orchestrator.yml |
batch foreman |
Routing is case-sensitive; the verb is the first non-whitespace token after /oc . A bare /oc is not a trigger — opencode.yml requires the trailing space. /oc triage is a hard STOP inside the resolver skill, so a triage run can never fall through into a build run.
-
run-name: "Triage #${{ github.event.issue.number }} - ${{ github.event.issue.title }}"— this exact string is whatticket-runner.jsparses out ofworkflow_run.display_titlevia/Triage #(\d+)/. - Trigger:
issue_comment: types: [created]only. Concurrencytriage-<n>,cancel-in-progress: false.timeout-minutes: 20. - Job
if: not a PR, body starts with or contains/oc triage, does not containopencode.ai/s/(the agent's own session link — the self-retrigger guard), andauthor_associationis COLLABORATOR/MEMBER/OWNER. -
Permissions:
contents: read,pull-requests: read,issues: write. Noid-token, nocontents: write— committing, pushing and opening PRs are structurally impossible. - Steps: checkout with
persist-credentials: true→ setuser.name "opencode-agent"/user.email "opencode-agent@users.noreply.github.com"→gh repo clone LSantha/jnode_ai.wiki .wikiwithsecrets.GH_PAT→ JDK 8 zulu → lock the checkoutchmod -R a-w .thenchmod -R u+w .git→ run the agent →opencode-post-step.js(if: success() || failure() || cancelled()) → cleanup. -
.gitis deliberately left writable so the action and the cleanup step still function; everything else is read-only. -
Deterministic prompt via
with: prompt:— a single-line string, not a skill-discovery prompt. It mandates loadingjnode-triage-issue, auditingkind/*andarea/*labels first, exactly one report opening## Triage, then the body addendum, and returning the report as the final response instead of callinggh issue comment. - Cleanup (
if: always()):git checkout -q masterfirst, then delete everyopencode/*branch, thenrm -rf /tmp/triage-* /tmp/oc-triage-*. Checking outmasterfirst is required because the lockdown can leave HEAD detached or on an agent branch. - No QEMU, no ISO build, no cache — unlike
opencode.yml. JDK is present sogit log/gh/ jar inspection work, but building is explicitly banned by the skill.
Hidden comment <!-- TICKET_RUNNER_STATE: {...} --> matched by STATE_RE. initState(maxTurns) (:34) produces:
{ "phase": "DEV", "pr": null, "turn": 0, "max_turns": 3, "retries": 0,
"review_in_progress": false,
"started": "2026-09-06T22:00:00.000Z", "history": [] }A rendered status block precedes it (renderStatusSection :59, CRLF-tolerant matcher STATUS_SECTION_RE :83) with rows Phase, Turn, Retries, PR, Review in progress, Started. Phase emoji: DEV 🔨, REVIEW 🔍, FEEDBACK 🔧, HUMAN_REVIEW 👤, MERGE 🚀, DONE ✅, FAILED ❌, fallback ⏳.
on: issue_comment:[created], issues:[labeled], workflow_run:[opencode, Java CI, Triage]/[completed], pull_request_review:[submitted]. Concurrency ticket-runner-<issue|PR|run id> with cancel-in-progress: false. Permissions: issues: write, pull-requests: write, contents: write, actions: read. Checkout is the default branch.
The YAML gate accepts any kind/ label; the script narrows to ACTIONABLE_KINDS.
- Body must be
/runor start with/run,\n,\r— so/runningdoes not match. -
author_associationmust be COLLABORATOR/MEMBER/OWNER. -
PR guard:
/runis issues-only; it comments back pointing at/oc review//oc fix. -
Orchestrator guard
checkOrchestratorManaged(:235): own body hasORCHESTRATOR_STATE, or the issue is in thequeue/current_taskof an openkind/orchestratormaster withstatus === "IN_PROGRESS". -
--turns N/--max-turns N→customMaxTurns(default 3). -
--reset/--freshnulls the state. Otherwise an existingDONE/FAILEDstate is nulled ("Starting fresh"). - No state →
initState, append status, update the issue, thenh.triggerTask(issueNumber). - Existing state in
HUMAN_REVIEW→ informational comment only, no re-trigger. - Otherwise re-trigger on
state.pr || issueNumberwithphaseMessage(state): FEEDBACK →/oc fix Address review feedback., REVIEW →h.getReviewPrompt(), else/oc Please proceed with this task.
A manual /run bypasses every auto-start guard — no-auto, blocking agent/* labels, actionable kind, triage clarity, existing state. Only the PR and orchestrator guards still apply. This is the documented escape hatch.
Fails, in order, on: fetch error, pull_request, no-auto, kind/orchestrator, own body has ORCHESTRATOR_STATE, state already present, any AUTO_BLOCKING_LABELS, no ACTIONABLE_KINDS match, orchestrator-managed.
triageStatus (:306) returns {present, clear, count, requested} from the comment history:
-
requested— any comment matching/(^|\s)\/oc\s+triage(?:\s|$)/i. - comments matching
/(^|\s)\/(oc|run|orchestrate)(\s|$)/are skipped so the slash command itself is never mistaken for a report. -
count— comments matching/## .*Triage/i. -
clear— evaluated on the last triage comment only:!VAGUE_RE.test(b) && !REFUSAL_RE.test(b), whereVAGUE_RE = /needs more info from reporter|needs the following|suggested next:\s*needs-info/iandREFUSAL_RE = /refusal|out of scope/i.
handleIssuesLabeled (:341) reacts to a newly added actionable kind label:
- triage requested but no report yet → wait, do not double-request.
- no triage requested → post the full request text (which repeats the "Triage run ONLY … FORBIDDEN: git checkout -b, git push, gh pr create" clause).
- triage not clear, or
count > 3→ wait. - otherwise auto-start with
initState(3)andhistory: {event: "auto_start", reason}.
maybeAutoStartAfterTriage (:370) does the same from the Triage workflow completion, requiring present && clear && count <= 3.
This is the most important ordering rule in the whole pipeline:
-
findPRForIssueWithComment(issueNumber)(:607) — firsth.findPRForIssue(n)(open PRs whose head ref isopencode/issue<n>-*or whose body matchesCloses|Fixes|Resolves #n); fall back to scanning comments newest-first for/\b(?:Created|Opened) PR #(\d+)\b/ito survive API index lag. -
If a PR exists: unconditionally set
pr,phase = "REVIEW",retries = 0,review_in_progress = true, thenh.triggerTask(prNumber, h.getReviewPrompt()). This happens before thephaseFailedcheck and before any label inspection. - Only when no PR exists does
phaseFailed → retryOrFail, thenSHORT_CIRCUIT_LABELS → DONE, then theagent/donebranch (kind/feature/kind/bugwith no PR → retry), then fallbackretryOrFail.
A PR is the authoritative completion signal. A stale agent/needs-info or agent/skip cannot short-circuit a ticket whose agent already opened a PR, and even a failed run advances if a PR exists. orchestrator.js:488-521 mirrors this for case 'DEV'.
Two layers:
-
handleJavaCICompletion(:415) only acts onconclusion === 'success', and requiresst.phase === "REVIEW"and!st.review_in_progress. On eligibility it sets the flag, records{event: "ci_green_rereview"}, persists, then triggers the review. The orchestrator mirror isorchestrator.js:202. - The flag is cleared only in
handleReviewCompletion(:679), as its first statement — so it stays set for the whole duration of an opencode review run.
The flag is set on every /oc review schedule: handleDevCompletion:633, handleFeedbackCompletion:790, the CI-green path :438, and the manual orchestrator retrigger orchestrator.js:415-417.
The resolver skill's review contract (.opencode/skills/jnode-issue-resolver/SKILL.md) uses three markers, and the 🟡 row changed on 2026-10-03 (c50a38d):
| Marker | Meaning | Required action |
|---|---|---|
| 🔴 | Must-fix: bug, license violation, Java 1.6 violation, missing test | request changes |
| 🟡 | Should-fix: correctness, test validity, maintainability, or a tracked follow-up | request changes; a deferred 🟡 requires a follow-up issue created in the same review |
| 🟢 | Nit: typo, formatting, optional refactor | comment, do not block |
Previously 🟡 was explicitly "comment, do not block". It is now a blocking severity, with exactly one escape hatch: the review may proceed to approve only if a follow-up issue was created and linked in the summary. This is stated twice, in the skill's marker table and in its review-instructions paragraph, and mirrored in .github/AGENTS.md's REVIEW phase:
A review must not approve with an untracked 🟡 finding: request changes or create and link a follow-up issue.
The motivation is visible in the same week's commits: two latent NumberUtils production bugs (getSizeUnit("1KB") returning B, and toString(f, Integer.MAX_VALUE) overflowing an int) were discovered while writing tests, and documented in the commit message but deliberately not turned into assertions. Without the untracked-🟡 rule that is an approval with a known unfixed defect and no tracking. See Core-Test-Suite.
-
review_in_progress = false;phaseFailed → retryOrFail. - PR labels in
SHORT_CIRCUIT_LABELS→DONEwithreview_short_circuit/feedback_short_circuit. -
h.getAgentReviewVerdict(pr)returns'approve','request-changes'ornull.-
approve +
!isAutoMergeEligible→HUMAN_REVIEW,retries = 0, PR comment "Awaiting human approval via native GitHub PR Review UI." -
approve + eligible +
isDiffSafe+isCIGreen→MERGE,h.mergePR(pr), thenDONE+merged+closeIssueWithLabel. On throw:merge_failed+ a⚠️ PR comment. -
approve + eligible but diff unsafe or CI not green → stay in
REVIEWwithmerge_deferred; CI completion will retry. -
request-changes →
turn += 1; overmax_turns→FAILED+max_turns_exceeded+agent/failed; elseFEEDBACK,retries = 0, trigger/oc fix Address review feedback. -
null →
retryOrFail("no verdict found for PR #N. Retrying review.").
-
approve +
| Budget | Default | Overridable | On exceed |
|---|---|---|---|
turn |
max_turns = 3 |
only by manual /run --turns N
|
FAILED, {event:"max_turns_exceeded"}, agent/failed
|
retries |
3 | no |
FAILED, {event:"max_retries", phase}, agent/failed
|
--turns is ignored on the auto paths — autoInitRun hardcodes initState(3). Terminal DONE/FAILED states short-circuit every handler (handleWorkflowRun:503, handleJavaCICompletion:435).
Skips bot reviewers and non-collaborator associations. Resolves the owning issue via findIssueByPR (:870, head ref ^opencode/issue(\d+)- or a closing keyword, then a scan of recently-updated open issues for st.pr === prNumber). Requires state.phase === "HUMAN_REVIEW".
-
approved→MERGE→mergePR→DONE→ close. -
changes_requested→turn += 1; over limit →FAILED; elseFEEDBACKwith/oc fix Address human review feedback.
Runs in both opencode.yml and triage.yml, if: success() || failure() || cancelled(), with PREV_CONCLUSION = steps.run_agent.outcome. It reads context.payload.issue.number and pages through comments with per_page: 100.
| Function | Test | Meaning |
|---|---|---|
isRefusalComment |
/^\s*##\s*(?:🤖\s*)?Refusal\b/im |
out-of-scope refusal heading |
isNeedsInfoComment |
content match on the three needs-info literals | "needs more info from reporter" / "needs the following" / "Suggested next: needs-info" |
isTriageComment |
/## .*Triage/i |
any ## … Triage heading |
isTriageClearComment |
triage ∧ ¬needs-info ∧ ¬refusal | actionable triage |
isTriggerComment |
/(^|\s)\/(oc|run|orchestrate)(\s|$)/ |
a slash command, never a report |
isDuplicateSignal |
/duplicate-of-#\d+/i |
Suggested next: duplicate-of-#M |
isPRCreationComment |
/\b(?:Created|Opened) PR #\d+\b/i |
PR work product |
isInvestigationReport |
/## .*Investigation Report/i |
investigation work product |
findLatestAgentComment(comments) (:66) walks backwards and returns the first body that is neither a trigger comment nor a non-report. Because the review prompt itself quotes both verdicts and the triage request text contains ## Triage, the trigger exclusion is what keeps the scanners from misfiring on instructions.
- refusal →
agent/skip - needs-info literals →
agent/needs-info -
## … Investigation Report→agent/investigated(verb-override; not kind-based) -
duplicate-of-#N→agent/duplicate -
Created|Opened PR #N→agent/done - clear triage →
{label: null, clearNeedsInfo: true, clearFailed: true} -
conclusion === 'failure' || 'cancelled'→agent/failed - an existing
agent/*label other thanagent/failedis respected - PR context →
agent/done - default →
agent/done
Reports 1-6 are evaluated before the failure check, so a run that dies during action finalization after the comment landed still gets the report-derived label. Steps 3-5 are evidence gates: the kind label alone is never enough. The old isInvestigationKind(labels) default was removed; agent/investigated now requires the report heading and agent/duplicate requires the link.
A label === null (clear triage) result removes stale agent/needs-info and agent/failed if present, but only when actually present — untouched labels are not churned. agent/in-progress is always removed at the end.
if (isPR) return false; // never close a PR
if (agentLabel === 'agent/duplicate') return true;
if (agentLabel !== 'agent/investigated') return false;
if (isInvestigationReport(latestComment)) return true; // the evidence
return CLOSE_KINDS.some(k => labels.includes(k));Because decideAgentLabel can only produce agent/investigated through the report-heading branch, the CLOSE_KINDS fallback is effectively unreachable today — the label decision is already evidence-gated. If the issue is already closed, the close call is skipped with an info log.
Auto-merge requires all of:
| Gate | Rule |
|---|---|
isAutoMergeEligible(issue, pr) |
explicit auto-merge on the issue or PR, or implicit kind/chore/kind/wiki/kind/test
|
isDiffSafe(pr) |
≤ 5 files, no path matching /(^|\/)(core\/src\/native\/x86\/|jnode\.properties$|all\/build\.xml$|all\/conf\/)/, and ≤ 600 added lines when every changed path matches /(^|\/)(src\/test\/|tests\/)/, otherwise ≤ 100; also false if the file list is empty or hits the 100-file pagination cap |
isCIGreen(pr) |
checks.listForRef on the head SHA: at least one success, none in failure/cancelled/timed_out/action_required
|
var FORBIDDEN_DIFF_RE = /(^|\/)(core\/src\/native\/x86\/|jnode\.properties$|all\/build\.xml$|all\/conf\/)/;
var TEST_PATH_RE = /(^|\/)(src\/test\/|tests\/)/; // orchestrator-helpers.js:94
// ...
var additions = 0;
var testOnly = true;
for (var i = 0; i < list.length; i++) {
var filename = list[i].filename || "";
additions += list[i].additions || 0;
if (FORBIDDEN_DIFF_RE.test(filename)) return false;
if (!TEST_PATH_RE.test(filename)) testOnly = false;
}
return additions <= (testOnly ? 600 : 100); // :118The allowance is all-or-nothing: testOnly starts true and is cleared by the first non-test path, so one production file collapses the whole diff back to the 100-line cap. TEST_PATH_RE matches the directory src/test/ or tests/, not the filename, so core/src/test/NumberUtilsTest.java qualifies and core/src/core/NumberUtils.java does not.
The cap was raised three times in one day as the suite-wiring issues (#489, #695, #696, #697) landed: 100 → 300 (950b022, after NumberUtilsTest's +244) → 400 (718680e) → 600 (842b6d7, after WaitTest/ViewMethodTest/DoubleTest/ForEachTest together added 506 lines). The file-count cap stays at 5 and the forbidden-path check stays unconditional, so ASM, jnode.properties, all/build.xml and all/conf/ can never ride the allowance.
.github/scripts/tests/ticket-runner.test.js has one sub-test pinning this triple:
await t.test("isDiffSafe: allows large test-only diffs but not production diffs", async (t) => {
const hTest = makeHelpers({ files: [{ filename: "core/src/test/NumberUtilsTest.java", additions: 601 }] });
assert.strictEqual(await hTest.isDiffSafe(99), false);
const hTestOk = makeHelpers({ files: [{ filename: "core/src/test/NumberUtilsTest.java", additions: 600 }] });
assert.strictEqual(await hTestOk.isDiffSafe(99), true);
const hMixed = makeHelpers({ files: [
{ filename: "core/src/test/NumberUtilsTest.java", additions: 200 },
{ filename: "core/src/core/NumberUtils.java", additions: 1 }
] });
assert.strictEqual(await hMixed.isDiffSafe(99), false);
});Note that raising the cap requires editing three files in lockstep: the constant in orchestrator-helpers.js, the matching sentence in .github/AGENTS.md's REVIEW phase description, and the test boundaries. Getting that wrong is what makes the raise a three-commit sequence rather than one.
mergePR (:155-188) squash-merges and then verifies:
try { await github.rest.pulls.merge({ ..., merge_method: "squash" }); }
catch (err) {
var message = String((err && err.message) || "");
if (err && err.status === 204) core.info("Merge API returned 204; verifying PR state");
else if (/Unexpected end of JSON input/i.test(message)) core.info("Merge API returned an empty body; verifying PR state");
else throw err;
}
var merged = await github.rest.pulls.get({ ..., pull_number: prNumber });
if (!merged.data || merged.data.merged !== true)
throw new Error("merge not confirmed; PR state=" + (merged.data && merged.data.state ? merged.data.state : "unknown"));
try { await github.rest.git.deleteRef({ ..., ref: "heads/" + pr.data.head.ref }); }
catch (e) { core.warning("Could not delete branch " + pr.data.head.ref + ": ": e.message); }Octokit surfaces a successful squash merge as 204 or as Unexpected end of JSON input because there is no response body to parse. Both are downgraded from throws to "verify"; every other error still propagates. Confirmation is authoritative — the PR is re-fetched and must report merged === true, or the caller throws and the ticket records merge_failed rather than claiming success. Branch deletion happens only after confirmation and is best-effort.
getAgentReviewVerdict skips trigger comments with the same /^\/(oc|run|orchestrate)(\s|$)/ guard, and tolerates decorated verdicts (**Verdict**: approve) via /(?:\*{0,2}Verdict\*{0,2}:?\*{0,2}\s*:?\s*)approve\b/i.
Two independent producers post the same marker-deduped comment, so at most one heal is ever attempted per SHA:
| Producer | Mechanism |
|---|---|
ant.yml heal job |
needs: [build, test, boot32, boot64], if: failure() && github.event_name == 'pull_request', permissions issues: write + pull-requests: write, token secrets.GH_PAT || secrets.GITHUB_TOKEN
|
ticket-runner.js postCiFixOnce (:392) |
from handleJavaCICompletion, only when phase is REVIEW or FEEDBACK
|
Both compute marker = '<!-- CI-HEAL:' + sha.slice(0,7) + ' -->', skip when the PR carries no-auto or agent/in-progress, skip when any of the last 100 comments already contains the marker, and then post:
/oc fix CI failed for <sha7> (jobs: <failed | unknown>): <runUrl>
<!-- CI-HEAL:<sha7> -->
Read the failing check output, fix the code on this branch, and push. Do not open a new PR.
The heal job derives the failed job list from toJSON(needs). ticket-runner.js gets the same information implicitly by only running for REVIEW/FEEDBACK.
node --test .github/scripts/tests/*.test.js # 105 tests, node:test, Node 18+
node .github/scripts/sync-labels.js --dry-runThe three state machines export their internals for tests: _parseState, _initState, _serializeState, _replaceOrAppendState, _replaceOrAppendStatus, _renderStatusSection on ticket-runner.js.
-
A PR beats everything. The DEV handler checks for a PR before
phaseFailedand before anyagent/*label. Changing that order re-introduces the "ticket opened a PR but a staleagent/needs-infoclosed the issue" bug. -
review_in_progressis the single re-review mutex. It is set on every/oc reviewschedule and cleared only inhandleReviewCompletion. Without it, a green CI run and a completing review race into two concurrent reviews. -
Merge confirmation is not optional. A
204or an unparseable body means "check the PR", not "it worked". The regression tests assert that an unconfirmed merge must not be reported as success. -
Slash commands are never reports.
isTriggerCommentis what stops the review prompt (which quotes both verdicts) and the triage request (which contains## Triage) from being classified as agent output. -
Refusal is matched by heading, not by prose.
^\s*##\s*(?:🤖\s*)?Refusal\b— a comment that merely says "out of scope" no longer setsagent/skip. Note the residual inconsistency:ticket-runner.js REFUSAL_RE = /refusal|out of scope/iis still unanchored, but it only marks a triage comment non-clear; it never sets a label. -
--turnsis a manual-only override. Auto-start always uses 3. -
A manual
/runis a deliberate bypass. It is the documented way to re-run a ticket the auto-guards would refuse. -
Triage is read-only by permissions, not by prompt.
contents: readand noid-tokenmake a push impossible even if the agent tries; the prompt's "no branches, no commits, no PRs" is defence in depth. -
.gitmust stay writable under lockdown.chmod -R a-w . && chmod -R u+w .git— without the second command the action and the branch cleanup cannot run. -
Workflow run names are load-bearing.
ticket-runner.jsextracts the issue number fromTriage #123 - titleindisplay_title, so renaming the workflow breaks auto-start. -
agent/in-progressis never added by a workflow. The resolver skill instructs the agent to add it viagh issue edit, but no workflow step sets it..github/AGENTS.md:204claims otherwise; the label is still consumed as a busy flag byauto-triage.yml,ant.yml heal,postCiFixOnceand the orchestrator. -
CLOSE_KINDSis effectively dead code. It can only be reached afterdecideAgentLabelalready returnedagent/investigated, which itself requires the report heading. -
Off-by-one across components.
auto-triage.ymlre-triages whiletriageCount < 3;ticket-runner.jsblocks auto-start atcount > 3. An issue that reaches 4 triage passes is never re-triaged and also cannot auto-start. -
All text is US-ASCII. The only exception is the em dash (
—, U+2014) in theopencode.ymlrun name..github/AGENTS.mdrenders it as a plain hyphen.
- GitHub-Actions-Workflows — Parent hub: every workflow file, trigger, and the CI/boot pipeline
-
Testing — The JUnit and boot verification that the
ant.ymlpipeline runs and thathealreacts to -
Build-System — The
cd-x86-combined-lite regression-testsbuild the CI and agent workflows execute - Code-Conventions — US-ASCII encoding rule enforced across the automation files