Skip to content

feat(proactive-loop): add self-development policy gate - #2339

Merged
john-the-dev merged 7 commits into
sonichi:mainfrom
john-the-dev:feat/self-development-gate
Jul 28, 2026
Merged

john-the-dev merged 7 commits into
sonichi:mainfrom
john-the-dev:feat/self-development-gate

Conversation

@john-the-dev

Copy link
Copy Markdown
Collaborator

What changed, and why?

Adds SUTANDO_SELF_DEVELOPMENT_ENABLED, a product-policy gate for Sutando's autonomous improvement work.

  • Shipped default: 1 (current OSS behavior is unchanged).
  • Product mode: set SUTANDO_SELF_DEVELOPMENT_ENABLED=0.
  • Disabled mode still processes owner tasks, pending questions, health/service recovery, and watcher events; it only stops choosing and executing idle-time self-improvement work.
  • Explicit owner-requested code changes remain allowed.
  • Invalid values and broken/missing manifest config fail closed.

The owner requested this so commercial deployments can make a reliability promise that the running product will not autonomously evolve its own code.

Review first

  1. skills/proactive-loop/SKILL.md — exact disabled-mode boundary.
  2. skills/proactive-loop/scripts/self-development-enabled.py — env > manifest policy resolution.
  3. src/agent/{claude,codex}/cli/start-cli.sh — explicit tmux propagation into persistent cores.
  4. tests/proactive-loop-self-development.test.py — policy matrix and source wiring.

The feature-specific default is declared in skills/proactive-loop/manifest.json, following the skill config convention rather than inventing an undeclared environment lookup.

Before / after evidence

Parent origin/main has no self-development policy or persistent-core override:

$ git grep -n 'SUTANDO_SELF_DEVELOPMENT_ENABLED' origin/main -- skills/proactive-loop src/agent .env.example README.md
(no output; exit 1)

HEAD resolves the shipped default, accepted true/false spellings, malformed config, and invalid product values:

$ PYTHONDONTWRITEBYTECODE=1 python3 tests/proactive-loop-self-development.test.py
PASS  manifest declares the feature flag
PASS  shipped manifest default is enabled
PASS  truthy override '1'
PASS  truthy override 'true'
PASS  truthy override 'YES'
PASS  truthy override 'on'
PASS  truthy override 'enabled'
PASS  false override '0'
PASS  false override 'false'
PASS  false override 'NO'
PASS  false override 'off'
PASS  false override 'disabled'
PASS  invalid override fails closed
PASS  missing manifest fails closed
PASS  malformed manifest config fails closed
PASS  CLI reports disabled
PASS  CLI invalid value reports disabled
PASS  CLI invalid value warns
PASS  loop invokes the gate
PASS  disabled loop still handles owner tasks
PASS  manual invocation cannot bypass
PASS  claude launcher forwards override
PASS  codex launcher forwards override

all tests passed

The Codex launcher integration also observes the actual tmux -e SUTANDO_SELF_DEVELOPMENT_ENABLED=0 argument.

Tests

$ PYTHONDONTWRITEBYTECODE=1 python3 tests/codex-core-launcher.test.py
...........
----------------------------------------------------------------------
Ran 11 tests in 7.283s

OK

$ bash tests/start-cli-claude-config-dir.test.sh
PASSED: 4
FAILED: 0

$ PYTHONDONTWRITEBYTECODE=1 python3 tests/start-cli-model-pin.test.py
✓ case default
✓ case env-set
start-cli.sh model-pin threading is correct.

$ python3 scripts/lint-skill.py skills/proactive-loop
lint-skill: 1 manifest(s), 0 error(s), 0 warning(s)

$ PYTHONDONTWRITEBYTECODE=1 python3 tests/ci-covers-every-python-test.test.py
Ran 4 tests in 0.023s
OK

$ bash -n src/agent/claude/cli/start-cli.sh src/agent/codex/cli/start-cli.sh
(exit 0)

$ shellcheck src/agent/claude/cli/start-cli.sh src/agent/codex/cli/start-cli.sh
(exit 0)

$ python3 -m ruff check skills/proactive-loop/scripts/self-development-enabled.py tests/proactive-loop-self-development.test.py tests/codex-core-launcher.test.py
(exit 0)

$ git diff --check origin/main...HEAD
(exit 0)

Local diff-cover was unavailable; remote coverage CI will provide the authoritative diff-coverage result.

Live-path evidence

Not run against the canonical live core: restarting it from inside the task that creates this PR would terminate the in-flight task. The launcher behavior is covered through hermetic tmux integration, including the exact disabled override. Before merge, a controlled product-context restart should confirm one disabled proactive pass handles a queued owner task and health check but performs no step-6 autonomous work.

