diff --git a/core/routing.py b/core/routing.py index dc4393d..df9428c 100644 --- a/core/routing.py +++ b/core/routing.py @@ -41,9 +41,12 @@ workflow for stale content. - ``issues`` events: - - ``opened`` routes to ``triage-new-issues`` regardless of the - issue's existing labels (``ready-to-spec`` / - ``ready-to-implement`` issues still get a triage pass). + - ``opened`` routes to ``create-implementation-from-issue`` when + the issue already carries ``auto-implement``; this route bypasses + the normal bot-author drop. Other opened issues route to + ``triage-new-issues`` regardless of existing lifecycle labels + (``ready-to-spec`` / ``ready-to-implement`` issues still get a + triage pass). - ``assigned`` routes to ``create-spec-from-issue`` or ``create-implementation-from-issue`` when the assignee being added is ``oz-agent`` and the issue carries the matching @@ -57,7 +60,9 @@ webhook can post a one-shot announcement comment letting contributors know the issue is open for the matching kind of contribution and that maintainers can tag ``@oz-agent`` to start - automated work. + automated work. ``auto-implement`` is intentionally not an + ``issues.labeled`` trigger because it is only honored at issue + creation time. - ``issue_comment`` ``created`` events on a plain (non-PR) issue route to ``triage-new-issues`` when the comment carries an ``@oz-agent`` @@ -93,6 +98,7 @@ NEEDS_INFO_LABEL = "needs-info" READY_TO_SPEC_LABEL = "ready-to-spec" READY_TO_IMPLEMENT_LABEL = "ready-to-implement" +AUTO_IMPLEMENT_LABEL = "auto-implement" OZ_AGENT_MENTION = "@oz-agent" OZ_REVIEW_COMMAND = "/oz-review" @@ -252,13 +258,16 @@ def _route_issues(payload: dict[str, Any]) -> RouteDecision: Three actions are routed: - - ``opened`` triggers a fresh triage pass regardless of the - issue's existing labels. Issues that arrive with prior - lifecycle labels (``ready-to-spec``, ``ready-to-implement``, - etc.) — for example because they were imported from another - repo or re-opened — still get a triage pass so the bot can - post a fresh progress comment and pick up any state changes - that landed while the issue was closed. + - ``opened`` triggers ``create-implementation-from-issue`` when + the issue already carries ``auto-implement``. That label is a + trusted issue-creation-time shortcut and is checked before the + normal bot-author drop. Other opened issues trigger a fresh + triage pass regardless of the issue's existing labels. Issues + that arrive with prior lifecycle labels (``ready-to-spec``, + ``ready-to-implement``, etc.) — for example because they were + imported from another repo or re-opened — still get a triage + pass so the bot can post a fresh progress comment and pick up + any state changes that landed while the issue was closed. - ``assigned`` triggers ``create-spec-from-issue`` or ``create-implementation-from-issue`` when the assignee being added is ``oz-agent`` itself and the issue carries the @@ -287,6 +296,12 @@ def _route_issues(payload: dict[str, Any]) -> RouteDecision: None, f"issues.{action} delivered for a pull request" ) if action == "opened": + labels = _label_names(issue.get("labels")) + if AUTO_IMPLEMENT_LABEL in labels: + return RouteDecision( + WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE, + "auto-implement label on newly opened issue", + ) if _is_bot(issue.get("user")): return RouteDecision(None, "issue authored by automation user") return RouteDecision( @@ -465,6 +480,7 @@ def route_event(event: str, payload: dict[str, Any]) -> RouteDecision: __all__ = [ + "AUTO_IMPLEMENT_LABEL", "NEEDS_INFO_LABEL", "OZ_AGENT_LOGIN", "OZ_AGENT_MENTION", diff --git a/specs/GH447/product.md b/specs/GH447/product.md new file mode 100644 index 0000000..207f30a --- /dev/null +++ b/specs/GH447/product.md @@ -0,0 +1,107 @@ +# Issue #447: Add `auto-implement` label that skips triage and opens a draft PR + +## Product Spec + +### Summary + +Maintainers need a one-step way to send trusted, implementation-ready issues directly to the implementation agent. When a newly opened issue already has the `auto-implement` label, Oz should skip the normal triage and spec stages, dispatch the existing implementation workflow, share the run session in the progress comment, and let the existing completion path open or update a draft implementation PR. + +This behavior is limited to labels present at issue creation time. Adding `auto-implement` to an existing issue after it has been opened should not start work. + +### Problem + +Every new issue currently goes through the `triage-new-issues` route. That is useful for normal public intake, but it slows down trusted intake pipelines where maintainers or authorized bots already know the issue is valid, scoped, and ready for implementation. Those pipelines must wait for triage and then move the issue through `ready-to-spec` or `ready-to-implement` with `oz-agent` assignment before the implementation agent starts. + +The desired label lets trusted labelers express that decision at the moment the issue is created, without introducing a new workflow or duplicating implementation behavior. + +### Goals + +- Honor `auto-implement` when it is already present on a plain issue in the `issues.opened` webhook payload. +- Skip `triage-new-issues` for those newly opened issues so no triage progress comment or triage result is posted. +- Dispatch the existing `create-implementation-from-issue` workflow directly. +- Preserve the existing implementation workflow behavior for assignment, progress comments, session link updates, draft PR creation, and PR-link reporting. +- Treat the label itself as the authorization gate; do not require the issue to already be assigned to `oz-agent`. +- Honor the label even when the issue author is a bot account, because authorized intake bots may file and label issues in one step. +- Keep all existing routing unchanged for issues that do not have `auto-implement` at creation time. + +### Non-goals + +- Adding a new implementation workflow. +- Changing how `create-implementation-from-issue` builds prompts, creates progress comments, uploads artifacts, or opens draft PRs. +- Creating or managing the GitHub `auto-implement` label automatically. +- Dispatching implementation when `auto-implement` is added to an already-open issue. +- Changing `ready-to-spec`, `ready-to-implement`, `plan-approved`, or `@oz-agent` mention behavior. +- Skipping GitHub webhook signature verification, repository installation checks, or any existing dispatch preflight outside routing. + +### Figma / design references + +Figma: none provided. This is a GitHub automation behavior with no product UI beyond labels, issue comments, Oz session links, and draft pull requests. + +### User experience + +#### Scenario: trusted maintainer opens a labeled issue + +1. A maintainer opens a new plain issue with `auto-implement` already applied. +2. The webhook router receives `issues.opened`. +3. Oz does not dispatch `triage-new-issues`. +4. Oz dispatches `create-implementation-from-issue`. +5. The existing implementation flow posts or updates its progress comment on the issue. +6. As the cloud agent runs, the existing poller updates the progress comment with the session link. +7. When the agent produces changes, the existing implementation completion path opens or updates a draft PR. +8. The issue progress comment includes the PR link when the workflow completes. + +#### Scenario: trusted intake bot opens a labeled issue + +1. An authorized bot account opens a new issue with `auto-implement` already applied. +2. Even though normal issue-opened routing ignores bot-authored issues, this event is accepted because the trusted label is present. +3. Oz dispatches `create-implementation-from-issue` with the same behavior as a maintainer-authored labeled issue. + +#### Scenario: normal issue opens without the label + +1. A new plain issue is opened without `auto-implement`. +2. Existing routing behavior is unchanged. +3. Non-bot-authored issues continue to route to `triage-new-issues`. +4. Bot-authored issues without the label continue to be dropped by the automation-author guard. + +#### Scenario: label is added after issue creation + +1. An issue is already open. +2. A maintainer or bot adds `auto-implement`. +3. The `issues.labeled` event is not treated as an implementation trigger. +4. Oz logs and drops the label event the same way it drops other unhandled issue labels. +5. Maintainers can still use existing promotion paths such as `ready-to-implement` plus `oz-agent` assignment or an `@oz-agent` mention on an issue that is already ready to implement. + +#### Behavior rules + +1. **Only `issues.opened` can use `auto-implement`.** The label is read from the issue's labels at creation time. +2. **Plain issues only.** Pull requests mirrored through the `issues` event remain ignored by the issue router. +3. **The label bypasses the bot-author drop.** Bot-authored issues are still dropped by default, but not when `auto-implement` is present on `issues.opened`. +4. **The label bypasses `oz-agent` assignment requirements.** The implementation workflow can perform its existing best-effort assignment behavior. +5. **No triage side effects occur for the bypass path.** A newly opened issue with `auto-implement` should not receive a triage comment, triage labels, or triage recommendations from this event. +6. **Existing lifecycle labels keep their current meaning.** `ready-to-spec` and `ready-to-implement` do not become aliases for `auto-implement` on issue creation. +7. **Unhandled label events remain safe no-ops.** `issues.labeled` for `auto-implement` should produce no workflow dispatch. + +### Success criteria + +1. A newly opened plain issue with `auto-implement` routes to `create-implementation-from-issue`. +2. A newly opened plain issue with `auto-implement` does not route to `triage-new-issues`. +3. A newly opened plain issue with `auto-implement` authored by a bot account still routes to `create-implementation-from-issue`. +4. A newly opened issue without `auto-implement` preserves existing behavior, including triage for normal users and no dispatch for bot-authored issues. +5. Adding `auto-implement` through an `issues.labeled` event does not dispatch any workflow. +6. Existing `ready-to-spec` and `ready-to-implement` routing tests continue to pass unchanged. +7. The implementation flow still surfaces the session link and PR link through the existing progress-comment and poller behavior. + +### Validation + +- Add unit tests in `tests/test_routing.py` for: + - `issues.opened` with `auto-implement` routing to `create-implementation-from-issue`. + - `issues.opened` with `auto-implement` from a bot author still routing to `create-implementation-from-issue`. + - `issues.opened` without `auto-implement` preserving existing triage and bot-drop behavior. + - `issues.labeled` with `auto-implement` returning no workflow and a dropped/unhandled-label reason. +- Run the routing test module after implementation. +- Run the repository test suite or the closest available CI-equivalent command when practical. +- Inspect `core/routing.py` documentation to confirm the new label is described alongside existing issue routes. + +### Open questions + +None. The issue defines the trust boundary, scope, and desired routing behavior clearly. diff --git a/specs/GH447/tech.md b/specs/GH447/tech.md new file mode 100644 index 0000000..9ec4215 --- /dev/null +++ b/specs/GH447/tech.md @@ -0,0 +1,160 @@ +# Issue #447: Add `auto-implement` label that skips triage and opens a draft PR + +## Tech Spec + +### Problem + +The webhook router currently sends every non-bot plain `issues.opened` event to `triage-new-issues`, regardless of existing labels. It also drops bot-authored opened issues before any workflow dispatch. Issue #447 requires a narrow routing exception: if a newly opened plain issue already carries `auto-implement`, route directly to `create-implementation-from-issue`, including for bot-authored issues, and do not route to triage. + +The implementation should not add a workflow. The existing `create-implementation-from-issue` workflow already gathers issue context, best-effort assigns `oz-agent`, creates or updates the progress comment, exposes session links through cron polling, consumes `pr-metadata.json`, and opens or updates a draft PR. + +### Relevant code + +- `core/routing.py:1` — module docstring documenting webhook routing behavior. +- `core/routing.py:83` — workflow identifier constants, including `WORKFLOW_TRIAGE_NEW_ISSUES` and `WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE`. +- `core/routing.py:94` — issue lifecycle label constants for `ready-to-spec` and `ready-to-implement`. +- `core/routing.py (124-136)` — `_label_names()`, which normalizes label objects from webhook payloads. +- `core/routing.py (147-164)` — `_is_bot()`, which currently powers the bot-author drop. +- `core/routing.py (254-342)` — `_route_issues()`, the target for the routing change. +- `core/builders.py (94-103)` — `build_create_implementation_request()`, which delegates implementation dispatch to the existing workflow. +- `core/workflows/__init__.py (577-632)` — `CreateImplementationWorkflow`, which builds the dispatch request and progress-comment spec. +- `core/workflows/create_implementation_from_issue.py (143-286)` — `gather_create_implementation_context()`, which assigns `oz-agent` if missing, resolves spec context when present, and prepares progress-comment state. +- `core/workflows/create_implementation_from_issue.py (287-326)` — implementation prompt construction for the cloud run. +- `core/workflows/create_implementation_from_issue.py (329-470)` — result application that opens or updates draft implementation PRs and updates the issue progress comment. +- `tests/test_routing.py (45-126)` — current `issues.opened` and bot-author routing tests. +- `tests/test_routing.py (232-274)` — current issue-label routing tests, including unhandled-label drops. + +### Current state + +`_route_issues()` handles plain issue events in this order: + +1. Reject missing issue payloads. +2. Drop pull requests delivered through the `issues` event. +3. For `action == "opened"`: + - drop if `_is_bot(issue["user"])` is true. + - otherwise return `WORKFLOW_TRIAGE_NEW_ISSUES`. +4. For `assigned`, dispatch spec or implementation only when the added assignee is `oz-agent` and the issue has the matching lifecycle label. +5. For `labeled`, dispatch spec or implementation only for `ready-to-spec` or `ready-to-implement` with an existing `oz-agent` assignee; otherwise announce availability or drop unhandled labels. + +This means a trusted `auto-implement` issue currently either routes to triage or is dropped if the author is a bot. The existing implementation workflow does not require a preexisting `oz-agent` assignee because `gather_create_implementation_context()` best-effort adds the assignee when it is missing. + +### Proposed changes + +#### 1. Add an `auto-implement` label constant + +Add a new constant in `core/routing.py` near the other label constants: + +- `AUTO_IMPLEMENT_LABEL = "auto-implement"` + +Export it from `__all__` for consistency with the existing lifecycle label constants. Tests do not need to import it, but exporting keeps the routing module's public surface complete. + +#### 2. Update the module docstring + +Update the `issues` section of `core/routing.py` to document that: + +- `issues.opened` with `auto-implement` routes directly to `create-implementation-from-issue`. +- This route bypasses the normal bot-author drop. +- `issues.labeled` with `auto-implement` is intentionally not a trigger. + +Keep the docstring clear that normal opened issues still route to triage. + +#### 3. Route `issues.opened` with `auto-implement` before the bot-author drop + +Change only the `action == "opened"` branch in `_route_issues()`: + +1. Normalize labels with `_label_names(issue.get("labels"))`. +2. If `AUTO_IMPLEMENT_LABEL` is present, return `RouteDecision(WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE, "...")`. +3. Only after that, apply the existing `_is_bot(issue.get("user"))` drop. +4. Otherwise keep returning `WORKFLOW_TRIAGE_NEW_ISSUES`. + +The pull-request mirror guard should remain before this branch so a PR delivered as an `issues.opened` payload is still ignored. + +The decision reason should make logs distinguish this path from the existing `ready-to-implement` route, for example `auto-implement label on newly opened issue`. + +#### 4. Leave `issues.labeled` behavior unchanged except for tests + +Do not add `AUTO_IMPLEMENT_LABEL` to the set of routed lifecycle labels in the `action == "labeled"` branch. The existing unhandled-label path should handle it and return `workflow=None`. + +This preserves the non-goal that applying `auto-implement` after issue creation is a no-op. + +#### 5. Do not change implementation workflow internals + +No changes are required in `core/builders.py`, `core/workflows/__init__.py`, `core/workflows/create_implementation_from_issue.py`, or cron handlers. Routing to `WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE` is enough for the builder registry to reuse the current dispatch path. + +The implementation prompt will receive issue labels and assignees as gathered from GitHub at dispatch time. If no approved spec or repository spec exists, the existing prompt explicitly tells the implementation agent that no approved or repository spec context was found, which is acceptable for the `auto-implement` bypass. + +### End-to-end flow + +#### Flow A: human-authored auto-implement issue + +1. GitHub sends `issues.opened`. +2. `route_event("issues", payload)` calls `_route_issues()`. +3. `_route_issues()` rejects PR mirrors, reads issue labels, finds `auto-implement`, and returns `WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE`. +4. The webhook handler evaluates the route with the existing builder registry. +5. `CreateImplementationWorkflow.build_dispatch()` gathers implementation context and creates a progress-comment spec. +6. The dispatcher starts the cloud run and stores run state. +7. Cron polling updates the session link and applies the completed implementation artifact by opening or updating a draft PR. + +#### Flow B: bot-authored auto-implement issue + +1. GitHub sends `issues.opened` for an issue authored by a bot account. +2. `_route_issues()` checks labels before `_is_bot(issue["user"])`. +3. Because `auto-implement` is present, routing returns `WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE`. +4. The normal implementation dispatch and completion flow runs. + +#### Flow C: issue without auto-implement + +1. GitHub sends `issues.opened`. +2. `_route_issues()` does not find `auto-implement`. +3. Existing behavior continues: + - bot-authored issues are dropped. + - non-bot issues route to `triage-new-issues`. + +#### Flow D: auto-implement added later + +1. GitHub sends `issues.labeled` with `label.name == "auto-implement"`. +2. `_route_issues()` enters the existing labeled branch. +3. Because the label is not `ready-to-spec` or `ready-to-implement`, routing returns `workflow=None` with the unhandled-label reason. +4. No implementation run starts. + +### Risks and mitigations + +**Risk: trusted bot bypass unintentionally applies to all bot-authored issues.** +Mitigation: place the bypass behind the explicit `auto-implement` label check only. Bot-authored issues without the label continue to be dropped. + +**Risk: adding the label later unexpectedly starts implementation.** +Mitigation: do not route `AUTO_IMPLEMENT_LABEL` in the `issues.labeled` branch. Add a regression test for this exact event. + +**Risk: implementation runs without spec context.** +Mitigation: this is intentional for `auto-implement`. The existing implementation workflow already supports the no-spec-context case when no unapproved spec PR blocks execution, and the agent fetches the issue body and comments as implementation input. + +**Risk: accidental triage and implementation double dispatch on issue creation.** +Mitigation: return a single `RouteDecision` from `_route_issues()` for `issues.opened`. The auto-implement branch should return before the triage branch. + +**Risk: label name case or whitespace variations.** +Mitigation: `_label_names()` already strips whitespace but does not lower-case label names. Keep exact matching to align with existing label constants and GitHub label conventions. + +### Testing and validation + +Add unit tests in `tests/test_routing.py` under `IssuesEventTest`: + +- `test_issues_opened_with_auto_implement_routes_to_create_implementation` + - Payload: `action="opened"`, issue labels include `auto-implement`, normal user. + - Expected: `WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE`. +- `test_issues_opened_with_auto_implement_from_bot_routes_to_create_implementation` + - Payload: `action="opened"`, issue labels include `auto-implement`, issue user is a bot. + - Expected: `WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE`. +- Preserve or update existing tests that prove `issues.opened` without `auto-implement` routes to triage for normal users and drops bot authors. +- `test_auto_implement_label_added_to_existing_issue_is_dropped` + - Payload: `action="labeled"`, `label.name="auto-implement"`, issue labels include `auto-implement`. + - Expected: `workflow is None` and reason contains `unhandled label`. + +Run: + +- `python -m unittest tests.test_routing` +- The broader repository test command used by CI, if available in the environment. + +### Follow-ups + +- Operators should create and document the `auto-implement` label in repositories that opt into this behavior. +- If maintainers later want promotion-after-open semantics, that should be a separate product decision because it changes the trust and dispatch model for `issues.labeled`. diff --git a/tests/test_routing.py b/tests/test_routing.py index 50184bf..2951194 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -14,6 +14,7 @@ from . import conftest # noqa: F401 from core.routing import ( + AUTO_IMPLEMENT_LABEL, OZ_AGENT_LOGIN, RouteDecision, WORKFLOW_ANNOUNCE_READY_ISSUE, @@ -54,6 +55,21 @@ def test_issues_opened_routes_to_triage(self) -> None: decision = route_event("issues", {"action": "opened", "issue": _issue()}) self.assertEqual(decision.workflow, WORKFLOW_TRIAGE_NEW_ISSUES) + def test_issues_opened_with_auto_implement_routes_to_create_implementation( + self, + ) -> None: + decision = route_event( + "issues", + { + "action": "opened", + "issue": _issue(labels=[AUTO_IMPLEMENT_LABEL]), + }, + ) + self.assertEqual( + decision.workflow, WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE + ) + self.assertIn(AUTO_IMPLEMENT_LABEL, decision.reason) + def test_issues_opened_on_triaged_issue_still_routes_to_triage(self) -> None: # Even issues that already carry post-triage labels (``triaged``, # ``ready-to-spec``, ``ready-to-implement``) should get a fresh @@ -92,6 +108,23 @@ def test_issues_opened_for_bot_author_is_dropped(self) -> None: ) self.assertIsNone(decision.workflow) + def test_issues_opened_with_auto_implement_from_bot_routes_to_create_implementation( + self, + ) -> None: + decision = route_event( + "issues", + { + "action": "opened", + "issue": _issue( + labels=[AUTO_IMPLEMENT_LABEL], + user={"login": "trusted-intake[bot]", "type": "Bot"}, + ), + }, + ) + self.assertEqual( + decision.workflow, WORKFLOW_CREATE_IMPLEMENTATION_FROM_ISSUE + ) + def test_oz_agent_assigned_to_ready_to_implement_routes_to_create_implementation( self, ) -> None: @@ -314,6 +347,20 @@ def test_unrelated_label_added_to_issue_is_dropped(self) -> None: self.assertIsNone(decision.workflow) self.assertIn("unhandled label", decision.reason) + def test_auto_implement_label_added_to_existing_issue_is_dropped(self) -> None: + decision = route_event( + "issues", + { + "action": "labeled", + "label": {"name": AUTO_IMPLEMENT_LABEL}, + "issue": _issue( + labels=[AUTO_IMPLEMENT_LABEL], assignees=[OZ_AGENT_LOGIN] + ), + }, + ) + self.assertIsNone(decision.workflow) + self.assertIn("unhandled label", decision.reason) + def test_issues_edited_event_is_dropped(self) -> None: # ``edited`` and other actions outside of # ``opened``/``assigned``/``labeled`` should still fall