feat(workflow): add scheduled PR health example - #529
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new “PR health” workflow example to the CAO examples set, including deterministic scoring for open PRs, optional importance synthesis, and guarded enforcement actions, along with scheduled-flow templates and documentation explaining setup and safety controls.
Changes:
- Introduces a new deterministic PR-health workflow (
pr_health.py) with snapshotting, scoring, artifact output, and apply-mode enforcement with safeguards. - Adds a biweekly scheduling guard script plus two scheduled-flow templates (dry-run vs explicitly authorized apply).
- Adds documentation and tests covering example validation, cadence guarding, and schedule/enforcement behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/examples/test_pr_health_workflow_example.py | Adds tests to validate the new PR-health example script and scheduled flow templates. |
| examples/workflows/pr-health/README.md | Documents installation, scoring model, safety controls, artifacts, and scheduling guidance for the example. |
| examples/workflows/pr-health/pr-health-biweekly.md | Adds a scheduled-flow template intended for non-mutating biweekly dry runs. |
| examples/workflows/pr-health/pr-health-biweekly-apply.md | Adds a scheduled-flow template intended for explicitly authorized apply-mode runs. |
| examples/workflows/pr-health/pr_health.py | Implements the deterministic scoring workflow, persistence, reporting, optional analysis step, and guarded enforcement. |
| examples/workflows/pr-health/pr_health_biweekly_guard.py | Implements a deterministic date-based gate to achieve an exact 14-day cadence. |
| docs/workflows.md | Adds a “see also” link to the new PR-health workflow examples directory. |
| docs/flows.md | Adds a short section pointing readers to the PR-health workflow scheduling example. |
Suppressed comments (1)
test/examples/test_pr_health_workflow_example.py:72
- Same issue as the dry-run schedule test: day-of-week
0is Sunday in crontab format. Update the expected schedule to Monday (1) to match the intended cadence and the flow files.
assert metadata["schedule"] == "0 9 * * 0"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,29 @@ | |||
| --- | |||
| name: pr-health-biweekly | |||
| schedule: "0 9 * * 0" | |||
| @@ -0,0 +1,31 @@ | |||
| --- | |||
| name: pr-health-biweekly-apply | |||
| schedule: "0 9 * * 0" | |||
| flow_path = EXAMPLE_DIR / "pr-health-biweekly.md" | ||
| metadata, prompt = _parse_flow_file(flow_path) | ||
|
|
||
| assert metadata["schedule"] == "0 9 * * 0" |
| CAO uses APScheduler weekday numbering, where `0` is Monday. The flow therefore | ||
| uses `0 9 * * 0` for Monday at 09:00 in the server's local timezone. The | ||
| `cao-server` process must remain running for scheduled flows to execute. |
gutosantos82
left a comment
There was a problem hiding this comment.
PR Review: #529 — feat(workflow): add scheduled PR health example
Summary
Adds examples/workflows/pr-health/: a 1309-line deterministic open-PR health scorer with guarded comments, draft transitions, and allowlisted closure, plus biweekly scheduled-flow templates (dry-run and apply), README, and tests. The engineering quality is high — no command injection, marker authenticity is genuinely enforceable via viewerDidAuthor, closure is double-gated, and doc↔code consistency is unusually strong. However, two behavioral defects contradict the PR's own documented safety model (duplicate notifications; cadence-coupled lifecycle), and the enforcement paths that actually mutate GitHub are almost entirely untested. Since this ships as a copyable reference policy that comments on, drafts, and closes other people's PRs, those should be fixed before merge.
Blocking (must fix before merge)
- [correctness] examples/workflows/pr-health/pr_health.py:692, ~645 — Cross-run idempotency is ineffective for
warn_owner/propose_draft. Markers embed per-runas_ofandscore, and_has_marker_for_actiondedupes by exact string match (onlyescalate_protected_pruses a loose prefix match), so a prior run's marker never matches and the same warning comment is posted again. Reproduced: twowarn_ownerruns before the 7-day draft threshold post the identical warning twice. This directly contradicts the safety claim that markers make actions idempotent — the workflow spams PR owners. Fix: dedupe on stage presence (as escalation already does), not onscore/as_of. - [tests] test/examples/test_pr_health_workflow_example.py:83-105 — The mutation/enforcement guards are essentially untested. The single
_apply_recommendationstest covers only theskipped_not_openbranch. Untested:already_appliedidempotency,skipped_live_driftrevalidation, theskipped_closure_not_allowlistedallowlist gate, and every actual mutation path (drafted_and_commented,closed_and_commented,commented). These are exactly the branches whose failure causes an unwanted comment, draft, or closure. Add monkeypatched per-branch tests asserting both result status and the recordedghcommands.
Important (should fix)
- [correctness] pr_health.py:406, 483-500 — The enforcement lifecycle is only correct at exactly the 14-day cadence.
_latest_markerreturns only the newest marker; once a PR is drafted, a later warning-stage marker shadows the draft marker, so the closure branch (stage=="draft" and marker_age>=14) is never re-evaluated. Reproduced at a 7-day cadence: the PR oscillateswarn_owner↔ owner-notification forever and never reachespropose_close. The README's own examples pass arbitraryas_offor manual runs, so this is a reachable state, not a theoretical one. Fix: drive lifecycle progression off persisted state (which already carries streak/as_of) or the earliest unanswered stage, not the newest comment marker. - [correctness] pr_health.py:444 — One stale per-PR state entry aborts the entire run:
_observation_streakraisesValueError("as_of predates the persisted PR health state")inside the per-PR loop, killing scoring for all other PRs (backfill, reopened PR with old state, clock skew). Prefer per-PR skip/normalize. - [tests] pr_health.py (
_run_self_tests) — Scoring is validated almost entirely by an in-product assert bundle that runs inside_run_lockedon every production run and is stripped underpython -O; meanwhile--cov=srcmeasures nothing underexamples/, so the passing suite gives false coverage confidence for the 1309-line file. Move the self-tests intotest/(or at minimum out of the production run path). - [consistency] pr-health-biweekly.md / pr-health-biweekly-apply.md — Both templates share the same guard, schedule, and mode-agnostic
run_id/snapshot_id(pr-health-biweekly-<date>/scheduled-<date>). If both flows are registered, the second to run on a due Monday hits the manifest guard (snapshot_id already exists with different inputs), making dry-run and apply silently mutually exclusive per day. Differentiate identifiers by mode, or state "do not register both" in the README schedule section. - [security] pr_health.py (
SAFE_ID_RE/_run_lockedvalidation) —SAFE_ID_RE = ^[A-Za-z0-9._-]+$accepts.., sosnapshot_id=".."resolvesartifact_dirto the state root (no escape beyond it, since/is rejected). Reject.and..explicitly.
Nits (optional)
- [correctness] pr_health_biweekly_guard.py / pr_health.py — Timezone mixing: the guard derives
as_offromdate.today()(server-local) while scoring compares gh UTC timestamps; near midnight, 7/14/21-day threshold crossings can shift by a day. - [correctness] pr_health.py (
_latest_marker,_latest_commit_at) — Marker detection and idle math assumegh pr viewreturns complete comment/commit history; GraphQL-backed fields are commonly capped. Add a defensive note or cap check, since enforcement mutates GitHub based on these. - [security] pr-health-biweekly-apply.md / README.md — The scheduled apply mode is a standing unattended write-grant under the operator's gh identity (drafting contributors' PRs has real social impact). Mitigations are solid (closure double-gated off, live re-score before mutation), but make the apply-template warning harder to skim and suggest trialing against a fork first; also add a one-line note that the LLM importance synthesis reads untrusted PR text (advisory-only, cannot change scores/actions).
- [consistency] pr_health.py:698, 73-76 —
stage=closedmarker is emitted by_action_markerbutMARKER_REonly parseswarning|draft; write-only vocabulary, harmless but worth aligning. - [conventions] PR title — scope
workflow(singular) vs the newexamples/workflows/directory (plural); cosmetic.
Tests
The new test file is a thin smoke/wiring layer (~5 test functions): flow static validation, one cadence check at 4 dates, script lint, running the in-product self-tests, and one enforcement skip branch. The "131 passed" claim is real but mostly pre-existing suites (test_flow_service.py, test_script_lint.py, test_workflow_spec_service.py) swept in by the command. The bespoke calendar engine (_is_leap, _date_ordinal, _days_between, _validate_as_of) — where month-boundary/rollover bugs would hide — is only touched by two asserts, and the fcntl.flock per-repo locking (a stated safety control) has no test at all. Minimum before merge: per-branch enforcement tests (see Blocking), calendar boundary + invalid-date cases, a lock acquire/release/contention test, and direct _comment_body marker assertions for warn/draft/close.
Verification
Verifier skipped (no changes under src/cli_agent_orchestrator/ — examples/docs/tests only). The correctness reviewer did run the shipped test file in the PR worktree (all 5 pass), live-verified the APScheduler 0 9 * * 0 = Monday claim, and reproduced both the duplicate-comment and cadence-oscillation defects by simulation; the security reviewer confirmed no shell=True anywhere and that forged markers are rejected.
Verdict
Request changes — fix the marker idempotency (duplicate owner comments) and cadence-coupled lifecycle defects that contradict the documented safety model, and add tests for the GitHub-mutating enforcement branches, before this lands as a reference policy.
Dedupe enforcement on marker stage instead of exact text, so a prior run's marker (which embeds that run's score and as_of) suppresses a repeat notification. Select the furthest-advanced marker so lifecycle progression no longer depends on the run cadence. Also: stale per-PR state restarts that PR's streak instead of aborting the run, reject snapshot_id "." and "..", derive as_of from UTC, mode-qualify the scheduled identifiers, and move the in-product self-tests into test/ (87 tests covering every mutating branch). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the review — all 12 items addressed in 5077414. Every fix below carries a test that was verified to redden when the fix is reverted. Blocking1. Marker idempotency ( 2. Untested enforcement branches — the test file went 105 → ~1250 lines / 87 tests. Important3. Cadence-coupled lifecycle — 4. One stale state entry aborting the run — 5. In-product self-tests — 6. Dry-run/apply identifier collision — the guard now emits mode-qualified 7. Nits
Additional fix found while testing
Tests requestedAll four landed: per-branch enforcement tests (above); calendar boundaries ( Verification
Note for maintainers: |
gutosantos82
left a comment
There was a problem hiding this comment.
PR Review: #529 — feat(workflow): add scheduled PR health example
Summary
Re-review at head 5077414 following the previous request-changes review. The author's point-by-point response holds up under independent verification: all 12 prior items (marker idempotency, cadence-coupled lifecycle, stale-state abort, untested enforcement branches, self-test removal, dry-run/apply id collision, .. reserved-id traversal, and the nits) are genuinely fixed at this head, and the mutation-matrix claims were spot-checked exactly — reverting each fix reddens the stated tests. The test file grew to 87 passing tests with per-branch gh-argv assertions; this is now a well-defended reference example. Two new correctness defects surfaced in the reworked lifecycle logic, however, and both break the README's documented escalation contract in reachable states: a reopened PR can never be re-notified in apply mode (dry-run and apply disagree), and a PR scoring exactly 0 never advances past observation. Both fail in the conservative direction (no wrongful mutation, no spam) and both fixes are small — one more round should close this out.
Important (should fix)
- [correctness] examples/workflows/pr-health/pr_health.py:938, :849 (
_has_marker_for_stage/_apply_recommendations) — Reopened PRs can never be re-notified at any stage in apply mode. The new dedup asks "has this identity EVER authored a marker for this stage" with no lifecycle-epoch scoping. After a reverted close, the warning/draft/closed markers from the first lifecycle persist;_recommend_actioncorrectly restarts the ladder (aclosedmarker falls through to observation, as documented), but_has_marker_for_actionfinds the stale first-life marker and returnsalready_applied— forever. Reproduced by simulation over 8 weekly runs on a reopened, still-unhealthy PR: every run recommendswarn_owner, yet enforcement is permanently suppressed, so the dry-run report contradicts apply behavior and escalation/closure are unreachable. Fix: scope stage dedup to markers newer than the current lifecycle epoch (e.g. ignore markers at or before the latestclosedmarker). - [correctness] pr_health.py:527 (
_observation_streak) —int(previous.get("last_score") or 100)treats a persistedlast_scoreof0as absent (falsy) and substitutes 100, soprior_qualifiedis false and the streak resets to 1 on every run.warn_ownerrequires streak ≥ 2, so a fully-broken PR scoring 0 — precisely the abandoned-PR case this workflow exists to manage — returnsobserve_againindefinitely and is never warned, drafted, or closed. Reproduced across 12 simulated runs at every cadence (score 0 →observe_againforever; score 50 advances normally). Fix:previous.get("last_score", 100)or an explicitis Nonecheck (same pattern risk anywhereor-defaulting is applied to scores/streaks).
Nits (optional)
- [correctness] pr_health.py:558-567 — A PR warned at score 51–59 holds at
await_owner_deadlineindefinitely:warn_ownerfires below 60 butpropose_draftrequires ≤ 50, so the 51–59 band never progresses nor re-warns. Looks like a designed threshold gap and it doesn't spam, but it's worth a one-line README note so operators aren't surprised. - [conventions] CHANGELOG.md — No
### Addedentry. Example additions historically get one (#377, #253, #109, #11); not mandated by CONTRIBUTING, but a one-liner would match the established pattern. - [consistency] docs/flows.md:120-122 — "Scheduled runs default to non-mutating dry-run mode" can mislead a skimmer since an apply template also ships and mutates GitHub; clarify that the dry-run template is non-mutating while the separate, explicitly-authorized apply template mutates.
- [consistency] docs/workflows.md:246 / docs/flows.md:118 — The new pr-health links use parent-relative
../examples/...while sibling bullets use doc-relative paths. The targets resolve correctly (verified); style-only. - [security] pr_health.py:460, :845 — Marker idempotency assumes the same
ghidentity across runs (viewerDidAuthor); switching the operator identity would re-post stage notifications once. A robustness assumption worth a README line, not a vulnerability.
Tests
The prior blocking tests item is fully resolved. The suite is now 1184 lines / 56 test functions expanding to 87 collected tests — all pass in 1.11 s in the PR worktree. Every _apply_recommendations branch has a dedicated test asserting both result status and the captured gh argv; calendar boundaries (1900/2000/leap/rollover/clamp, 12 invalid-date cases, ordinal cross-checked against stdlib), fcntl.flock acquire/release/contention (LOCK_EX|LOCK_NB + BlockingIOError, release-on-failure), and _comment_body marker assertions across all four actions are all present and non-vacuous. Three of the author's "reverting reddens" claims were independently re-run and matched exactly (reserved-id → 2 red, exact-text dedupe → 6 red, newest-wins marker → 2 red). One correction to the author's comment: the claim that "ci.yml triggers on main only, so no test job runs here" is inaccurate — pull_request: branches: [main] filters on the PR's base branch, so the full unit-test matrix (3.10/3.11/3.12) and markdown-link validation do run on this PR; CI is a real gate here. Remaining gaps are low-risk: the live gh wire path and GraphQL page-truncation behavior are untested (acceptable for an offline example suite), and the two new Important findings above need accompanying tests.
Verification
Verifier skipped (no changes under src/cli_agent_orchestrator/ — examples/docs/tests only). The tests reviewer ran the shipped suite in the PR worktree (87 passed) and re-executed three mutation-matrix claims with exact matches. The correctness reviewer reproduced both new defects by multi-run simulation (reopened-PR suppression over 8 weekly runs; score-0 stagnation over 12 runs at multiple cadences) and verified the five prior behavioral fixes the same way. The security reviewer confirmed validation-before-network ordering for reserved ids, list-form subprocess argv throughout (no shell=True), viewerDidAuthor forgery rejection, and that the new await_owner_deadline branch adds no mutation surface. The conventions reviewer verified black/isort/markdown-links clean with the repo's own tooling and that no dependencies, lockfile, or __pycache__ artifacts entered the diff.
Verdict
Request changes — narrow scope this round: fix the reopened-PR enforcement suppression (epoch-scope the stage dedup) and the score-0 streak reset (or-defaulting on a falsy score), each with a test; everything raised previously is verified fixed and the PR is otherwise in strong shape.
…ning flag as env Three defects observed on the 2026-07-31 run of PR awslabs#529 (a 48-minute, 1,693-line review): 1. Reap race — the wait loop expired at 05:22:06 and the report landed at 05:23, so a SUCCESSFUL review was logged as 'awslabs#529 — no report (head moved or review did not complete)' and its Telegram decision message was skipped. Adds a bounded settle pass (SETTLE_SECS, default 180s) that keeps reaping while any launched PR still lacks its report file, exiting as soon as the last one lands. 2. Retrospection ran against a stale snapshot — it was dispatched at 05:16, before the 05:23 report existed, so it observed 'no report / session stalled', found zero outcomes, and correctly-but-uselessly stored no lessons. Retrospection now runs after the settle pass and is skipped entirely when any report is still missing, rather than distilling from a half-finished batch. 3. Self-learning was blocked by Amazon agent sandboxing, not by prompt adherence or a missing MCP mount (both of which I mis-diagnosed earlier — cao-mcp-server did load, and the supervisor did follow its Step 7). The sandbox gives the agent a synthetic ~/.aws, so it cannot read settings.json and is_learning_enabled() fails closed; report_outcome then returns 'learning-disabled' while cao-server reports it enabled. Both launch sites now forward --env CAO_MEMORY_LEARNING_ENABLED=true, which takes precedence over settings.json. Only forwarded when the server confirms learning is on (GET /outcomes 200). Verified: env var reaches the agent process and report_outcome returns success:true from inside a launched session (probe row deleted); settle loop exits at 6s when a report lands at 5s, bounds at SETTLE_SECS when none arrives, and correctly skips retrospection in that case.
Summary
Safety
Validation
cao workflow validate: validuv run black --check ...: passeduv run isort --check-only ...: passeduv run mypy ... --ignore-missing-imports: passeduv run pytest test/examples/test_pr_health_workflow_example.py test/services/test_flow_service.py test/services/test_script_lint.py test/services/test_workflow_spec_service.py -q --no-cov: 131 passeduv run python scripts/validate_markdown_links.py: passedgit diff --check: passed