Merge readiness: worker evidence¶
Task: 34b790f8-2a97-4ae6-bfd4-0763d4174b80, frozen version 2. Execution: 1f17ec2d-0dcb-4e71-8184-496a7efe4d77. Repository: h8v6/heyaira; branch: heyaira/34b790f8. Source and starting HEAD: 42013a6fea0f92cd529d05370c17293d32f7d580. The starting worktree was clean; no carried candidate was present. No subagents were used. Lead model: gpt-6.1-sol.
Cause: a Bridge CI follow-up silently ended green, exposing neither a merge recipient nor a durable reminder to a restarted session. The patch adds one fenced history entry and notification, a shared SQL readiness rule and recipient overlay, and restart/task read fields. It adds no table, migration, GitHub request, merge, watch, task-column write or scheduling change. Confidence: high for the PostgreSQL/ASGI paths tested below; real TCP integration remains unverified in this sandbox. This is worker evidence, not independent acceptance.
Commands ran in the assigned workspace, except the isolated base run. TMPDIR pointed inside the workspace; the runner used the prepared local Unix-socket PostgreSQL capability. No DSN or credential is included here. All fixtures and GitHub/provider replies are synthetic. No installation, push, PR, merge, deployment or production mutation occurred.
K1 — base regression and HEAD behavior¶
Command, on an isolated archive of 42013a6 with only the final tests/test_merge_ready.py copied in:
python scripts/test_postgres.py tests/test_merge_ready.py -p no:cacheprovider
Working directory: .heyaira/check-tmp/base, prepared using:
git archive 42013a6fea0f92cd529d05370c17293d32f7d580 | tar -x -C .heyaira/check-tmp/base
Exit: 1. Actual result: 15 failed, 4 passed in 21.29s; no skips, import or collection errors. Full actual output: base regression log. HEAD-only tests were not copied or run on base.
| Regression | Base result | HEAD result (K2) |
|---|---|---|
| M1 | AssertionError: task_ci_passed count 0, expected 1 | pass |
| M2(a) | AssertionError: ready_to_merge count 0, expected 1 | pass |
| M2(b) | TypeError: absent next_actor instead of owner assignment | pass |
| M2(c), M2(d) | KeyError: absent workflow.merge_waiting | pass |
| M2(e) | AssertionError: override assignment filter selects no task | pass |
| M2(f), published-task hold rejection | pass | pass |
| M2(f), shared hold-overlay precedence | not run on base (HEAD-only) | pass |
| M3 | AssertionError: absent continuity_context.merge_waiting | pass |
| M4, no readiness/push for known merged publication | pass | pass |
| M4h, empty restart reminder for known merged publication | not run on base (HEAD-only) | pass |
| M5, no correction after direct merged-PR reaction | pass | pass |
| M5, readiness half | KeyError: absent workflow.merge_waiting | pass |
| M6(a) | AssertionError: absent workflow.merge_waiting | pass |
| M6(b) | KeyError: absent workflow.merge_waiting | pass |
| M7, running then passing second workflow | AssertionError: ready_to_merge count 0, expected 1 | pass |
| M7, incomplete paged run list | pass | pass |
| M8(a/b) | 3b classification/last-poll assertions pass, then KeyError: absent workflow.merge_waiting | pass |
| M9 | AssertionError: ready pushes count 0, expected 2 after correction | pass |
| M10 | AssertionError: ready entries/events count 0, expected 1 | pass |
| M11 | KeyError: absent workflow.merge_waiting | pass |
| M12, M13, M14 | not run on base (HEAD-only) | pass |
K1's six required base failures are asserted behavior failures, not missing imports or zero selections. All 19 integration regressions and 7 HEAD-only checks passed in the final K2 run.
During test authoring, the first HEAD run stopped with 1 failed / 2 passed because the owner fixture used a session rather than the required person. The next run had 2 failed / 17 passed due to the overflow fixture's ambiguous UUID parameter and a timestamp-format assertion. Those test-fixture errors were corrected before the final base and K2 runs; neither is claimed as a production defect.
K2 — selected PostgreSQL acceptance and separate TCP checks¶
Command:
python scripts/test_postgres.py tests/test_merge_ready.py tests/test_merge_ready_head.py tests/test_ci_correction.py tests/test_task_ci.py tests/test_schema_guard.py tests/test_notification_docs_contract.py tests/test_docs_contract.py -p no:cacheprovider
Exit: 0. Actual output:
........................................................................ [ 55%]
.......................................................... [100%]
130 passed in 91.32s (0:01:31)
postgres_acceptance=passed: selected tests, zero skipped tests
Each TCP file was first invoked separately with PYTHONPATH=src:
python -m pytest tests/<file> -p no:cacheprovider -rs
All exited 0 and skipped because their isolated fixture database variable was not set. A second equivalent run prepared fresh disposable databases through the same runner's normalization/database helpers, passed only those test database URLs and PYTHONPATH=src to each subprocess, and invoked the command above separately. Only the random databases created by that wrapper were dropped.
| File | Exit | Passed | Skipped | Actual limitation |
|---|---|---|---|---|
| test_ci_follow_up.py | 0 | 0 | 27 | listening sockets denied |
| test_publication_end_state.py | 0 | 0 | 17 | listening sockets denied |
| test_server_checks.py | 0 | 0 | 8 | listening sockets denied |
| test_org_v1_b4.py | 0 | 0 | 10 | listening sockets denied |
| test_org_v1_b1.py | 0 | 0 | 6 | listening sockets denied |
The earlier missing-database runs had the same counts. These are skips, not passed integration tests; CI must exercise them with listening capability. The frozen criterion explicitly permits reporting these TCP skip counts. tests/test_backup_state_marker.py was not run, as instructed. No table was added, so MARKER_TABLES is unchanged.
Actual final TCP output:
test_ci_follow_up.py¶
============================= test session starts ==============================
platform darwin -- Python 3.12.7, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/jablonskib/HeyAira-agents/.heyaira-worktrees/heyaira/34b790f8-worker
configfile: pyproject.toml
plugins: asyncio-1.4.0, anyio-4.15.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 27 items
tests/test_ci_follow_up.py sssssssssssssssssssssssssss [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_ci_follow_up.py:65: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:86: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:101: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:119: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:142: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:159: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:178: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:191: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:211: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:225: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:236: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:259: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:282: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:320: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:369: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [2] tests/test_ci_follow_up.py:407: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:427: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:467: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:478: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:499: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:517: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:532: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:561: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:586: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:619: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_ci_follow_up.py:645: listening sockets are not permitted here (agent sandbox); runs in CI
============================= 27 skipped in 1.24s ==============================
test_publication_end_state.py¶
============================= test session starts ==============================
platform darwin -- Python 3.12.7, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/jablonskib/HeyAira-agents/.heyaira-worktrees/heyaira/34b790f8-worker
configfile: pyproject.toml
plugins: asyncio-1.4.0, anyio-4.15.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 17 items
tests/test_publication_end_state.py sssssssssssssssss [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_publication_end_state.py:38: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:53: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:81: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:93: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:128: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:142: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:181: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:207: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:233: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:251: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:266: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:285: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:312: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:341: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:361: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:408: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_publication_end_state.py:430: listening sockets are not permitted here (agent sandbox); runs in CI
============================= 17 skipped in 1.22s ==============================
test_server_checks.py¶
============================= test session starts ==============================
platform darwin -- Python 3.12.7, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/jablonskib/HeyAira-agents/.heyaira-worktrees/heyaira/34b790f8-worker
configfile: pyproject.toml
plugins: asyncio-1.4.0, anyio-4.15.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 8 items
tests/test_server_checks.py ssssssss [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_server_checks.py:72: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:87: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:102: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:122: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:134: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:162: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:175: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_server_checks.py:193: listening sockets are not permitted here (agent sandbox); runs in CI
============================== 8 skipped in 1.29s ==============================
test_org_v1_b4.py¶
============================= test session starts ==============================
platform darwin -- Python 3.12.7, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/jablonskib/HeyAira-agents/.heyaira-worktrees/heyaira/34b790f8-worker
configfile: pyproject.toml
plugins: asyncio-1.4.0, anyio-4.15.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 10 items
tests/test_org_v1_b4.py ssssssssss [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_org_v1_b4.py:107: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:121: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:135: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:148: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:176: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:199: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:228: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:258: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:295: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b4.py:324: listening sockets are not permitted here (agent sandbox); runs in CI
============================= 10 skipped in 1.22s ==============================
test_org_v1_b1.py¶
============================= test session starts ==============================
platform darwin -- Python 3.12.7, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/jablonskib/HeyAira-agents/.heyaira-worktrees/heyaira/34b790f8-worker
configfile: pyproject.toml
plugins: asyncio-1.4.0, anyio-4.15.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 6 items
tests/test_org_v1_b1.py ssssss [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_org_v1_b1.py:103: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b1.py:125: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b1.py:160: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b1.py:174: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b1.py:196: listening sockets are not permitted here (agent sandbox); runs in CI
SKIPPED [1] tests/test_org_v1_b1.py:232: listening sockets are not permitted here (agent sandbox); runs in CI
============================== 6 skipped in 1.29s ==============================
K3 — contract and whitespace¶
PYTHONPATH=src python scripts/export_mcp_contract.py --check
Exit: 0. Actual output: MCP contract is up to date.
PYTHONPATH=src python scripts/check_docs_contract.py
Exit: 0. Actual output: documentation contract passed: 49 MCP tools covered.
git diff --check
Exit: 0; no output (clean). Git was invoked directly as /Library/Developer/CommandLineTools/usr/bin/git because Apple's /usr/bin/git shim emitted denied xcrun cache writes; the Git operations themselves succeeded.
K4 — scope and ancestry¶
The implementation is confined to src/, tests/, docs/ and contracts/. The only modified existing workflow.py functions are _known_merged (query-string constant extraction) and follow_ci (the required ended-branch elif); the new function is _ready_to_merge and the other additions are fixed SQL constants. No migration, existing TCP test module, task_ci.py, stage projection or protected CI/publication function was edited.
Exact committed-HEAD checks are recorded below after the implementation commit. The result SHA and its evidence-only successor are recorded in the HeyAira receipt.
Final regression-module fingerprints¶
- tests/test_merge_ready.py: SHA-256 6d26a6207305e39d8f2afd5f65c323a5a61d0d4c9d9ef9847d3ebf6a880bf3d1
- tests/test_merge_ready_head.py: SHA-256 1d5ccff5a00d9cf879b857130c66b14adf50ceefdf74a1113aaefe33d7dba183
Criterion records¶
criterion_id: K1 outcome: pass evidence_path: docs/reports/merge-ready-34b790f8-evidence.md feedback: Base exit 1 with six required assertion failures and merged/no-correction controls passing; K2 exit 0, all HEAD regressions pass.
criterion_id: K2 outcome: pass evidence_path: docs/reports/merge-ready-34b790f8-evidence.md feedback: Selected runner exit 0, 130 passed and zero skipped; separate TCP exits 0 with 27/17/8/10/6 socket-denied skips; backup-state-marker test intentionally unrun.
criterion_id: K3 outcome: pass evidence_path: docs/reports/merge-ready-34b790f8-evidence.md feedback: MCP and docs contract checks exit 0; git diff --check exit 0 with no whitespace defects.
criterion_id: K4 outcome: pass evidence_path: docs/reports/merge-ready-34b790f8-evidence.md feedback: Committed source..HEAD path and workflow diffs exit 0; ancestor check exit 0; only authorized paths and existing workflow functions changed as specified.
Committed-HEAD K4 results¶
Implementation commit: c70b76a1581964146c97e348d4b54790cf4807c2; its parent is the source/base 42013a6fea0f92cd529d05370c17293d32f7d580. All tests and contract checks above ran against exactly these implementation/test/contract bytes. The next commit changes only this evidence file; no tested code is changed.
Command:
git diff --name-only 42013a6fea0f92cd529d05370c17293d32f7d580..HEAD
Exit: 0. Actual output:
contracts/mcp-tools.json
docs/concepts/task-version-execution.md
docs/reference/mcp-tools.md
docs/reference/notifications.md
docs/reports/merge-ready-34b790f8-base.txt
docs/reports/merge-ready-34b790f8-evidence.md
src/heyaira/bridge_reads.py
src/heyaira/server.py
src/heyaira/task_org.py
src/heyaira/workflow.py
tests/test_merge_ready.py
tests/test_merge_ready_head.py
All 12 paths are in src/, tests/, docs/ or contracts/; no migrations.
git merge-base --is-ancestor 42013a6fea0f92cd529d05370c17293d32f7d580 HEAD
Exit: 0; no output. The source is an ancestor.
git diff 42013a6fea0f92cd529d05370c17293d32f7d580..HEAD -- src/heyaira/workflow.py
Exit: 0. Actual output:
diff --git a/src/heyaira/workflow.py b/src/heyaira/workflow.py
index 7180468..c7177b3 100644
--- a/src/heyaira/workflow.py
+++ b/src/heyaira/workflow.py
@@ -18,6 +18,34 @@ from .bridge_transport import BridgeTransportError
from .db import record_dict
from .execution import ExecutionSpec, Profile, StrictInput, digest, head_matches
+MERGE_READY_DAYS = 7
+_KNOWN_MERGED_SQL = """SELECT 1 FROM task_workflow_events WHERE workflow_id=w.id AND evidence->>'commit'=w.candidate_commit
+ AND ((stage IN ('pr_opened','publication_checked') AND evidence->'server_check'->>'merged'='true')
+ OR (stage='ci_completed' AND evidence->'reaction'->>'why'='pull_request_merged'))"""
+_CI_NEWEST_SQL = """SELECT DISTINCT ON (coalesce(e.evidence->>'workflow_id', 'name:'||coalesce(e.evidence->>'workflow_name','CI')))
+ e.evidence
+ FROM task_workflow_events e WHERE e.workflow_id=w.id AND e.stage='ci_completed'
+ AND e.evidence->>'commit'=w.candidate_commit
+ ORDER BY coalesce(e.evidence->>'workflow_id', 'name:'||coalesce(e.evidence->>'workflow_name','CI')),
+ coalesce((e.evidence->>'run_number')::bigint, 0) DESC,
+ e.evidence->>'updated_at' DESC NULLS LAST, e.sequence DESC"""
+CI_GREEN_SQL = f"""EXISTS (SELECT 1 FROM task_workflow_events e WHERE e.workflow_id=w.id
+ AND e.stage='ci_completed' AND e.evidence->>'commit'=w.candidate_commit)
+ AND NOT EXISTS (SELECT 1 FROM ({_CI_NEWEST_SQL}) newest
+ WHERE newest.evidence->>'conclusion' IS NULL
+ OR newest.evidence->>'conclusion' NOT IN ('success','neutral','skipped'))"""
+MERGE_READY_SQL = f"""CASE WHEN w.is_current AND w.phase='done' AND w.publication_state='confirmed'
+ AND w.publication->>'commit'=w.candidate_commit AND NOT EXISTS ({_KNOWN_MERGED_SQL})
+ AND ({CI_GREEN_SQL})
+ THEN (SELECT r.evidence FROM task_workflow_events r WHERE r.workflow_id=w.id AND r.stage='ready_to_merge'
+ AND r.evidence->>'commit'=w.candidate_commit
+ AND r.created_at > now() - {MERGE_READY_DAYS} * interval '1 day'
+ ORDER BY r.sequence DESC LIMIT 1) END"""
+_KNOWN_MERGED_QUERY = _KNOWN_MERGED_SQL.replace("w.id", "$1").replace("w.candidate_commit", "$2") + " LIMIT 1"
+_READY_GREEN_QUERY = f"SELECT ({CI_GREEN_SQL}) FROM task_workflows w WHERE w.id=$1"
+_READY_RUNS_QUERY = f"""SELECT newest.evidence FROM task_workflows w
+ CROSS JOIN LATERAL ({_CI_NEWEST_SQL}) newest WHERE w.id=$1 LIMIT 10"""
+
class Criterion(StrictInput):
id: str = Field(pattern=r"^[A-Za-z0-9_.-]{1,128}$")
@@ -1748,9 +1776,7 @@ async def _ci_runs(connection, w, commit):
async def _known_merged(connection, w):
"""GitHub already said the candidate's pull request is merged: when it was published, or on a CI run."""
- return bool(await connection.fetchval("""SELECT 1 FROM task_workflow_events WHERE workflow_id=$1 AND evidence->>'commit'=$2
- AND ((stage IN ('pr_opened','publication_checked') AND evidence->'server_check'->>'merged'='true')
- OR (stage='ci_completed' AND evidence->'reaction'->>'why'='pull_request_merged')) LIMIT 1""",
+ return bool(await connection.fetchval(_KNOWN_MERGED_QUERY,
w["id"], w["candidate_commit"]))
@@ -2464,6 +2490,9 @@ async def follow_ci(pool, git_provider, limit=8):
if (ended is not None and not settled and ended["ci_poll_after"] is None and ended["is_current"]
and ended["phase"] == "done" and ended["publication_state"] == "confirmed"):
await _ci_unfinished(connection, ended, pending)
+ elif (ended is not None and settled and ended["is_current"] and ended["phase"] == "done"
+ and ended["publication_state"] == "confirmed"):
+ await _ready_to_merge(connection, ended)
return count
@@ -2490,6 +2519,38 @@ async def _ci_unfinished(connection, w, pending):
f"workflow:{w['id']}:ci_unfinished:{w['candidate_commit']}")
+async def _ready_to_merge(connection, w):
+ """The fenced green completion records one reminder per candidate, without a GitHub read."""
+ from .server import _enqueue_task_event
+ from .task_org import load_assignments, merge_recipient, merge_waiting_text
+
+ publication = _publication(w)
+ commit = w["candidate_commit"]
+ if (publication.get("commit") != commit or not await connection.fetchval(_READY_GREEN_QUERY, w["id"])
+ or await _known_merged(connection, w)
+ or await connection.fetchval("""SELECT 1 FROM task_workflow_events WHERE workflow_id=$1
+ AND stage='ready_to_merge' AND evidence->>'commit'=$2""", w["id"], commit)):
+ return
+ task = await connection.fetchrow("SELECT * FROM tasks WHERE id=$1 AND project_id=$2", w["task_id"], w["project_id"])
+ owner = await connection.fetchval("""SELECT a.owner_subject_id FROM accounts a
+ JOIN projects p ON p.account_id=a.id WHERE p.id=$1""", w["project_id"])
+ recipient = merge_recipient(task, await load_assignments(connection, task), owner)
+ recipient = {"assignment_id": recipient["id"], **recipient} if recipient else None
+ rows = await connection.fetch(_READY_RUNS_QUERY, w["id"])
+ runs = [json.loads(r["evidence"]) if isinstance(r["evidence"], str) else r["evidence"] for r in rows]
+ evidence = dict(commit=commit, pr_number=publication.get("pr_number"), pr_url=publication.get("pr_url"),
+ recipient=recipient, ci_runs=[{k: r.get(k) for k in ("workflow_name", "run_id", "html_url")} for r in runs])
+ reason = merge_waiting_text(evidence, recipient)
+ version = await connection.fetchval("""UPDATE task_workflows SET version=version+1,updated_at=now()
+ WHERE id=$1 RETURNING version""", w["id"])
+ await connection.execute("""INSERT INTO task_workflow_events(workflow_id,sequence,phase,stage,execution_id,reason,actor_subject,evidence)
+ VALUES($1,$2,'done','ready_to_merge',NULL,$3,'server',$4::jsonb)""", w["id"], version, reason, json.dumps(evidence))
+ who = f"{recipient['label']} ({recipient['role_slug']})" if recipient else "the owner"
+ await _enqueue_task_event(connection, w["project_id"], w["task_id"], "task_ci_passed", "attention",
+ f"workflow:{w['id']}:ready_to_merge:{commit}",
+ detail=f"CI passed on {_pr_text(publication)}; ready to merge; next_actor: {who}.")
+
+
async def _classify_runs(pool, w, runs, git_provider):
"""Classify each listed failed attempt once; GitHub reads never hold a transaction or lock."""
import asyncio
Supplemental AST comparison of every existing workflow.py function against the source (Python ast.parse/ast.dump; Git show of the source), exit 0:
Changed existing workflow functions: _known_merged, follow_ci
Added workflow functions: _ready_to_merge
Protected workflow functions unchanged: PASS
Manual diff inspection confirms that follow_ci gained only its ended-branch elif, and _known_merged only replaced its query string with the extracted constant. The fixed SQL query text is the same merged-evidence predicate, shared with the readers.
Git printed a denied optional packed-refs.lock write during commit, but returned exit 0 and created the expected branch commit and parent, verified by git log. The sandbox limitation did not prevent committing. Generated scratch archives and logs are removed before the final worktree check.