feat: migrate control plane to vercel webhook and consolidate agents on cloud runs#400
feat: migrate control plane to vercel webhook and consolidate agents on cloud runs#400captainsafia wants to merge 44 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Round 2: Vercel control plane wiringThis round adds the control-plane wiring that turns the scaffolding from round 1 into an end-to-end PR-flow runtime. Every PR-triggered webhook now flows through the Vercel control plane (webhook → builder → cloud-agent dispatch → KV → cron → apply-result-to-GitHub). Commits
What's live
The issue-triggered workflows and the plan-approval workflows are still routed by Validation
Operator notes
The new Out of scope
The legacy GitHub Actions paths still pass all tests and continue to handle every workflow during the cutover. |
|
I'm running I couldn't run Powered by Oz |
|
I'm working on changes requested in this PR (responding to a PR conversation comment). You can view the conversation on Warp. I pushed changes to this PR based on the comment. Next steps:
Powered by Oz |
Fix
|
| Suite | Result |
|---|---|
python -m pytest control-plane/tests |
120 passed, 14 subtests passed |
PYTHONPATH=.github/scripts python -m unittest discover -s .github/scripts/tests |
494 passed |
python -m compileall control-plane/api control-plane/lib .github/scripts/oz_workflows |
clean |
|
Pushed What was failing
Fixes
Vercel deployment — likely needs project-side changeThe Vercel deployment fails almost immediately, which usually means Vercel rejected the build before running any commands. The previous The most likely cause is the Vercel project's Root Directory setting (Project Settings → General → Root Directory) — if it's still pinned to |
… them
The previous setup relied on a vercel.json installCommand that copied
.github/scripts/oz_workflows and the four PR-flow entrypoints into
control-plane/lib/ at build time. That approach assumed Vercel would
clone the entire repo onto the build runner, but Vercel's project
root is set to control-plane/ and Vercel does not pull files outside
the project root by default. The install script silently couldn't
locate the source on the build runner, so the function bundle was
deployed without oz_workflows, producing:
{"error": "webhook runtime not ready: No module named 'oz_workflows'"}
This commit checks the mirrored copies into the branch directly so
they ship with the function bundle regardless of Vercel's source
inclusion settings. The vendored copies live at
control-plane/lib/oz_workflows/ and control-plane/lib/scripts/ and
are no longer in .gitignore.
The vercel.json installCommand is dropped so Vercel uses its default
'pip install -r requirements.txt' for runtime deps.
scripts/vercel_install.sh is retained as a local refresh helper that
contributors run manually after editing .github/scripts/oz_workflows
or any of the four PR-flow entrypoints. CI does not run it; Vercel
does not run it.
A future improvement worth tracking: a pre-commit hook (or CI job)
that fails if control-plane/lib/oz_workflows is out of sync with
.github/scripts/oz_workflows, so contributors cannot merge without
running the refresh.
Co-Authored-By: Oz <oz-agent@warp.dev>
…before dispatch The Vercel webhook handler dispatched cloud agent runs with bare skill names like 'review-pr', 'implement-issue', and 'verify-pr', which the Oz API rejects with: invalid skill_spec format: missing ':' separator. Expected format: 'repo:skill' or 'org/repo:skill' The legacy oz_workflows.oz_client.skill_spec resolves these by checking the local filesystem, but the Vercel function has no filesystem access to the skill files so it cannot use that path. This change adds cloud_skill_spec(skill_name) in control-plane/lib/dispatch.py that formats a bare skill name into <workflow_code_repo>:.agents/skills/<name>/SKILL.md defaulting <workflow_code_repo> to 'warpdotdev/oz-for-oss' (the repo that hosts the bundled core skills) and allowing forks to override via the WORKFLOW_CODE_REPOSITORY env var. dispatch_run now calls cloud_skill_spec on request.skill_name before passing it to the Oz SDK runner, so the skill arrives at the API in the required form. Repo-local override skills (e.g. 'review-pr-local' in the consuming repo) remain referenced inside the prompt body via the existing oz_workflows.repo_local.format_repo_local_prompt_section helper; this change only fixes the dispatch-side core skill spec. Adds 7 tests for cloud_skill_spec covering the default repo, the env-var override, the kwarg override, the already-qualified pass-through, the SKILL.md suffix path, the empty-string case, and the None-skill dispatch path. Co-Authored-By: Oz <oz-agent@warp.dev>
…reconstructed by callers The Vercel control plane needs to drive the WorkflowProgressComment lifecycle across the webhook → cron seam: the webhook posts the 'starting...' comment at dispatch time and the cron poller updates the same comment as the run progresses. To support that without forking the helper, two changes are needed: - WorkflowProgressComment now accepts optional comment_id, run_id, oz_run_id, and session_link kwargs. Callers (the cron's result_applier and failure_handler) construct an instance bound to the comment posted at dispatch time so progress.complete / progress.replace_body / progress.report_error edit that exact comment instead of falling back to the workflow-prefix lookup. Defaults preserve the legacy GitHub Actions runtime contract. - apply_review_result / apply_pr_comment_result / apply_verification_result / apply_issue_association_result now accept an optional progress kwarg. When supplied, the helper reuses the caller's WorkflowProgressComment instead of constructing a fresh one. Production GHA callers that omit progress see no behavior change. Co-Authored-By: Oz <oz-agent@warp.dev>
Builders now drive the user-facing progress comment lifecycle that the legacy GitHub Actions `main()` entrypoints used to: each builder constructs a WorkflowProgressComment for the originating issue/PR, posts the workflow-specific 'starting...' status line, and stashes progress_comment_id + progress_run_id onto DispatchRequest.payload_subset so the cron poller can reconstruct the same instance when the run terminates. The review-pr / respond-to-pr-comment / verify-pr-comment builders post the start comment directly via _start_progress_comment. The enforce-pr-issue-state builder hands its progress instance to the synchronous helper, which already drives the start line for the need-cloud-match branch. Without this, real PR webhook deliveries dispatched a cloud agent run but never produced any user-visible 'Oz is starting to review...' / '/oz-verify is running...' comments, and operators had no way to follow run progress. Co-Authored-By: Oz <oz-agent@warp.dev>
The cron poller now keeps the originating progress comment in sync with the run's lifecycle: - A new non_terminal_handler protocol on WorkflowHandlers fires on every pending poll. Each PR-flow handler reconstructs the WorkflowProgressComment from the persisted payload_subset (progress_comment_id / progress_run_id) and calls record_run_session_link so the session-share link surfaces in the user-visible comment as soon as Oz reports it. Failures are absorbed. - Each result_applier reconstructs the same progress instance and passes it through to the workflow-specific apply_*_result helper so the final progress.complete / progress.replace_body call edits the comment posted at dispatch time instead of creating a sibling comment. - Each failure_handler also reconstructs the progress instance and invokes progress.report_error, producing an in-place error message rather than orphaning the in-flight progress comment. Together with the builder-side start, this restores the user-facing 'Oz is starting to review...' → '...session link...' → 'Oz completed the review' progression on the originating PR / issue when runs are dispatched through the Vercel control plane. Co-Authored-By: Oz <oz-agent@warp.dev>
Mirrors the helpers + apply-result signature changes from .github/scripts/ into control-plane/lib/. The vendored copies are the canonical Vercel-side source — Vercel does not run scripts/vercel_install.sh, so any edit to .github/scripts/ only reaches the function bundle through this committed mirror. Co-Authored-By: Oz <oz-agent@warp.dev>
…GH Actions
Move the webhook + cron control plane out of `control-plane/` into the
repository root so the Vercel function bundle reads `api/`, `lib/`,
`tests/`, `vercel.json`, and `requirements.txt` directly without a
mirror step. The legacy `control-plane/scripts/vercel_install.sh`
mirror is no longer needed: `lib/oz_workflows/` and `lib/scripts/` are
now the only canonical copies.
PR-driven bot behavior (`review-pull-request`, `enforce-pr-issue-state`,
`respond-to-pr-comment`, `verify-pr-comment`) now flows exclusively
through the webhook. The matching GitHub Actions wrappers and Python
entrypoints have been deleted, along with the `pr-hooks.yml` umbrella
workflow that used to fan them out:
- .github/workflows/{enforce-pr-issue-state, pr-hooks,
respond-to-pr-comment*, review-pull-request, verify-pr-comment*}.yml
- .github/scripts/{enforce_pr_issue_state, resolve_review_context,
respond_to_pr_comment, review_pr, verify_pr_comment}.py
- The accompanying tests under .github/scripts/tests/
Issue-triggered (`triage`, `create-spec-from-issue`,
`create-implementation-from-issue`, `respond-to-triaged-issue-comment`,
`comment-on-unready-assigned-issue`, `comment-on-ready-to-{spec,
implement}`) and plan-approval (`comment-on-plan-approved`,
`trigger-implementation-on-plan-approved`,
`remove-stale-issue-labels-on-plan-approved`) workflows stay on the
GitHub Actions delivery path because they clone the repo, push
branches, and open PRs from inside the runner. The router in
`lib/routing.py` deliberately drops `issues` events and `issue_comment`
events on plain issues so the webhook does not double-fire alongside
the workflow.
Other supporting changes:
- Convert `run-tests.yml` from a `workflow_call` shim into a standalone
PR-triggered workflow that runs both the webhook tests
(`python -m pytest tests`) and the helper unit tests
(`PYTHONPATH=lib:.github/scripts python -m unittest discover -s
.github/scripts/tests`).
- Update `.github/actions/run-oz-python-script` to set
`PYTHONPATH=lib:.github/scripts` and install dependencies from the
top-level `requirements.txt` so the issue-triggered + self-improvement
workflows can still resolve `oz_workflows` after the move.
- Update `.github/actions/setup-oz-python` to default to the top-level
`requirements.txt`.
- Refresh `README.md`, `CONTRIBUTING.md`, and `docs/platform.md` to
describe the two delivery surfaces, the routing split, and the new
paths.
Validation:
- python -m pytest tests \u2192 110 passed, 14 subtests passed
- PYTHONPATH=lib:.github/scripts python -m unittest discover \\
-s .github/scripts/tests \u2192 362 passed
Co-Authored-By: Oz <oz-agent@warp.dev>
The new `run-tests.yml` workflow runs `python -m pytest tests` against the webhook control-plane test suite, but pytest is intentionally not listed in `requirements.txt` (those are runtime dependencies for the Vercel function bundle). The CI job therefore failed with `No module named pytest`. Install the test-only dependencies (`pytest` + `pytest-subtests`) in the workflow before invoking pytest, and document the same install in `CONTRIBUTING.md` so local setups stay aligned with CI. Validation: 110 passed, 14 subtests passed (python -m pytest tests) Co-Authored-By: Oz <oz-agent@warp.dev>
Migrates the triage-new-issues workflow off the GitHub Actions delivery surface and onto the Vercel webhook + cron control plane that already serves the PR-driven workflows in this branch. - Routing: issues.opened on a non-triaged issue routes to triage-new-issues; plain issue_comment events route to triage when they carry @oz-agent on a non-triaged issue or are a needs-info reply from the original reporter. @oz-agent mentions on triaged issues stay on the GitHub Actions respond-to-triaged path. - Builder: build_triage_request gathers issue + dedupe + triage-config context via PyGithub, posts the WorkflowProgressComment start line, and stuffs progress_comment_id / progress_run_id onto payload_subset so the cron poller can edit the same comment. - Handler: build_triage_handlers wires up load_triage_artifact, the cloud-mode applier, the failure handler, and the non-terminal session-link refresh. - Cloud-mode helpers: lib/scripts/triage_new_issues.py now exposes TriageContext, gather_triage_context, build_triage_prompt_for_dispatch, and apply_triage_result_for_dispatch alongside the legacy main() entrypoint and the pure helpers consumed by the unit tests. - Cleanup: deletes .github/workflows/triage-new-issues{,-local}.yml, the .github/scripts/triage_new_issues.py shim, and moves the triage unit tests under tests/. - Docs: README, api/cron.py, and lib/routing.py docstrings now describe the triage delivery path. - Tests: tests/test_routing.py covers the new issues + plain-issue routing; tests/test_builders.py and tests/test_handlers.py cover the new builder and handler; the relocated tests/test_triage.py preserves the existing pure-helper coverage. Co-Authored-By: Oz <oz-agent@warp.dev>
The pinned uv version (0.11.7 / 0.11.8) in uv.toml was too strict for Vercel's bundled uv (0.10.11) and caused production deploys to fail with 'Required uv version does not match the running version'. Removing uv.toml lets each environment use whatever uv it ships with; GitHub Actions can pin via the action's own input if needed. Also extend .gitignore with the local-only artifacts that have been showing up in working trees: .vercel CLI output (no trailing slash so it matches both file and dir), .env*.local for the per-environment secret files, and .DS_Store from macOS. Co-Authored-By: Oz <oz-agent@warp.dev>
Previously the webhook routing skipped issues that already carried the 'triaged' label: 'issues.opened' deliveries returned None, and '@oz-agent' mentions on triaged issues were left to the legacy 'respond-to-triaged-issue-comment' GitHub Actions workflow. That workflow in turn excludes issues with 'ready-to-spec' or 'ready-to-implement' (see .github/workflows/respond-to-triaged-issue-comment.yml:30), so issues that progressed past triage fell into a routing gap and never re-triaged on new comments. Drop both gates: every 'issues.opened' delivery now triggers triage (useful when an issue is reopened or imported with prior labels), and every '@oz-agent' mention on a non-PR issue routes to triage so the bot picks up new context from the conversation regardless of the issue's lifecycle stage. Operators can remove the 'respond-to-triaged-issue-comment' GitHub Actions workflow once they're comfortable letting triage handle every mention; for now leaving it in place will simply cause a duplicate inline-response comment alongside the triage progress comment for 'triaged'-but-not-'ready' issues. Updates two existing tests to assert the new behavior and adds two new tests covering the 'ready-to-implement' label path. Co-Authored-By: Oz <oz-agent@warp.dev>
Resolves #399. The PR review workflow used to request review from every matching stakeholder (capped at 3) by iterating the agent's recommended_reviewers list in order. That over-notified maintainers and gave the assignment no randomness, so the same person tended to get pinged on every PR that touched their files. This change reworks _normalize_reviewer_logins to: - Build the full eligible candidate pool (preserving the existing filtering: dedup, strip @-prefix, drop the PR author, require membership in .github/STAKEHOLDERS when allowed_logins is set). - Uniformly sample sample_size logins from that pool using random.sample (without replacement). The default sample_size is the new _REVIEWER_SAMPLE_SIZE = 1 constant. Callers can pass a different size and an injected random.Random instance for deterministic tests. Also updates the agent-facing prompt (in both gather_review_context and the legacy main() path) to clarify that the workflow uniformly samples exactly one reviewer from the candidates the agent returns, so the agent should provide a meaningful pool rather than picking the single 'best' candidate itself. Adds 16 tests (NormalizeReviewerLoginsTest + ResolveNonMemberReviewActionTest) covering: default sample size of 1, single-pick semantics, uniform distribution across the pool, PR-author exclusion (case-insensitive), dedup, @-prefix stripping, allowed_logins gating, empty-pool / non-list / sample-size=0 short circuits, sample-size > pool fallback, larger explicit sample sizes, and the APPROVE / REQUEST_CHANGES branches in _resolve_non_member_review_action. Co-Authored-By: Oz <oz-agent@warp.dev>
The agent now only ever takes a REQUEST_CHANGES action against a PR. Positive verdicts are posted as plain COMMENT reviews so a human still has to actually approve; the reviewer-request still pings a randomly sampled stakeholder so the PR is handed off to a maintainer. - _resolve_non_member_review_action maps verdict=APPROVE -> event=COMMENT while preserving recommended_reviewers; verdict=REQUEST_CHANGES is left as event=REQUEST_CHANGES with no reviewer request. - apply_review_result and the legacy main() now post the GitHub review only when there is real content (or the event is REQUEST_CHANGES) and always issue the reviewer request when recommended_reviewers is set. - The empty-feedback short-circuit now also requires no reviewers so an APPROVE verdict with no inline comments still pings a human. - _format_review_completion_message no longer claims the bot approved the PR; the COMMENT-with-reviewers branch says I left feedback as a comment so a maintainer can approve. - Updated the non-member-review prompt in both gather_review_context and legacy main() to explain that APPROVE will be downgraded to COMMENT and that a human reviewer is the one who actually approves. - Tests in tests/test_review_pr_reviewer_sampling.py updated to assert the new mapping and the rewritten completion message. Co-Authored-By: Oz <oz-agent@warp.dev>
The triage workflow now accepts a `comment_type` discriminator on `triage_result.json` so the agent can return either a structured triage comment or a lighter issue-thread response when answering a follow-up question on an already-triaged issue. - `comment_type="triage"` (default; backwards compatible) drives the existing structured comment with statements, follow-up questions, duplicate detection, and a maintainer-details expando, plus the label mutations applied by `apply_triage_result`. - `comment_type="response"` drives a brief user-facing reply (`response_body`) with a maintainer-only Reasoning expando (`details`). The workflow does NOT change any labels in this mode so the issue's lifecycle state stays as the maintainer left it. Routing was already correct: `@oz-agent` mentions on plain issues route to `WORKFLOW_TRIAGE_NEW_ISSUES` regardless of triaged / needs-info / ready-to-implement state. The routing docstrings were updated to call out that the workflow itself picks the comment shape from the discriminator on its result payload. - Added `COMMENT_TYPE_TRIAGE`, `COMMENT_TYPE_RESPONSE`, `ALLOWED_COMMENT_TYPES`, `RESPONSE_DETAILS_SUMMARY`, and `RESPONSE_FALLBACK_BODY` constants. - Added `extract_comment_type`, `extract_response_body`, `extract_response_details`, and `build_response_comment_body` helpers. Unknown / non-string `comment_type` values fall back to `triage` so a malformed payload never produces a half-rendered response. - `apply_triage_result_for_dispatch` (cloud path) and the legacy `process_issue` GHA path branch on `comment_type` before calling `apply_triage_result`. Response mode replaces the progress comment with the response shape and skips all label mutations. - The triage prompt now documents both shapes and tells the agent when to pick each one. - Tests cover the discriminator extraction, response field normalization, response comment markdown layout, and the apply-result dispatch (response mode skips labels; unknown `comment_type` falls back to triage and applies labels). Co-Authored-By: Oz <oz-agent@warp.dev>
The README has grown to ~260 lines mixing repository layout, webhook flow, GitHub Actions plumbing, onboarding instructions, and local development notes. Cut it back to a short intro plus a documentation index, and move the moved content into: - docs/architecture.md — repo layout, webhook flow, GHA workflows table - docs/onboarding.md — GitHub App + Vercel + GHA secrets walkthrough Local development notes already live in CONTRIBUTING.md, so the README now links there directly instead of duplicating them. Co-authored-by: Oz <oz-agent@warp.dev>
… members
The control plane previously refused to dispatch a respond-to-pr-comment
or verify-pr-comment run unless the triggering comment's author was an
organization member, owner, or collaborator (or passed an
`/orgs/{org}/members/{login}` probe). The bot now responds to every
human-authored comment; the only authors filtered out are automation
accounts, which the existing `is_automation_user` check already drops.
- `lib/scripts/respond_to_pr_comment.py:main()` no longer calls
`is_trusted_commenter`. The bot-author short-circuit at the top of
`main()` is the only filter that remains.
- `lib/scripts/verify_pr_comment.py:main()` does the same for the
`/oz-verify` command.
- The respond-to-pr-comment prompt no longer claims the workflow has
pre-screened the commenter as a trusted org member. The new copy
reminds the agent to treat all fetched PR / comment content as
untrusted (per the security rules) and to weigh maintainer comments
more heavily than drive-by replies based on the
`author_association` labels the fetch script returns.
- Removed the now-dead trust surface: `is_trusted_commenter` from
`lib/oz_workflows/helpers.py`, the `urllib.parse` import that only
existed for the org-membership probe, the standalone `lib/trust.py`
module, and the `tests/test_trust.py` suite that tested it.
Out of scope by design (different policies, not the comment-response
gate): `enforce_pr_issue_state._is_pr_author_org_member` (auto-close
PRs without a ready issue), `review_pr._is_non_member_pr` (non-member
PRs still get the APPROVE/REQUEST_CHANGES review-action gate), and the
prompt-context filtering helpers `org_member_comments_text` /
`review_thread_comments_text` / `all_review_comments_text` (consumed
by skill scripts rather than the webhook).
Co-Authored-By: Oz <oz-agent@warp.dev>
…webhook The Vercel control plane now owns every dispatch path that lands a `create-spec-from-issue` or `create-implementation-from-issue` run. `lib.routing._route_issues` was extended to handle: - `issues.assigned` when oz-agent is the new assignee and the issue carries `ready-to-spec` or `ready-to-implement`. - `issues.labeled` when the added label is one of those lifecycle labels and oz-agent is already in the assignees. - `issue_comment` `@oz-agent` mentions on `ready-to-spec` / `ready-to-implement` issues (already migrated; cloud-mode helpers in `lib/scripts/create_*_from_issue.py` plus builders/handlers). `ready-to-implement` wins over `ready-to-spec` so an issue carrying both labels lands on the implementation workflow rather than regenerating a spec. The legacy GitHub Actions adapters that used to fan these triggers out into the runtime are gone: - .github/workflows/create-spec-from-issue-local.yml - .github/workflows/create-spec-from-issue.yml - .github/workflows/create-implementation-from-issue-local.yml - .github/workflows/create-implementation-from-issue.yml - .github/scripts/create_spec_from_issue.py (no remaining consumers) - .github/scripts/tests/test_issue_workflow_dispatch_inputs.py `.github/scripts/create_implementation_from_issue.py` is kept on purpose because `trigger_implementation_on_plan_approved.py` still imports it for the plan-approved trigger; that flow is a separate migration. Webhook test suite: 224 passing (9 new routing cases for the `issues.assigned` / `issues.labeled` paths). Co-Authored-By: Oz <oz-agent@warp.dev>
Move the three GitHub Actions workflows that fired on `pull_request_target.labeled`
for the `plan-approved` label onto the Vercel webhook:
- `comment-on-plan-approved.yml` (posts the spec-approved comment on
the linked issue).
- `remove-stale-issue-labels-on-plan-approved.yml` (strips
`ready-to-spec` from the linked issue).
- `trigger-implementation-on-plan-approved.yml` (dispatches a
create-implementation cloud agent run when the linked issue carries
`ready-to-implement` and `oz-agent` is assigned).
The router exposes a single `WORKFLOW_PLAN_APPROVED` workflow keyed
on `pull_request.labeled` with the `plan-approved` label. The
webhook handler runs `apply_plan_approved_sync` inline (analogous
to `enforce-pr-issue-state`'s sync path) which:
1. Verifies the PR is open and is a spec PR (head branch matches
`oz-agent/spec-issue-{N}` or every changed file lives under
`specs/`).
2. Resolves the linked issue via `resolve_issue_number_for_pr`.
3. Posts the spec-approved comment on the linked issue with a
workflow-prefix metadata marker so retried webhook deliveries
do not double-post.
4. Removes `ready-to-spec` from the linked issue if present.
5. Returns a structured outcome unless the issue is ready for
implementation, in which case it returns `None` and the webhook
falls through to `build_plan_approved_request`. That builder
reuses `gather_create_implementation_context` and dispatches a
cloud agent run keyed on `WORKFLOW_PLAN_APPROVED`. The cron-side
handler is a thin alias around the existing
`build_create_implementation_handlers` so the apply path
(`apply_create_implementation_result`) is unchanged.
Behavior tightening: the synchronous helper gates all three side
effects on the spec-only check, so a non-spec PR labeled
`plan-approved` no longer strips `ready-to-spec` off an unrelated
issue. The legacy GHA `remove-stale-issue-labels-on-plan-approved`
workflow stripped the label regardless of spec-only status.
Webhook test suite: 241 passing (+17 new cases for plan-approved
across routing, builder, handler, sync helper, and dispatch
fallthrough).
Deletions:
- .github/workflows/comment-on-plan-approved.yml
- .github/workflows/trigger-implementation-on-plan-approved-local.yml
- .github/workflows/trigger-implementation-on-plan-approved.yml
- .github/workflows/remove-stale-issue-labels-on-plan-approved-local.yml
- .github/workflows/remove-stale-issue-labels-on-plan-approved.yml
- .github/scripts/trigger_implementation_on_plan_approved.py
- .github/scripts/remove_stale_issue_labels_on_plan_approved.py
- .github/scripts/tests/test_trigger_implementation_on_plan_approved.py
- .github/scripts/create_implementation_from_issue.py (last consumer
was `trigger_implementation_on_plan_approved.py`).
Co-Authored-By: Oz <oz-agent@warp.dev>
…uming repo
The cloud-mode `gather_review_context` was loading
`.github/STAKEHOLDERS` from `workspace_path / '.github' / 'STAKEHOLDERS'`.
The Vercel webhook hands in `Path('/tmp')` because the consuming
repository is not checked out on the function's filesystem, so the
file never resolved and non-member-PR review requests silently lost
stakeholder enforcement: no `allowed_logins` filter on the agent's
`recommended_reviewers` list, and no `Stakeholders (from
`.github/STAKEHOLDERS`):` block in the prompt.
The triage cloud path already loaded STAKEHOLDERS via the GitHub API
(`_load_stakeholders_from_repo` in `triage_new_issues.py`); review's
cloud path missed that migration.
Promote the API-backed helpers into `oz_workflows.triage`:
- `STAKEHOLDERS_REPO_PATH = '.github/STAKEHOLDERS'` constant.
- `decode_repo_text_file(repo_handle, path)` — generic text loader
that wraps `Repository.get_contents` and tolerates missing files,
directory paths, and GithubException.
- `load_stakeholders_from_repo(repo_handle)` — drop-in API-backed
counterpart to the workspace-backed `load_stakeholders`.
- Both share `_parse_stakeholders_lines` so the workspace and API
surfaces produce byte-for-byte identical entries.
Update `gather_review_context` to call the API-backed loader so the
file is read from the repository that triggered the webhook. The
existing private `_load_stakeholders_from_repo` and
`_decode_repo_text_file` in `triage_new_issues.py` become thin
aliases over the moved-out helpers so test fixtures patching those
private names keep working.
Webhook test suite: 249 passing (+8 cases for the API-backed loader).
Co-Authored-By: Oz <oz-agent@warp.dev>
… API
The Vercel webhook does not check out the consuming repository, so
the cloud-mode `gather_*_context` helpers that resolved the
repo-local companion skill or the directory-spec context through
`workspace_path = Path('/tmp')` always degraded to empty results:
the prompt lost the repo-local override section, and any issue
without an approved spec PR lost its `specs/GH<N>/` content.
Add API-backed counterparts and wire them into the cloud paths:
- `oz_workflows/repo_local.py`:
- `repo_local_skill_path_for_dispatch(repo_handle, name)` reads
`.agents/skills/<name>-local/SKILL.md` via
`decode_repo_text_file` and returns the repo-relative path
string when the body is non-empty.
- `format_repo_local_prompt_section` accepts `Path | str` so
cloud and legacy callers share one formatter.
- `oz_workflows/helpers.py`:
- `read_repo_spec_files(repo_handle, issue_number)` mirrors
`read_local_spec_files` via the API.
- `resolve_spec_context_for_issue_via_api` /
`resolve_spec_context_for_pr_via_api` walk both branches
(approved spec PR + `specs/GH<N>/{product,tech}.md`) without
touching the local filesystem.
Wire the helpers into the cloud paths:
- `gather_review_context` uses
`repo_local_skill_path_for_dispatch` for the companion section
and `resolve_spec_context_for_pr_via_api` for the spec text.
The legacy subprocess-based `_resolve_spec_context_text_for_pr`
is replaced with a `_format_spec_context_text` helper that
renders the resolved dict.
- `gather_pr_comment_context` and
`gather_create_implementation_context` switch to the API spec
resolver.
- `build_triage_prompt_for_dispatch` accepts `repo_handle` and
resolves the `triage-issue-local` and `dedupe-issue-local`
companions through the API.
- `lib.builders.build_triage_request` passes `repo_handle`
through.
Webhook test suite: 265 passing (+16 cases for the API-backed
repo-local skill and spec-context helpers).
Co-Authored-By: Oz <oz-agent@warp.dev>
The triage agent's SME (subject-matter expert) suggestions were never consumed downstream: there's no `extract_sme_candidates` helper, no surfacing in comments/labels/reviewers, and the only production reference was the schema example inside the prompt. Remove the goal line, JSON schema field (`sme_candidates`), `Repository Stakeholders` prompt section, and the `stakeholders_text` parameter that fed it from `build_triage_prompt`. Drop `stakeholders_text` from the cloud-mode `TriageContext`, the `gather_triage_context` / `build_triage_prompt_for_dispatch` flow, and the legacy `process_issue` / `main` chain. Update `.agents/skills/triage-issue/SKILL.md`: drop the SME workflow step (and renumber subsequent steps), the STAKEHOLDERS input line, and the `SMEs` reference in the output expectations. The `oz_workflows.triage` helpers (`load_stakeholders_from_repo`, `format_stakeholders_for_prompt`, `STAKEHOLDERS_REPO_PATH`) and their tests stay in place because `scripts/review_pr.py` still uses them for non-member PR reviewer enforcement. Co-Authored-By: Oz <oz-agent@warp.dev>
…to-X labels
Move the `comment-on-ready-to-implement` / `comment-on-ready-to-spec` GitHub Actions workflows into the Vercel webhook control plane. When a maintainer adds `ready-to-spec` or `ready-to-implement` to an issue without enlisting `oz-agent` as an assignee, the webhook now posts 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 have the bot pick up the work automatically.
- Add `WORKFLOW_ANNOUNCE_READY_ISSUE` to `lib/routing.py` and split the `issues.labeled` route: oz-agent assigned still routes to create-spec / create-implementation, oz-agent NOT assigned now routes to the new workflow instead of being dropped.
- Add `lib/scripts/announce_ready_issue.py` with `apply_announce_ready_issue_sync`. The handler is fully synchronous (no cloud-agent dispatch fallback), idempotent via the `oz-agent-metadata` marker, and emits label-specific announcement bodies that link back to `@oz-agent` and the `specs/` tree as relevant.
- Wire `sync_announce_ready_issue` into `api/webhook.py` alongside the existing `sync_enforcer` / `sync_plan_approved` paths, special-cased to short-circuit so the workflow never reaches the dispatch code path.
- Add 13 tests covering routing, sync-handler outcomes (announced / noop / skipped), and webhook short-circuit behavior.
- Delete the legacy `.github/workflows/comment-on-ready-to-{implement,spec}.yml` adapters now that the webhook owns this behavior.
Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
d2df840 to
a3810bc
Compare
Summary
This PR migrates the triage / respond-to-triaged / PR-review workflows off Docker and onto Warp-hosted cloud agent runs, and lays down a Vercel-based control plane that will eventually replace the GitHub Actions delivery surface entirely.
The cloud-mode rewrite is the immediately-active change: each of the three Python entrypoints now calls
run_agent(cloud) +build_agent_config(role="review-triage")and reads results through the newoz_workflows.artifacts.load_*_artifacthelpers. Their prompts now describe aoz artifact upload <name>.jsonhandoff instead of a/mnt/outputmount; security rules, output schemas, and skill references survive the rewrite verbatim. The Docker assets (docker/triage/,docker/review/,build-{triage,review}-imagecomposite actions,docker_agent.py,test_docker_agent.py) are deleted, and the three workflow YAMLs that used them are updated to drop theBuild … agent containerstep and forwardWARP_ENVIRONMENT_ID+WARP_REVIEW_TRIAGE_ENVIRONMENT_IDto the script step. The workflow YAMLs themselves are intentionally retained so the existing GitHub Actions delivery path keeps working through the cutover.The new
control-plane/Python project is the long-term target: a Vercel webhook handler that verifies HMAC-SHA256 signatures and routes events to a workflow handler, plus a 1-minute cron poller that reads in-flight run state from Vercel KV and applies completed cloud-agent results back to GitHub. Lib helpers cover signatures, routing, trust evaluation, dispatch, in-flight state, GitHub App token exchange, and the cron drain loop. The full architecture and deployment runbook live incontrol-plane/README.md.Workflows served by the Vercel webhook in this PR
The webhook + cron control plane now owns the following delivery surface, and the corresponding
.github/workflows/*.yml+.github/scripts/*.pyshims have been removed in favor of the cloud-mode helpers underlib/scripts/:review-pull-request(PR opened / ready_for_review / review_requested / labeled //oz-review).enforce-pr-issue-state(PR synchronize / edited).respond-to-pr-comment(@oz-agentmention on PRs and review threads).verify-pr-comment(/oz-verifyon PRs).triage-new-issues— newly added:issues.openedon non-triagedissues,@oz-agentmention on a non-triagedissue, andneeds-inforeporter replies all route through the webhook.@oz-agentmentions on already-triagedissues continue to flow through the legacyrespond-to-triaged-issue-commentGitHub Actions workflow until that workflow is migrated in a follow-up.Cutover steps after merge
control-plane/.vercel.jsondeclares the runtime, both functions, and the 1-minute cron schedule.OZ_GITHUB_WEBHOOK_SECRET,OZ_GITHUB_APP_ID,OZ_GITHUB_APP_PRIVATE_KEY,WARP_API_KEY,WARP_API_BASE_URL,WARP_ENVIRONMENT_ID,WARP_REVIEW_TRIAGE_ENVIRONMENT_ID,CRON_SECRET. Detail per-secret incontrol-plane/README.md.vercel_kvlazily.https://<project>.vercel.app/api/webhook. The webhook handler returns 202 with the routed workflow id so the Recent Deliveries UI stays green..github/workflows/*per plan §5a — once the Vercel control plane is verified end-to-end, delete the remaining legacy GitHub Actions YAMLs in a follow-up PR. This PR keeps the issue-triggered helpers (respond-to-triaged-issue-comment,create-spec-from-issue,create-implementation-from-issue,comment-on-*) and the plan-approval workflows in place so both delivery paths can be exercised in parallel during cutover.Validation
python -m pytest tests→ 191 tests passed, 47 subtests passed (signature verification, routing table, trust evaluation, dispatch, cron drain loop, builder lifecycle, handler wiring, triage prompt + apply helpers).PYTHONPATH=lib:.github/scripts python -m unittest discover -s .github/scripts/tests→ 294 OK (helpers, role parameter, named artifact loaders, cloud-mode triage/review/respond-to-triaged dispatch, and skill-section assertions; the triage-specific tests have moved totests/test_triage.py).Security Rules:blocks were diff-checked byte-for-byte againstmainto confirm the cloud rewrite did not weaken or relax the prompt-injection / output-schema rules.References
Plan id:
7e8e8b6a-9e8a-4cbf-ab95-dd37cf4cc44c.Conversation: https://staging.warp.dev/conversation/da08a6c7-4f86-4dac-99f6-3358cfe3258e
Run: https://oz.staging.warp.dev/runs/019dd6d9-cb65-73a2-8cfb-d03d37afd03a
Plans:
This PR was generated with Oz.