Skip to content

chore(loop): let a triaged task actually reach the implementing stage - #49

Merged
Alex1990 merged 1 commit into
mainfrom
chore/loop-claim-signal
Sep 15, 2026
Merged

Alex1990 merged 1 commit into
mainfrom
chore/loop-claim-signal

Conversation

@Alex1990

Copy link
Copy Markdown
Owner

Triage could decide an issue was ready-for-agent and nothing would ever happen to it. This fixes the last missing edge in the loop's automatic path, and it was just reproduced end to end on issue #48.

The gap, demonstrated

Issue #48 was filed; the loop triaged it correctly (run 35005501011):

[loop] run r-20260915-180756-4frk finished: outcome=triaged (exit 0), tokens=28303
[loop] GitHub actions executed: 2
[loop] task #48 → status=ready, label=ready-for-agent

…and stopped. The task sits in ready, the label is on the issue, and no stage that implements anything can ever pick it up. route.mjs had no feature/bugfix entry at all — run() was reachable only for triage, pr-review, deps and the two schedules. STAGE_OUTCOMES.bugfix was structurally unreachable on Actions, which is also why the A1 acceptance item for "bugfix-feature" was never unsampled: it could not run.

Two mechanisms, because the obvious one does not work

A person applies ready-for-agent → issues.labeled runs the stage directly. stageForIssue picks bugfix when the issue also carries bug, else feature. This doubles as the manual claim path: labelling any issue hands it to the loop.

The loop applies it (the ordinary triage verdict) → there is no run to route. GitHub suppresses workflow runs for events raised by GITHUB_TOKEN — the documented recursion guard — which is why the loop has never had an issues event in its history. workflow_dispatch is the stated exception, so entry.mjs asks for the next stage explicitly (dispatchStage). That is the entire reason for the new actions: write scope: the narrowest one that lets the loop start its own work.

Only ready-for-agent is a claim. The other four role labels are states the loop itself writes, and reacting to them would make it chase its own output — a stale ready-for-agent sitting on an issue must not hijack a later needs-info event.

Scope change

permissions gains actions: write. What it buys and what it does not:

can now start loop.yml (how the hand-off works)
still cannot change main (the Main branch ruleset refuses every credential)
still cannot edit .github/workflows/** (no workflows permission exists for permissions: to grant)
still cannot edit the ruleset that bounds it (no administration)

A failed dispatch warns and leaves the task claimable rather than failing the run — a stalled claim should be visible, but it is not a failure of the triage that produced it.

Verification

The router is a pure function, so it is tested offline rather than by firing GitHub events: 17 assertions covering the claim (both stages), the four non-claim labels, a stale ready-for-agent that must not hijack another label event, the untouched pre-existing routes, and stageForIssue against both label shapes (API objects and plain strings). gh-check now reports the dispatch scope as a needed capability instead of a forbidden one.

docs/agents/ops.md gains the claim signal in its trigger map plus both mechanisms; scripts/loop/README.md carries the same for the workflow table and the piece-by-piece table.

After this merges

Issue #48 is already sitting in ready with bug + ready-for-agent, so it becomes the first real exercise of the path this PR adds: dispatch → bugfix → branch → PR.

Triage could decide an issue was `ready-for-agent` and nothing would ever happen to
it. The route table had no `feature`/`bugfix` entry at all: `run()` was reachable only
for `triage`, `pr-review`, `deps` and the two schedules, so `STAGE_OUTCOMES.bugfix`
was unreachable on Actions and the A1 acceptance item for "bugfix-feature" was never
unsampled — it was structurally unreachable. The loop could classify work and then
never do any.

Two mechanisms, because the obvious one does not work:

- **A person applies `ready-for-agent`** → `issues.labeled` runs the stage directly,
  and `stageForIssue` picks `bugfix` when the issue also carries `bug`, else `feature`.
  This doubles as the manual claim path: labelling any issue hands it to the loop.
- **The loop applies it** (the ordinary triage verdict) → no run exists to route.
  GitHub suppresses workflow runs for events raised by `GITHUB_TOKEN`, which is the
  documented recursion guard, and it is why the loop has never had an `issues` event.
  `workflow_dispatch` is the stated exception, so `entry.mjs` asks for the next stage
  explicitly (`dispatchStage`). That is the whole reason for the new `actions: write`
  scope — the narrowest one that makes the loop able to start its own work.

Only `ready-for-agent` is a claim. The other four role labels are states the loop
itself writes, and reacting to them would make it chase its own output.

Also in scope, because it defines this path's vocabulary and had drifted from it:
`docs/agents/ops.md` now lists the claim signal in the trigger map and states both
mechanisms, and `scripts/loop/README.md` carries the same for the workflow table.

Verification: the router is a pure function, so the route is tested offline rather
than by firing GitHub events — 17 assertions covering the claim (both stages), the
four non-claim labels, a stale `ready-for-agent` that must not hijack another label
event, the untouched pre-existing routes, and `stageForIssue` against both label
shapes. `gh-check` now reports the dispatch scope as a needed capability rather than
a forbidden one.
@Alex1990
Alex1990 force-pushed the chore/loop-claim-signal branch from ea035b7 to f2a58e5 Compare September 15, 2026 18:15
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Metric % Covered / Total
Lines 90.19% 1003/1112
Statements 89.06% 1083/1216
Functions 91.03% 193/212
Branches 76.43% 519/679

@github-actions github-actions Bot added the needs-triage Maintainer needs to evaluate (loop can't decide; too risky; external PR) label Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Loop triage — needs-triage (human review + merge).

PR #49 edits the loop's own machinery and norms layer (.github/workflows/loop.yml, scripts/loop/{entry.mjs,shared/route.mjs,gh-check.mjs,README.md}, docs/agents/ops.md) on a same-repo branch chore/loop-claim-signal with no loop-task: marker. loopTaskOf() returns null, so the router sends owner repo PRs to triage: it is not a loop PR, the loop must not be maker or checker for the change it runs on, and it touches .github/workflows/**, which the job token can never push. docs/agents/ops.md is norms-layer, so this is a proposal a human resolves. The change is already written and green — what remains is a maintainer review/merge decision. No acceptance row.

The gap is real. On main (1e452a2, this PR's base) route.mjs reaches run() only for issues.opened/reopened triage, pr-review, deps, the two schedules and workflow_dispatch; there is no feature/bugfix route, and issues.labeled fell through to a noop. STAGE_OUTCOMES.bugfix was therefore reachable only by a hand-run dispatch. The two new mechanisms are the right shape: issues: labeled plus the ready-for-agent claim (only that label — a stale one cannot hijack a later needs-info), and gh workflow run for the loop's own label, which cannot raise a run because GitHub suppresses GITHUB_TOKEN-raised events. actions: write is what the dispatch API requires; no narrower grant exists, and a PAT would be strictly worse.

Verified read-only. Diff vs origin/main is 1 commit, 6 files, +131/−11; the base sha equals the current main head. node --check is clean on the four touched modules. I re-ran the router offline with 19 assertions (claim → bugfix/feature, the four non-claim labels, stale-claim non-hijack, pre-existing routes, workflow_dispatch inputs, stageForIssue for both label shapes): 19/19 pass. CI head f2a58e5: Lint pass (22 s), Test pass (44 s); the pending loop check is this triage run. mergeable: MERGEABLE, mergeStateStatus: BLOCKED, reviewDecision: REVIEW_REQUIRED.

Points for the reviewer.

  1. dispatchStage is not gated by writeLevel: entry.mjs performs the gh workflow run POST even under the report boundary, where finishRun withholds the label/comment — so report mode still performs a GitHub write and starts a real implementing run while ready-for-agent was never applied to the issue. That contradicts the file's own "no GitHub write under report" invariant.
  2. No committed test: the "17 assertions" are not in the repo — the diff adds no test file and stageForIssue/CLAIM_LABEL appear only in the source. The assertions are not reproducible from the tree (my 19 equivalent ones all pass, so the behaviour is right; they just are not a durable guard).
  3. actions: write also permits cancelling/disabling workflows, re-running jobs, deleting artifacts, and dispatching any workflow_dispatch workflow — not only loop.yml. It is the narrowest grant GitHub offers, but the scope table's "can now: start loop.yml" understates it.
  4. "After this merges, CI never runs the Azure signer oracle #48 becomes the first exercise" is not automatic: CI never runs the Azure signer oracle #48's triage run already completed, sweep does not dispatch ready tasks, and push is not a trigger. Re-apply the label (remove first — a no-op relabel fires no event), comment, or dispatch CI never runs the Azure signer oracle #48 manually; the first automatic run will be the next issue triaged to ready-for-agent.

Proposed action: label needs-triage; a human reviews and merges. The loop must not merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-triage Maintainer needs to evaluate (loop can't decide; too risky; external PR)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant