docs(readme): trim README and split details into docs/#402
Merged
captainsafia merged 1 commit intooz-agent/control-plane-migrationfrom Apr 29, 2026
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
7361557
into
oz-agent/control-plane-migration
10 checks passed
Contributor
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and posted feedback on this pull request. Comment Powered by Oz |
Contributor
There was a problem hiding this comment.
Overview
This PR trims the README to a short introduction and documentation index, moving the longer architecture and onboarding material into new docs pages.
Concerns
- No blocking correctness, security, or documentation concerns found in the changed lines.
Verdict
Found: 0 critical, 0 important, 0 suggestions
Approve
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
captainsafia
added a commit
that referenced
this pull request
Apr 29, 2026
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>
captainsafia
added a commit
that referenced
this pull request
Apr 30, 2026
…on cloud runs (#400) * feat(control-plane): scaffold vercel webhook and cron poller Add a greenfield Python project at control-plane/ that hosts the GitHub webhook endpoint and the cron poller for in-flight cloud agent runs. The webhook handler verifies HMAC-SHA256 signatures, routes events to a workflow handler, and returns 202 immediately; the cron poller drains run state from Vercel KV and applies terminal results back to GitHub through workflow-specific handlers. The lib/ helpers (signatures, routing, trust, dispatch, state, poll_runs, github_app) keep the runtime stdlib-friendly so the test suite runs without extra dependencies. vercel.json declares the Python 3.12 runtime, the webhook function, the cron function, and a 1-minute schedule. requirements.txt mirrors the legacy .github/scripts/requirements.txt pins. Co-Authored-By: Oz <oz-agent@warp.dev> * feat(oz_workflows): add role parameter and review-triage env-var resolution build_agent_config() now accepts a 'role' keyword. When the caller passes role='review-triage' the resolver consults WARP_REVIEW_TRIAGE_ENVIRONMENT_ID first, then falls back to WARP_ENVIRONMENT_ID. Default role behavior is unchanged. Unknown roles emit a ::warning:: and use the default lookup so a future workflow addition does not have to coordinate a lockstep change to the role registry before it ships. Co-Authored-By: Oz <oz-agent@warp.dev> * feat(oz_workflows): generalize artifact loaders for cloud-mode workflows Add load_run_artifact(run_id, filename) as the workflow-agnostic entry point and named wrappers load_triage_artifact (triage_result.json), load_issue_response_artifact (issue_response.json), and load_review_artifact (review.json). Rewrite load_pr_metadata_artifact as a thin wrapper around load_run_artifact that keeps its original schema enforcement. The named filenames are exported as module-level constants so the cloud-mode prompts and the cron poller stay in lockstep with the filename the agent uploads via 'oz artifact upload <name>.json'. Co-Authored-By: Oz <oz-agent@warp.dev> * feat: switch triage and review workflows to cloud-mode run_agent The triage, respond-to-triaged-issue-comment, and review-pull-request workflows now dispatch cloud agent runs via run_agent + build_agent_config instead of run_agent_in_docker. The agent uploads the workflow result artifact via 'oz artifact upload <name>.json' and the host loads it through the corresponding load_*_artifact helper. The cloud-mode prompts replace the Docker mount references (/mnt/repo, /mnt/output) with explicit artifact-upload instructions. Security rules, output-schema requirements, and skill references are preserved verbatim across the rewrite. Each caller passes role='review-triage' to build_agent_config so the operator can route those workloads onto WARP_REVIEW_TRIAGE_ENVIRONMENT_ID when configured. Tests now mock run_agent + load_*_artifact instead of run_agent_in_docker; the obsolete _container_companion_path test classes are removed alongside the helper. Co-Authored-By: Oz <oz-agent@warp.dev> * chore: delete docker assets after cloud-mode migration The triage and review workflows now run on Warp-hosted cloud agent runs, so the Docker images, build composite actions, host-side docker_agent.py helper, and its test_docker_agent.py suite are no longer reachable from any active call site. Removing them keeps the cloud workflow path the only path; the workflow YAMLs in .github/workflows/ continue to reference run-oz-python-script as before and need no Docker context. Co-Authored-By: Oz <oz-agent@warp.dev> * chore(workflows): drop docker build steps and forward review-triage env vars The three workflows that previously dispatched Dockerized agents (triage-new-issues, respond-to-triaged-issue-comment, review-pull-request) no longer need the Build … agent container step or the TRIAGE_IMAGE / REVIEW_IMAGE env passthrough. Each workflow now forwards WARP_ENVIRONMENT_ID and the optional WARP_REVIEW_TRIAGE_ENVIRONMENT_ID to the Python entrypoint so build_agent_config(role='review-triage') can route those workflows onto a dedicated cloud environment when the operator configures one. The workflow files themselves are intentionally retained so the existing GitHub Actions delivery path keeps working until the Vercel control plane is provisioned and the GitHub App webhook URL flips over. A follow-up PR will delete .github/workflows/* per plan §5a. Co-Authored-By: Oz <oz-agent@warp.dev> * docs(skills): rewrite docker workflow mode sections to cloud workflow mode The triage-issue, dedupe-issue, review-pr, and review-spec skills now run as Warp-hosted cloud agent runs rather than inside a Docker container, so the Docker mount-based handoff is no longer the right contract. Each skill's workflow-mode section now describes the cloud flow: the agent writes its result file in the run's working directory, validates it with jq, and uploads it via 'oz artifact upload <name>.json' so the host workflow can fetch it after the run reaches a terminal state. Repository-specific overrides, output schemas, severity labels, suggestion-block constraints, and other cross-repo contracts are preserved verbatim across the rewrite. Co-Authored-By: Oz <oz-agent@warp.dev> * docs: describe vercel control plane and review-triage env var README.md now explains both delivery surfaces (GitHub Actions + Vercel control plane) and points at control-plane/README.md for the cutover runbook. The new WARP_REVIEW_TRIAGE_ENVIRONMENT_ID variable is documented as the optional override for routing triage and review runs onto a dedicated cloud environment. CONTRIBUTING.md replaces the outdated Docker setup guidance with a 'Local Vercel dev' section that shows how to run 'vercel dev', sign a synthetic webhook, and replay it against the local handler. docs/platform.md acknowledges the Vercel webhook + cron flow in the 'How a workflow uses an agent' walkthrough so readers see both delivery surfaces in the platform overview. Co-Authored-By: Oz <oz-agent@warp.dev> * fix(control-plane): use upstash-redis for KV access and drop invalid python runtime Two fixes required to make the Vercel deploy succeed end-to-end: - vercel.json: remove the "runtime": "python3.12" entries; Vercel rejected them as invalid runtime identifiers. The Python runtime is auto-detected from .py files in api/ and defaults to 3.12. - requirements.txt + api/cron.py: replace the vercel-kv package (whose KV() constructor does not accept the documented kwargs and rejects the Vercel-injected KV_* env vars) with upstash-redis, the official Upstash Python SDK. Vercel KV is Upstash Redis under the hood, and upstash-redis consumes KV_REST_API_URL / KV_REST_API_TOKEN directly. Smoke tests against the preview deploy now pass: - GET /api/webhook -> 200 {"status": "ok"} - GET /api/cron without auth -> 401 invalid cron secret - GET /api/cron with bearer -> 200 empty drain Co-Authored-By: Oz <oz-agent@warp.dev> * feat(oz_workflows): add dispatch_run for fire-and-forget agent dispatch Splits the dispatch step out of run_agent into a standalone function so callers that cannot afford to block on the polling loop (most notably the Vercel webhook handler, which must respond within GitHub's ~10s delivery window) can dispatch a run and persist its run_id to KV for the cron poller to drain asynchronously. run_agent is preserved as the blocking convenience wrapper used by GitHub Actions entrypoints, refactored to call dispatch_run internally so the request-construction path stays single-source. Co-Authored-By: Oz <oz-agent@warp.dev> * feat(oz_workflows): add fire-and-forget dispatch_run helper The Vercel webhook handler dispatches cloud agent runs in fire-and-forget mode: it persists ``RunState`` keyed by the returned ``run_id`` and returns 202 immediately. The cron poller drains the run on the next tick. Splitting the SDK call out of ``run_agent`` keeps the legacy GitHub Actions entrypoints synchronous (``run_agent`` now wraps ``dispatch_run`` plus the existing polling loop) while giving the control-plane webhook handler a primitive that does not block on a poll. Co-Authored-By: Oz <oz-agent@warp.dev> * chore(control-plane): mirror oz_workflows + entrypoints into the function Vercel's project root is `control-plane/`, but the four PR-flow entrypoints (`review_pr.py`, `respond_to_pr_comment.py`, `verify_pr_comment.py`, `enforce_pr_issue_state.py`) and the `oz_workflows` package they depend on live under `.github/scripts/`. Add a Vercel `installCommand` (`scripts/vercel_install.sh`) that mirrors those into `control-plane/lib/oz_workflows/` and `control-plane/lib/scripts/` before each Vercel build. Extend the function-runtime `PYTHONPATH` to include `lib/` so the control-plane code can `from oz_workflows.oz_client import ...` and `from scripts.review_pr import ...` directly. The mirrored directories are git-ignored so the repository keeps a single source of truth at `.github/scripts/`. Co-Authored-By: Oz <oz-agent@warp.dev> * refactor(workflows): expose gather/build/apply helpers on PR entrypoints Refactors the four PR-flow entrypoints to surface three importable helpers each so the Vercel control plane can reuse the same logic: * `verify_pr_comment.py`: `gather_verify_context`, the existing `build_verification_prompt`, and `apply_verification_result`. * `enforce_pr_issue_state.py`: `enforce_pr_state_synchronously` (returns an `EnforceDecision` of allow / close / need-cloud-match), `gather_enforce_context`, the existing `build_issue_association_prompt`, and `apply_issue_association_result`. * `review_pr.py`: `gather_review_context`, `build_review_prompt_for_dispatch` (cloud-mode prompt with PR description / annotated diff / spec context inlined since the cloud agent has no host-prepared workspace), and `apply_review_result`. The existing `build_review_prompt` keeps the file-based variant for the GitHub Actions path. * `respond_to_pr_comment.py`: `gather_pr_comment_context`, `build_pr_comment_prompt`, and `apply_pr_comment_result`. All three event paths (review_comment / issue_comment / review_body) now funnel through the shared helpers. Each entrypoint's `main()` was updated to call the same helpers so the GitHub Actions path and the Vercel control plane path stay byte-for-byte equivalent. The full `.github/scripts/tests` suite still passes (494/494). Co-Authored-By: Oz <oz-agent@warp.dev> * feat(control-plane): wire builders, handlers, webhook, and cron registry Adds the production wiring that lets the Vercel control plane drive PR workflows end-to-end: * `lib/builders.py` exposes one `PromptBuilder` per cloud-agent workflow (`review-pull-request`, `respond-to-pr-comment`, `verify-pr-comment`, `enforce-pr-issue-state`) plus `build_builder_registry` for the webhook handler. * `lib/handlers.py` exposes the corresponding `WorkflowHandlers` registry (artifact_loader / result_applier / failure_handler) for the cron poller, with each handler minting a fresh GitHub client per call. * `api/webhook.py:process_webhook_request` now verifies the signature, routes the event, runs the synchronous `enforce-pr-issue-state` path in-process when applicable, evaluates the route through the builder registry, dispatches the cloud agent run via `dispatch_run`, and persists the run state to KV. Errors at any stage surface as a structured 500 instead of hanging the request. * `api/cron.py:build_workflow_handlers` returns the concrete handler registry instead of an empty `{}`. The dispatch path is gated behind optional kwargs on `process_webhook_request` so the existing unit tests continue to pass without GitHub or oz-agent SDKs on PYTHONPATH. Co-Authored-By: Oz <oz-agent@warp.dev> * test(control-plane): add builders + handlers + webhook dispatch unit tests Adds three new test modules covering the wiring introduced in the previous commit: * `tests/test_builders.py` (7 tests) — one test class per builder. Stubs `scripts.<workflow>` so the assertions stay focused on payload parsing, repo handle resolution, and DispatchRequest shape. * `tests/test_handlers.py` (10 tests) — one test class per `WorkflowHandlers`. Stubs `scripts.<workflow>` and `oz_workflows.*` so the assertions verify which apply helper / artifact loader gets called with the right arguments. Includes a failure_handler test that confirms `WorkflowProgressComment.report_error` is invoked. * `tests/test_webhook_dispatch.py` (6 tests) — exercises the new dispatch path in `process_webhook_request`: success, no-builder, builder-raises, dispatch-raises, plus the synchronous enforce-pr-issue-state short-circuit and need-cloud-match fallthrough. Also updates `control-plane/README.md` to drop the "currently no-op" caveat, list which PR-triggered workflows are live vs. still pending, and document the new install hook + GitHub App env vars. Total control-plane test count: 110 (up from 87). The legacy `.github/scripts/tests` suite still passes (494/494). Co-Authored-By: Oz <oz-agent@warp.dev> * docs(control-plane): refresh entrypoint runtime docs Co-Authored-By: Safia Abdalla <captainsafia@users.noreply.github.com> Co-Authored-By: Oz <oz-agent@warp.dev> * fix(control-plane): vendor oz_workflows + entrypoints so vercel ships 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> * fix(control-plane): resolve bare skill names into <repo>:<path> spec 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> * feat(workflows): allow WorkflowProgressComment + apply helpers to be 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> * feat(control-plane): post WorkflowProgressComment start before dispatch 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> * feat(control-plane): drive WorkflowProgressComment lifecycle from cron 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> * chore(control-plane): refresh vendored oz_workflows + entrypoints 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> * refactor(repo): host webhook control plane at root and prune PR-only 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> * fix(ci): install pytest + pytest-subtests for control-plane tests 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> * feat(control-plane): route issue triage through the Vercel webhook 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> * chore: drop uv version pin and ignore local-only build artifacts 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> * feat(routing): triage every issue regardless of lifecycle labels 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> * feat(review): assign exactly one randomly-selected human reviewer per PR 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> * feat(review): downgrade APPROVE verdict to COMMENT review on PRs 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> * feat(triage): add comment_type discriminator for issue-thread responses 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> * docs(readme): trim README and split details into docs/ (#402) 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> * feat(comments): respond to every human-authored comment, not just org 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> * feat(routing): migrate create-spec/create-implementation triggers to 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> * feat(routing): migrate plan-approved handlers to the webhook 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> * fix(review): load .github/STAKEHOLDERS via the GitHub API on the consuming 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> * fix(cloud): resolve repo-local skills and spec context via the GitHub 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> * feat(triage): drop SME identification from triage prompt and skill 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> * feat(webhook): add announce-ready-issue handler for unassigned ready-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> * fix(review-pr): ensure approved non-member PRs request review Co-Authored-By: Oz <oz-agent@warp.dev> * fix(webhook): update ready issue announcement copy Co-Authored-By: Oz <oz-agent@warp.dev> * fix(review-pr): approve after dismissing stale app review Co-Authored-By: Oz <oz-agent@warp.dev> * fix: make PR reviewer selection deterministic Co-Authored-By: Oz <oz-agent@warp.dev> * Add verdict to review.json, request changes, only request review on approve * refactor: add agent workflow lifecycle abstraction Co-Authored-By: Oz <oz-agent@warp.dev> * chore: prune legacy GitHub Actions runtime Co-Authored-By: Oz <oz-agent@warp.dev> * chore: remove PR state enforcement checks Co-Authored-By: Oz <oz-agent@warp.dev> * fix: address control plane migration regressions Co-Authored-By: Oz <oz-agent@warp.dev> * fix: remove synchronous agent entrypoints Co-Authored-By: Oz <oz-agent@warp.dev> * refactor: rename workflow packages Rename the shared oz_workflows package to oz and move executable workflow modules from lib/scripts into lib/workflows. Update imports, documentation, specs, and tests to use the new package layout. Co-Authored-By: Oz <oz-agent@warp.dev> * refactor: rename lib package to core Move the control-plane package from lib to core and update runtime imports, PYTHONPATH configuration, docs, and tests to match the new package layout. Co-Authored-By: Oz <oz-agent@warp.dev> * fix: harden workflow apply branch checks Constrain implementation branch overrides to the expected branch or a delimiter-bounded suffix and restore the one-minute timestamp cushion when checking agent-pushed spec and implementation branches. Co-Authored-By: Oz <oz-agent@warp.dev> * fix: remove dedupe issue recency bias Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev> Co-authored-by: Roland Huang <roland@warp.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cleans up the README per Slack feedback on PR #400: keep it short and precise with minimal headings, and move the longer reference material into
docs/.Changes
README.mdis now a short intro plus a documentation index (~12 lines, down from ~260).docs/architecture.mdcaptures the repository layout, the webhook-driven flow, and the GitHub Actions workflow table that used to live in the README.docs/onboarding.mdcaptures the GitHub App / Vercel project / GitHub Actions secrets walkthrough that used to live under the README's "How to use these workflows in your own repo" section.CONTRIBUTING.mdalready documents env setup, the test suites, andvercel dev— the README now links there instead.Targeting
This PR targets
oz-agent/control-plane-migration(the branch behind PR #400) so the README cleanup can be reviewed independently and folded into that PR.Conversation: https://staging.warp.dev/conversation/69d7450e-0974-42dc-9a91-26bdb498f667
Run: https://oz.staging.warp.dev/runs/019dd7c4-b4e7-7bd8-b0b6-37b274351909
This PR was generated with Oz.