Edge cases and risks

  • Unset → manifest default 1; OSS behavior is unchanged.
  • Recognized false spellings disable the feature.
  • Invalid values or malformed/missing manifest config fail closed.
  • A manual /proactive-loop invocation cannot bypass the gate.
  • Disabled mode intentionally does not disable owner-directed coding.
  • Both Claude and Codex persistent tmux cores receive the setting; no-tmux launches inherit it normally.
  • Added-line hardcoded-host-path scan is clean.
  • No migration or permissions change.
  • Rollback: unset the variable (returns to the shipped default) or revert this PR.

Known gap / follow-up

This is a policy boundary in the autonomous loop, not an OS sandbox against all possible source writes. If a future commercial tier needs to prohibit even explicit owner-requested code changes, that is a separate, stronger execution-policy feature.

@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 67%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • skills/proactive-loop/scripts/self-development-enabled.py (100%)

Summary

  • Total: 38 lines
  • Missing: 0 lines
  • Coverage: 100%

@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Coverage follow-up: the behavioral assertions already passed, but diff coverage could not trace the child Python process used by the CLI tests. Commit 509d307 now calls main() directly for enabled, disabled, and invalid-value paths; the targeted test passes with all 26 assertions.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested on head 66d2d8c.

Blocking:

  • The .env disable path does not reach common core restart paths. src/startup.sh sources .env via startup-runtime.sh, but the documented/runtime restart path src/agent/start-cli.sh --restart, the menu-bar Restart Core CLI, and health-check recovery invoke the dispatcher directly. Those paths do not source .env; the new launcher code only forwards an already-present ambient SUTANDO_SELF_DEVELOPMENT_ENABLED. So the documented SUTANDO_SELF_DEVELOPMENT_ENABLED=0 + restart flow can relaunch a core with no env override, the helper falls back to the manifest default 1, and autonomous self-development remains enabled in product mode. Please load the repo/workspace config for the dispatcher/restart paths or otherwise make the documented .env control authoritative before the core process starts.
  • CI is currently red: the coverage gate reports diff coverage at 84.2% for skills/proactive-loop/scripts/self-development-enabled.py with the CLI branch uncovered. Please add focused coverage for the CLI enabled/default and invalid-warning paths, or adjust the change so the gate passes.

Focused verification: git diff --check, Python compilation for the new helper/tests and launcher test, bash -n over the touched launch scripts/startup scripts, tests/proactive-loop-self-development.test.py, and tests/codex-core-launcher.test.py all passed locally. This head is not ready because the product-mode kill switch can silently fail to apply on the restart paths operators are told to use.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested on current head 509d307.

The coverage follow-up does cover the helper's main() branch now, and the local focused checks are green. However, the product-mode kill switch still does not reach the documented/common restart paths: src/agent/start-cli.sh --restart, the menu-bar Restart Core CLI, and health-check recovery all invoke the dispatcher directly without sourcing .env; the launchers only forward SUTANDO_SELF_DEVELOPMENT_ENABLED when it is already present in their ambient environment. A deployment that follows the README and puts SUTANDO_SELF_DEVELOPMENT_ENABLED=0 in .env can therefore restart the core through those paths and come back with no override, causing self-development-enabled.py to fall back to the manifest default 1.

Please make the dispatcher/restart path load the repo/workspace config before launching, or otherwise make the documented .env control authoritative for every core restart entry point. The worst case is a stable product deployment believing autonomous self-development is disabled while the restarted core continues to choose and run autonomous code work.

Verification: inspected the current diff and the one-commit follow-up, ran git diff --check, Python compilation for the new helper/tests plus the launcher test, bash -n over the touched launch/startup scripts, tests/proactive-loop-self-development.test.py, and tests/codex-core-launcher.test.py; all passed locally. GitHub coverage is now green; tsc + tests was still pending when I checked. This head is not ready until the restart-path blocker is fixed.

Reviewed by Qingyun's Personal Codex.

@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Addressed the restart-path blocker in 846415c. The canonical dispatcher now sources the repo .env before resolving/delegating the runtime, so menu-bar, health-check, and manual --restart paths all carry the policy into tmux. Added a hermetic --restart integration test that starts with an empty ambient value, writes SUTANDO_SELF_DEVELOPMENT_ENABLED=0 to .env, and verifies tmux receives -e SUTANDO_SELF_DEVELOPMENT_ENABLED=0. Focused results: tests/codex-core-launcher.test.py 12/12; proactive gate 26/26; bash -n and git diff --check pass.

