From 320db06ac6278f4d7ed472076618f70f7b8ed3a9 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 20:31:53 +0700 Subject: [PATCH] queen(app): t27-bees reacts to issue events, so a new issue is dispatched on the webhook, not the next poll (Closes #10399) specs/queen/app.t27 gains the issue events: issues.opened, issues.reopened, issues.labeled and issues.closed in EVENT_PAIRS, codes 13..16 appended after APP_EVENTS_CI (no earlier code moves), and dispatch_reaction answering DISPATCH / DROP / IGNORE (never for a bot sender or an unserved repository). APP_SUBSCRIBED_EVENTS and tools/bees/manifest.json default_events add `issues`. Five new tests (24/24, 0 vacuous), two new invariants; the 19 existing tests are unchanged. Seal queen_QueenApp remade on the Railway lab. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/queen_QueenApp.json | 18 +++--- specs/queen/app.t27 | 99 +++++++++++++++++++++++++++++- tools/bees/README.md | 2 +- tools/bees/manifest.json | 3 +- 4 files changed, 108 insertions(+), 14 deletions(-) diff --git a/.trinity/seals/queen_QueenApp.json b/.trinity/seals/queen_QueenApp.json index 889c0f40b5..662a8fa9ba 100644 --- a/.trinity/seals/queen_QueenApp.json +++ b/.trinity/seals/queen_QueenApp.json @@ -1,24 +1,24 @@ { - "built_by": "t27c-bootstrap@0.5.2+cc19d1fcd", + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:6826f83e8df029b2611772eb05ae83f6f74f2753878234aecadde730f25d3aea", - "gen_hash_rust": "sha256:59377e2be1ac583fdca7c92f2b9167be28cfbf649feae60ef60dbcc316e4f03b", - "gen_hash_verilog": "sha256:ac673fb18c41bc3710d4cf89d2de9274aad10daa109765ed9350bada4d61e883", - "gen_hash_zig": "sha256:b7164f9a42b17ff9f13866b5c46281d6d2f5584a69dae33c70ce3f2ec507c23f", + "gen_hash_c": "sha256:5c99dce3207e8ce0ec9d806e6260dd0564d1d63bea63b352eb3ba89e7c1bf753", + "gen_hash_rust": "sha256:96960c99d5f0b870bb694b8e2f4afbbf3df3a672bd9552636bff5f7c66a3579a", + "gen_hash_verilog": "sha256:925f962b7acfa382f9bfcf8281c0f28997595f1a779326a9a4aa3f062bdd533c", + "gen_hash_zig": "sha256:ab6e6e5f41b1dd79f553ca3572b64f66748a9c3cd8dbdf39a6c2db526c1e48f7", "module": "QueenApp", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-11T10:29:36Z", + "sealed_at": "2026-10-18T13:27:28Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:854770015052ccdf70a7e8b7b831ed878a3100b028e2b3ff39010c3cbf8eccbe", + "spec_hash": "sha256:6909de6d8fbda29f8aaa41612838c7c9818aecec78a142b94888e54908890802", "spec_path": "specs/queen/app.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 19, - "total": 19, + "passed": 24, + "total": 24, "vacuous": 0 }, "toolchain": { diff --git a/specs/queen/app.t27 b/specs/queen/app.t27 index c552a08a4c..32401e1052 100644 --- a/specs/queen/app.t27 +++ b/specs/queen/app.t27 @@ -38,12 +38,24 @@ pub const GE_CHECK_RERUN : u8 = 10; pub const GE_CI_DONE : u8 = 11; pub const GE_MERGE_GROUP : u8 = 12; pub const APP_EVENTS_CI : u8 = 13; +/// The issue events (#10399), appended after APP_EVENTS_CI so that no earlier code moves: a supervisor +/// built before them reads them as codes app_reaction and ci_reaction ignore. dispatch_reaction is what +/// answers them, so a new or relabelled issue is a dispatch on the webhook and the poll is the backstop. +/// ISSUE_OPENED an issue was opened (issues.opened) +/// ISSUE_REOPENED a closed issue was reopened (issues.reopened) +/// ISSUE_LABELED a label was added to an issue (issues.labeled) +/// ISSUE_CLOSED an issue was closed (issues.closed) +pub const GE_ISSUE_OPENED : u8 = 13; +pub const GE_ISSUE_REOPENED : u8 = 14; +pub const GE_ISSUE_LABELED : u8 = 15; +pub const GE_ISSUE_CLOSED : u8 = 16; +pub const APP_EVENTS_ISSUES : u8 = 17; /// The webhook as the supervisor hands it over: ".". Entry n of EVENT_PAIRS /// is GE code EVENT_CODES[n]. A reopened pull request is an opened one; a suspended installation is /// an uninstalled one until it is unsuspended. Anything not listed is GE_OTHER. -pub const EVENT_PAIRS : str = "installation.created,installation.unsuspend,installation.deleted,installation.suspend,installation_repositories.added,installation_repositories.removed,pull_request.opened,pull_request.reopened,pull_request.synchronize,pull_request.ready_for_review,pull_request.closed,issue_comment.created,check_run.rerequested,workflow_run.completed,merge_group.checks_requested,$"; -pub const EVENT_CODES : [15]u8 = [0, 0, 1, 1, 2, 3, 4, 4, 5, 6, 7, 8, 10, 11, 12]; +pub const EVENT_PAIRS : str = "installation.created,installation.unsuspend,installation.deleted,installation.suspend,installation_repositories.added,installation_repositories.removed,pull_request.opened,pull_request.reopened,pull_request.synchronize,pull_request.ready_for_review,pull_request.closed,issue_comment.created,check_run.rerequested,workflow_run.completed,merge_group.checks_requested,issues.opened,issues.reopened,issues.labeled,issues.closed,$"; +pub const EVENT_CODES : [19]u8 = [0, 0, 1, 1, 2, 3, 4, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16]; /// What the Queen does about it. /// REGISTER remember the installation's repositories (they may now be served) @@ -283,7 +295,7 @@ pub fn is_t27_path(path: []const u8, len: usize) -> bool { /// tools/bees/manifest.json subscribes only pull_request and issue_comment: a workflow_run webhook from /// every CI run of every installed repository, read by a supervisor that ignores it, is load for nothing. pub const CHECK_NAME : str = "t27-bees review$"; -pub const APP_SUBSCRIBED_EVENTS : str = "pull_request,issue_comment,check_run,workflow_run,merge_group,$"; +pub const APP_SUBSCRIBED_EVENTS : str = "pull_request,issue_comment,check_run,workflow_run,merge_group,issues,$"; pub const APP_PERMISSIONS : str = "pull_requests:write,issues:write,checks:write,actions:read,contents:read,metadata:read,$"; /// What the Queen does about a CI/CD event. @@ -320,6 +332,27 @@ pub fn ci_reaction(event: u8, from_a_bot: bool, served: bool, paused: bool, ci_f return CR_IGNORE; } +/// What the Queen does about an issue event (#10399). +/// DISPATCH the issue is work to hand out: put it in front of the dispatcher now, not on the next poll +/// DROP the issue was closed: forget a dispatch that is still pending for it +/// IGNORE nothing to do +pub const DR_IGNORE : u8 = 0; +pub const DR_DISPATCH : u8 = 1; +pub const DR_DROP : u8 = 2; + +/// The reaction to an issue event; `from_a_bot` and `served` mean what they mean for app_reaction. A +/// bot's issue or label is never dispatched (the swarm files and labels issues itself, and answering +/// that would loop), and a repository nobody installed the app on is never read. Opened, reopened and +/// labeled are one answer because a dispatch is keyed by the issue, not by the event that raised it: +/// asking for the same issue twice is the same dispatch. Any event that is not an issue event is IGNORE. +pub fn dispatch_reaction(event: u8, from_a_bot: bool, served: bool) -> u8 { + if (from_a_bot) { return DR_IGNORE; } + if (served == false) { return DR_IGNORE; } + if (event == GE_ISSUE_CLOSED) { return DR_DROP; } + if (event == GE_ISSUE_OPENED or event == GE_ISSUE_REOPENED or event == GE_ISSUE_LABELED) { return DR_DISPATCH; } + return DR_IGNORE; +} + /// The gate. What is checked decides it; what is opined never does. A changed .t27 file the compiler /// calls broken (compiler_verdict), or a spec whose tests passed without one runtime assert (a vacuous /// pass, read from the CI run's t27c test-report artifact, not run here), fails the check. The model's @@ -544,6 +577,66 @@ test a_failure_red_on_the_base_is_not_the_pull_requests { assert triage_owner(false) == TO_PULL_REQUEST; } +test the_issue_events_are_read_by_their_event_and_action { + assert app_event_of("issues.opened", 13, false) == GE_ISSUE_OPENED; + assert app_event_of("issues.reopened", 15, false) == GE_ISSUE_REOPENED; + assert app_event_of("issues.labeled", 14, false) == GE_ISSUE_LABELED; + assert app_event_of("issues.closed", 13, false) == GE_ISSUE_CLOSED; + // the actions the Queen does not act on are other, and so is the pull request pair of the same name + assert app_event_of("issues.edited", 13, false) == GE_OTHER; + assert app_event_of("issues.assigned", 15, false) == GE_OTHER; + assert app_event_of("issues.open", 11, false) == GE_OTHER; + assert app_event_of("issues.closedx", 14, false) == GE_OTHER; +} + +test an_issue_is_dispatched_on_the_webhook_and_a_closed_one_is_dropped { + assert dispatch_reaction(GE_ISSUE_OPENED, false, true) == DR_DISPATCH; + assert dispatch_reaction(GE_ISSUE_REOPENED, false, true) == DR_DISPATCH; + assert dispatch_reaction(GE_ISSUE_LABELED, false, true) == DR_DISPATCH; + assert dispatch_reaction(GE_ISSUE_CLOSED, false, true) == DR_DROP; +} + +test a_bot_issue_or_a_stranger_repo_is_never_dispatched { + // negative control: a bot sender is ignored for every issue event, closing included + assert dispatch_reaction(GE_ISSUE_OPENED, true, true) == DR_IGNORE; + assert dispatch_reaction(GE_ISSUE_REOPENED, true, true) == DR_IGNORE; + assert dispatch_reaction(GE_ISSUE_LABELED, true, true) == DR_IGNORE; + assert dispatch_reaction(GE_ISSUE_CLOSED, true, true) == DR_IGNORE; + // a repository nobody installed the app on is never read + assert dispatch_reaction(GE_ISSUE_OPENED, false, false) == DR_IGNORE; + assert dispatch_reaction(GE_ISSUE_CLOSED, false, false) == DR_IGNORE; + // events that are not issue events have no dispatch + assert dispatch_reaction(GE_PR_OPENED, false, true) == DR_IGNORE; + assert dispatch_reaction(GE_COMMENT, false, true) == DR_IGNORE; + assert dispatch_reaction(GE_OTHER, false, true) == DR_IGNORE; + assert dispatch_reaction(APP_EVENTS_ISSUES, false, true) == DR_IGNORE; +} + +test a_supervisor_from_before_issues_ignores_the_issue_events { + assert app_reaction(GE_ISSUE_OPENED, false, true, false, false, 5) == AR_IGNORE; + assert app_reaction(GE_ISSUE_REOPENED, false, true, false, false, 5) == AR_IGNORE; + assert app_reaction(GE_ISSUE_LABELED, false, true, false, false, 5) == AR_IGNORE; + assert app_reaction(GE_ISSUE_CLOSED, false, true, false, false, 5) == AR_IGNORE; + assert ci_reaction(GE_ISSUE_OPENED, false, true, false, true, 5) == CR_IGNORE; + assert ci_reaction(GE_ISSUE_CLOSED, false, true, false, true, 5) == CR_IGNORE; +} + +test an_old_webhook_keeps_its_code_after_the_issue_pairs_are_appended { + assert app_event_of("installation.created", 20, false) == GE_INSTALLED; + assert app_event_of("pull_request.closed", 19, false) == GE_PR_CLOSED; + assert app_event_of("merge_group.checks_requested", 28, false) == GE_MERGE_GROUP; + assert app_event_of("issue_comment.created", 21, true) == GE_COMMENT; + assert app_event_of("issue_comment.created", 21, false) == GE_OTHER; +} + +invariant the_issue_codes_follow_the_ci_codes { + assert GE_ISSUE_OPENED == APP_EVENTS_CI and GE_ISSUE_REOPENED == 14 and GE_ISSUE_LABELED == 15; + assert GE_ISSUE_CLOSED == 16 and APP_EVENTS_ISSUES == 17; + assert EVENT_CODES[15] == GE_ISSUE_OPENED and EVENT_CODES[18] == GE_ISSUE_CLOSED; + assert EVENT_CODES[14] == GE_MERGE_GROUP and EVENT_CODES[0] == GE_INSTALLED; + assert DR_IGNORE == 0 and DR_DISPATCH == 1 and DR_DROP == 2; +} + invariant the_ci_codes_never_move_either { assert GE_CHECK_RERUN == APP_EVENTS and GE_CI_DONE == 11 and GE_MERGE_GROUP == 12; assert APP_EVENTS_CI == 13; diff --git a/tools/bees/README.md b/tools/bees/README.md index 1b5bd9d2c7..bdd659b321 100644 --- a/tools/bees/README.md +++ b/tools/bees/README.md @@ -12,7 +12,7 @@ SHA and **only** the bot's label. | File | What it is | |------|------------| -| `manifest.json` | The app, as GitHub's manifest flow reads it: public since 2026-10-08, webhook to the supervisor (`https://api.t27.ai/queen/app/webhook`, BrowserOS#543) for `pull_request` and `issue_comment`, five permissions (pull requests write, issues write for labels, contents/checks/metadata read). The CI/CD gate's events and permissions are in `specs/queen/app.t27` (APP_SUBSCRIBED_EVENTS, APP_PERMISSIONS) and join this file when the supervisor acts on them (#7834). | +| `manifest.json` | The app, as GitHub's manifest flow reads it: public since 2026-10-08, webhook to the supervisor (`https://api.t27.ai/queen/app/webhook`, BrowserOS#543) for `pull_request`, `issue_comment` and `issues` (a new, reopened, relabeled or closed issue is a dispatch reaction, `dispatch_reaction` in `specs/queen/app.t27`, #10399; the App's event subscription in its GitHub settings is separate from this file), five permissions (pull requests write, issues write for labels, contents/checks/metadata read). The CI/CD gate's events and permissions are in `specs/queen/app.t27` (APP_SUBSCRIBED_EVENTS, APP_PERMISSIONS) and join this file when the supervisor acts on them (#7834). | | `bee-app` | Owner, once: `create` registers the app from the manifest and saves the key locally; `convert CODE` is the manual fallback. | | `bee-token` | A bee: prints a one-hour installation token scoped to one repository. | | `bees.py` | All of the above; `python3 tools/bees/bees.py self-test` needs no network and no real secret. | diff --git a/tools/bees/manifest.json b/tools/bees/manifest.json index dd197287e1..a900bd1683 100644 --- a/tools/bees/manifest.json +++ b/tools/bees/manifest.json @@ -19,6 +19,7 @@ }, "default_events": [ "pull_request", - "issue_comment" + "issue_comment", + "issues" ] }