diff --git a/scripts/loop/README.md b/scripts/loop/README.md index 460ed31..7e68a2c 100644 --- a/scripts/loop/README.md +++ b/scripts/loop/README.md @@ -920,12 +920,21 @@ the first `auto` run is a verification step rather than a milestone. comment, then apply the claim label — were no-ops, and the only remaining entry was `workflow_dispatch`, which hits the same claim check. `humanClaimOf` (`shared/route.mjs`) now recognises exactly that event — - `issues.labeled`, the claim label, applied by an OWNER/MEMBER/COLLABORATOR, - out of `waiting-human` — and `doRun` moves the task to `ready` (timeline + `issues.labeled`, the claim label, out of `waiting-human`, applied by + anything that is not a bot — and `doRun` moves the task to `ready` (timeline `claimed`, labels refreshed from the event) before claiming it. Narrow on purpose: D29's repair path, `start --task` and PR `synchronize` still cannot convert an inbox item, so the cognitive guard is unchanged and only an explicit human act opens it. + **The first version of the predicate failed in production** on the very next + run: it gated on `event.sender.author_association`, but `author_association` + is a field of the issue, not of the sender (`simple-user` has no such field), + so the check was `undefined` and the run still logged + `skip: task #77 not claimable (status=waiting-human)`. Permissions need no + local test anyway — editing labels requires triage/write access, so the + event's mere existence is the maintainer's act; the predicate now excludes + only bots. Lesson recorded because the shape is easy to repeat: verify a + payload field against a real event payload, not against a hand-written one. ## Environment facts diff --git a/scripts/loop/shared/route.mjs b/scripts/loop/shared/route.mjs index 88ef64e..c16da18 100644 --- a/scripts/loop/shared/route.mjs +++ b/scripts/loop/shared/route.mjs @@ -87,8 +87,15 @@ export function humanClaimOf({ eventName, action, event = {} }, status) { if (eventName !== 'issues' || action !== 'labeled') return null; if (event.label?.name !== CLAIM_LABEL) return null; if (status !== 'waiting-human') return null; - if (!OWNER_ROLES.includes(event.sender?.author_association ?? '')) return null; - return event.sender?.login ?? 'a maintainer'; + // Who may apply it is settled by GitHub, not by this predicate: editing labels + // needs triage/write access, so the existence of an `issues.labeled` run is + // already a maintainer's act (a `GITHUB_TOKEN`-raised one never runs at all). + // Only bots are excluded. Do **not** test `sender.author_association`: that + // field lives on the issue, not on the sender, so the first version of this + // predicate could never be true and #77 kept skipping (D37). + const sender = event.sender ?? {}; + if (sender.type === 'Bot' || String(sender.login ?? '').endsWith('[bot]')) return null; + return sender.login ?? null; } /**