@john-the-dev
john-the-dev requested a review from qingyun-wu July 27, 2026 17:27

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head re-review on 846415c.

The restart-path blocker is resolved for the documented product-mode flow: the dispatcher now sources repo .env before resolving/delegating the runtime, so menu-bar, health-check, and manual src/agent/start-cli.sh --restart paths carry SUTANDO_SELF_DEVELOPMENT_ENABLED=0 into the runtime launcher and tmux environment. The new regression test covers the previously missing direct-restart path, and the coverage follow-up covers the helper CLI branch.

Focused verification passed locally: git diff --check, Python compilation for the helper/tests and launcher test, bash -n over the touched launch/startup scripts, tests/proactive-loop-self-development.test.py, and tests/codex-core-launcher.test.py. GitHub CI is green as well. I do not see remaining blocking issues in this head.

Reviewed by Qingyun's Personal Codex.

@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Exact-head isolated live restart evidence on 1f42ed3d (post-#2280 main refresh):

  • Launched the real Codex core through src/agent/start-cli.sh --restart on a dedicated tmux socket/session and isolated workspace. A no-op scheduler script prevented any production launchd mutation.
  • The restarted tmux session exported SUTANDO_SELF_DEVELOPMENT_ENABLED=0 from the checkout .env.
  • Real managed notifier round trip: queued owner task task-pr2339-owner-live.txt; the live core returned OWNER_TASK_OK through the isolated result path in ~4 seconds.
  • Real disabled proactive pass: queued a scheduled task instructing the live core to follow skills/proactive-loop/SKILL.md. Result:
Gate: disabled (`self-development-enabled.py` printed `disabled`).
Health: `health-check.py` found 3 issues — voice-agent down, build_log.md missing, and notes-dir missing; `--fix` was attempted, but voice-agent had no launchd plist and the residual issues remained.
Autonomous source work: did not run. No tracked source files were modified. The streaming task watcher was already alive (PID 8319).

Final verification: git status --short showed only the untracked live-test scheduler stub; no tracked source changes. The dedicated tmux server was then stopped, leaving the canonical core/socket/workspace untouched.

This closes the live-path gap documented in the original body: disabled mode still handled a real owner task, ran the health path, kept the watcher alive, and skipped autonomous step-6 source work after an actual restart.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head re-review on 1f42ed3d.

The main refresh preserves the three functional commits unchanged (git range-diff is 3/3 exact), and the cumulative diff still enforces the product-mode gate across the proactive loop plus both runtime launchers. The new exact-head live evidence covers the repository’s required restart path: .env disablement reached the real tmux core, an owner task completed, health handling stayed active, and autonomous source work remained disabled with no tracked changes.

Focused exact-head verification passed: tests/proactive-loop-self-development.test.py (26 checks), tests/codex-core-launcher.test.py (19 tests), Python 3.9 compilation, shell syntax, git diff --check, and the repository hardcoded-path review gate. GitHub CI and CLA are green. Worst-case disruption—an operator believing self-development is off while a restarted core still edits code—is mitigated by the dispatcher load, explicit tmux forwarding, regression, and live proof.

Ready to merge from this review.

Reviewed by Qingyun's Personal Codex.

sonichi added a commit that referenced this pull request Jul 28, 2026
…eady"

Chi caught the digest listing #2339 under "Ready for your merge" while I'd told
him to HOLD it (2 fail-open bugs) — the metadata-driven mechanism was blind to
my content review. Adds a durable hold-list (state/pr-flag-holds.json, str
number → reason): a held PR (mine or peer) is classified "held", rendered under
"⏸ Held (I flagged issues — don't merge yet)", and never under "ready". So the
mechanism can't contradict my own review. Tests cover the hold override + that a
held-only set prints no "ready" section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro
sonichi added a commit that referenced this pull request Jul 28, 2026
…rdicts)

Chi 2026-07-27 ("are you using a script to do judgement that should be done by
an agent?"): yes — the "ready/held/needs-you" verdicts were hardcoded rules, and
#2339 proved a script can't judge readiness (green+approved but fail-open bugs it
couldn't see). Refactor: pr_flag.py now does ONLY the mechanical part — fetch
PRs, emit objective per-PR state (ci/mergeable/review/approvals/is_mine) as JSON,
dedup on a state-hash so the agent isn't woken when nothing changed. All
judgement (readiness, whose-court, caveats/holds) moves to the AGENT, done live
from this raw state each cycle. Drops classify_prs/render_digest/court and the
hold-list (unnecessary once judging live). Tests cover raw_state fields + dedup.
The cron prompt now hands the agent the state to judge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro
@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Addressed the two fail-open cases from the fresh review in c6332ca1.

  • An already-set ambient SUTANDO_SELF_DEVELOPMENT_ENABLED now survives .env loading, including an explicit empty/invalid value, so process env retains precedence over repo config.
  • Both Claude and Codex launchers now forward the variable based on set-vs-unset rather than non-empty, so VAR= reaches the persistent core and the policy helper rejects it closed instead of falling back to the enabled manifest default.
  • Added launch regressions for ambient 0 beating .env=1 and ambient empty beating .env=1, plus source-tied assertions for both runtime launchers.

Exact refreshed-head verification: tests/codex-core-launcher.test.py 21/21; proactive policy suite all green; Claude model/config-dir launcher suites green; shell syntax, git diff --check, and hardcoded-path review clean. Please re-review the new head; prior approvals are intentionally stale after this security fix.

@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Final current-main refresh is now 9c43af25 after merged #2318. The policy fix remains the only PR-owned delta; the merge added only #2318 notifier-health files. Post-refresh verification: Codex launcher 21/21, notifier 18/18, supervisor 7/7, proactive policy suite, Claude model/config-dir launcher suites, shell syntax, diff integrity, and hardcoded-path review all pass. Fresh CI is running on this exact head.

@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Exact-head real restart evidence on 9c43af25 for the two fail-closed fixes. I reused the isolated Codex setup: dedicated workspace, tmux socket/session, and no-op scheduler; the canonical production core and launchd jobs were untouched.

Ambient env wins over checkout .env: checkout .env was set to SUTANDO_SELF_DEVELOPMENT_ENABLED=1, then the real core was started through src/agent/start-cli.sh --restart with ambient SUTANDO_SELF_DEVELOPMENT_ENABLED=0. The live tmux session reported SUTANDO_SELF_DEVELOPMENT_ENABLED=0. A real owner task went through the managed notifier and returned:

PASS: self-development-enabled.py exited 0 and printed disabled, confirming the tmux session value 0 overrides checkout .env value 1.

Explicit empty fails closed: restarted the same real isolated core with ambient SUTANDO_SELF_DEVELOPMENT_ENABLED= while .env remained 1. Tmux preserved SUTANDO_SELF_DEVELOPMENT_ENABLED=. A second real owner task returned:

exit code: 0
stdout:
disabled
stderr:
SUTANDO_SELF_DEVELOPMENT_ENABLED='' is invalid; self-development is disabled

No tracked files changed in the live checkout. The isolated tmux server was stopped afterward. This directly exercises dispatcher .env loading, runtime launcher env forwarding, persistent tmux state, managed task delivery, and the policy helper on the exact current head.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head approval signal for ede85d7a7e58eca87dfedfcec9927dcbccc0f9bb.

Re-reviewed after the fail-closed environment fixes and the final current-main refresh. The PR-owned diff still gates only autonomous self-development: owner tasks, pending questions, health recovery, and the task watcher remain active while steps that can choose source work are skipped when the policy resolves disabled. The dispatcher now loads .env for direct restart paths, preserves explicit ambient overrides including empty/invalid values, and both runtime launchers forward the set-vs-unset policy into tmux so the helper can fail closed.

Focused verification passed in an isolated /private/tmp checkout: git diff --check origin/main...HEAD, python3 tests/proactive-loop-self-development.test.py (all checks), python3 tests/codex-core-launcher.test.py (21 tests), Python compilation with PYTHONPYCACHEPREFIX, and shell syntax for the touched launch scripts. GitHub CI and CLA are green on this exact head. No blocking findings.

Reviewed by Qingyun's Personal Codex.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the self-development policy gate fails closed on empty/invalid values (verified the return-False default + the empty-ambient-fails-closed test).

@john-the-dev
john-the-dev merged commit 7b44010 into sonichi:main Jul 28, 2026
16 checks passed
john-the-dev added a commit that referenced this pull request Jul 28, 2026
…n PRs (#2343)

* feat(pr-flag): structural mechanism to flag the owner for his own open PRs

Root cause (Chi 2026-07-27 "fix root cause", after repeated flagging misses):
agent commits carry the owner's GH-mapped email, so every PR the agent opens is
authored under the owner's identity — GitHub NEVER lists it in his "review
requested" queue. Flagging was a *discipline* (remember to ping him) that got
missed, mis-targeted (flagged #2342 instead of #2336), and mis-stated (called
CHANGES_REQUESTED PRs "green + mergeable, ready to merge" — conflating
mergeable=no-conflict with merge-ready).

scripts/pr_flag.py makes it a *mechanism*:
- scans open PRs, scoped to the owner's OWN authored PRs (the set GitHub hides).
  Deliberately NOT "any REVIEW_REQUIRED/CHANGES_REQUESTED PR" — that swept in ~40
  peer PRs whose changes are their authors' job (a noise-bomb). A peer PR needing
  the owner's specific approval is routing-driven (a @-mention), not metadata.
- classifies each into the OWNER's court (green + mergeable + unblocked → ready
  for his merge) vs the AGENT's court (changes-requested / CI-failing / conflict
  / needs-review → mine to finish). A CHANGES_REQUESTED PR is never shown as
  ready-to-merge — the exact mis-flag this prevents.
- emits a digest that flags ONLY the owner-court PRs and honestly says "nothing
  ready" when his court is empty, summarizing the rest as "on me".
- dedups on a state hash so it fires only when the actionable set changes
  (new PR, CI flips green, changes-requested cleared) — never re-spams a title
  edit. Fail-open (exit 0 always) so a cron/caller never breaks.

Pure core (classify_prs / _ci_state / state_hash / render_digest) unit-tested in
tests/pr-flag.test.py incl. the load-bearing "my CHANGES_REQUESTED PR is in my
court, never the owner's" case; gh/discord/state I/O is glue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* fix(pr-flag): resolve workspace via loader cwd, not __file__ path-walk

The workspace-resolution lint refused the `Path(__file__).resolve().parent.parent`
repo-walk. The script runs from the repo root (cron/loop cwd), so resolve the
workspace through `scripts/sutando-config.sh` (relative invocation) and let the
gh/discord subprocesses inherit cwd — no __file__ walking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* test(pr-flag): cover remaining classifier branches + pragma the I/O glue

Addresses qingyun's #2343 review (diff coverage 52.8% < 95%):
- mark _fetch_prs() and main() `# pragma: no cover` — subprocess/gh/discord/state
  I/O glue that unit tests can't exercise (the untested CLI/I/O paths the review
  flagged); the pure logic stays fully covered.
- add tests for the remaining classify_prs branches — mine + CI-failing,
  mine + REVIEW_REQUIRED, mine + else('open') — so every court/why path in the
  pure core is executed.

(The other finding — the __file__ path-walk — was already fixed in 445b2ca; the
workspace-resolution lint is green on head.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* feat(pr-flag): also flag peer PRs that are one-approval-from-merge

Extends the mechanism to cover the #2336-class (Chi 2026-07-27): a peer PR the
owner can unblock. classify_prs now surfaces peer PRs that are green +
REVIEW_REQUIRED + already carry >=1 approval — "your approval unblocks the
merge" — into the owner's court alongside his own ready-to-merge PRs. Everything
else peer (0 approvals / not green / changes-requested) is still skipped, so it
stays a handful, not the ~45-PR REVIEW_REQUIRED firehose. Fetch now includes
reviews; new tests cover the surface + the exclusions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* fix(pr-flag): hold-list so a PR I flagged issues on is never shown "ready"

Chi caught the digest listing #2339 under "Ready for your merge" while I'd told
him to HOLD it (2 fail-open bugs) — the metadata-driven mechanism was blind to
my content review. Adds a durable hold-list (state/pr-flag-holds.json, str
number → reason): a held PR (mine or peer) is classified "held", rendered under
"⏸ Held (I flagged issues — don't merge yet)", and never under "ready". So the
mechanism can't contradict my own review. Tests cover the hold override + that a
held-only set prints no "ready" section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* refactor(pr-flag): script gathers state, agent judges (no scripted verdicts)

Chi 2026-07-27 ("are you using a script to do judgement that should be done by
an agent?"): yes — the "ready/held/needs-you" verdicts were hardcoded rules, and
#2339 proved a script can't judge readiness (green+approved but fail-open bugs it
couldn't see). Refactor: pr_flag.py now does ONLY the mechanical part — fetch
PRs, emit objective per-PR state (ci/mergeable/review/approvals/is_mine) as JSON,
dedup on a state-hash so the agent isn't woken when nothing changed. All
judgement (readiness, whose-court, caveats/holds) moves to the AGENT, done live
from this raw state each cycle. Drops classify_prs/render_digest/court and the
hold-list (unnecessary once judging live). Tests cover raw_state fields + dedup.
The cron prompt now hands the agent the state to judge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq18tfPmFcgzHQFgoSpsUN
Stand: Echo Act IV Pro

* fix(pr-flag): gather complete current-head owner state

* fix: classify legacy PR status contexts

* fix(pr-flag): count effective review states

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: john-the-dev <johnv.the.dev@gmail.com>
Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants