diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index eb581df..c7e18d8 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -42,7 +42,7 @@ Checks: C-36 work-record-flavor-unknown WARN No flavor is set but not in the closed list C-37 residual-provenance-missing WARN No flavor: residual without origin/origin_ref C-38 task-flavor-drift WARN Yes task flavor differs between file and DB (file wins) - C-39 task-wait-unqualified WARN No unrouted human wait (open non-residual) or wait task has none of task-block depends_on, needs_human: true, blocked_on: message-from:, wait_until: YYYY-MM-DD (CUST-WP-0074 rules 1/6) + C-39 task-wait-unqualified WARN No wait task has none of task-block depends_on, needs_human: true, blocked_on: message-from:, wait_until: YYYY-MM-DD (CUST-WP-0074 rules 1/6) C-40 task-wait-blocker-satisfied WARN No wait task whose depends_on targets are all terminal, whose decision_id is resolved, or whose wait_until has passed (CUST-WP-0074 rules 3/4) C-41 task-wait-qualifier-drift WARN Yes needs_human/blocking_reason/decision_id/blocked_on/wait_until differ between file and DB (file wins) C-42 workplan-blocked-no-wait WARN No blocked workplan has no task in status wait (CUST-WP-0074-T06) @@ -2082,21 +2082,6 @@ def check_repo( file_value=t_status, fixable=False, ) - # CUST-WP-0075 P1: human gates belong in the decision queue. - if ( - wait_fields["needs_human"] and not wait_fields["decision_id"] - and not is_residual_flavor(meta.get("flavor")) - ): - report.add( - severity="WARN", check_id="C-39", - message=( - f"Human wait task '{t_id}' in workplan '{meta.get('id', fname)}' " - "has needs_human: true without decision_id — unrouted human gate" - ), - file_path=f"{fname}#{t_id}", - file_value="needs_human: true", - fixable=False, - ) # C-40: rule 3 for a wait window — the date has passed (UTC today). if wait_until and _date.fromisoformat(wait_until) < datetime.now(timezone.utc).date(): report.add( @@ -2829,61 +2814,6 @@ def _check_blocked_unblock_sweep( ) - # Task-level returns can clear a wait even after somebody read the reply. - # Preserve the legacy workplan check above; task findings carry stable IDs - # for an owner runner to deduplicate without changing task status here. - messages_by_agent: dict[str, list[dict]] = {} - for wp_file, meta, body in workplan_infos: - if ( - normalise_workstream_status(str(meta.get("status", ""))) in CLOSED_WORKSTREAM_STATUSES - or is_residual_flavor(meta.get("flavor")) - ): - continue - for task in get_tasks_from_workplan(meta, body): - if normalise_task_status(task.get("status", "todo")) != "wait": - continue - counterpart = parse_blocked_on(str(task.get("blocked_on") or "")) - if not counterpart: - continue - if counterpart not in messages_by_agent: - rows = _api_get(api_base, "/messages", { - "to_agent": repo_slug, "from_agent": counterpart, - "unread_only": False, "limit": 200, - }) or [] - messages_by_agent[counterpart] = rows if isinstance(rows, list) else [] - # Dated notes are the available acknowledgement watermark. Date-only - # notes cover the whole UTC day, avoiding same-day re-notification. - note_dates = [] - for value in re.findall(r"(?m)^\s*(?:[-*]\s+)?(?:\*\*)?(\d{4}-\d{2}-\d{2})\b", str(task.get("description") or "")): - parsed = _parse_message_timestamp(value) - if parsed: - note_dates.append(parsed + timedelta(days=1)) - explicit_updated = _parse_message_timestamp(task.get("updated")) - cutoff = max(note_dates + ([explicit_updated] if explicit_updated else []), default=None) - hits = [] - for message in messages_by_agent[counterpart]: - if message.get("from_agent") != counterpart or message.get("archived_at"): - continue - created = _parse_message_timestamp(message.get("created_at")) - if created and (created >= cutoff if cutoff else not message.get("read_at")): - hits.append(message) - if not hits: - continue - hits.sort(key=lambda message: str(message.get("created_at", ""))) - task_id = str(task.get("id", "")).strip() - message_ids = [str(message["id"]) for message in hits if message.get("id")] - report.add( - severity="WARN", check_id="C-25", - message=(f"Owner-return task '{task_id}' waiting on {counterpart} has " - f"{len(hits)} reply/replies after its last dated note " - f"(or unread when undated) — owner action needed: {', '.join(message_ids)}"), - file_path=f"{workplan_display_path(repo_dir, wp_file)}#{task_id}", - file_value=str(task.get("blocked_on")), - fixable=False, - _fix_context={"task_record_id": task_id, "message_ids": message_ids}, - ) - - def _check_inbox_hygiene(api_base: str, repo_slug: str, report: ConsistencyReport) -> None: hygiene = collect_inbox_hygiene(api_base, repo_slug) if hygiene["stale_unread_count"]: @@ -4324,7 +4254,6 @@ def report_to_dict(report: ConsistencyReport) -> dict: "file_value": i.file_value, "db_value": i.db_value, "fixable": i.fixable, - **({"owner_return": i._fix_context} if i.check_id == "C-25" and i._fix_context.get("task_record_id") else {}), } for i in report.issues ], diff --git a/tests/test_consistency_check.py b/tests/test_consistency_check.py index 9540b26..7e8a844 100644 --- a/tests/test_consistency_check.py +++ b/tests/test_consistency_check.py @@ -2199,7 +2199,7 @@ class TestTaskWaitQualifiers: repo = self._make_repo( tmp_path, "id: STATE-WP-0001-T01\nstatus: wait\npriority: high\n" - "state_hub_task_id: \"task-1\"\nneeds_human: true\ndecision_id: decision-1\n" + "state_hub_task_id: \"task-1\"\nneeds_human: true\n" "blocking_reason: \"operator must provision creds\"", ) self._install(monkeypatch, self._fake_get(repo)) @@ -2314,7 +2314,7 @@ class TestTaskWaitQualifiers: repo = self._make_repo( tmp_path, "id: STATE-WP-0001-T01\nstatus: wait\npriority: high\n" - "state_hub_task_id: \"task-1\"\nneeds_human: true\ndecision_id: decision-1\n" + "state_hub_task_id: \"task-1\"\nneeds_human: true\n" "decision_id: CCR-2026-0004\n" "blocking_reason: \"operator must provision creds\"", extra_files={"docs/decisions.md": decision_doc}, @@ -2532,56 +2532,3 @@ class TestTaskWaitQualifiers: assert ("/tasks/task-1", wait_fields) in patches assert any("C-20 fixed" in fix for fix in report.fixes_applied) assert any("C-41 fixed" in fix for fix in report.fixes_applied) - - -class TestRoutedHumanGates: - WORKPLAN_HEAD = TestTaskWaitQualifiers.WORKPLAN_HEAD - _make_repo = TestTaskWaitQualifiers._make_repo - _fake_get = TestTaskWaitQualifiers._fake_get - _install = TestTaskWaitQualifiers._install - _issues = staticmethod(TestTaskWaitQualifiers._issues) - @pytest.mark.parametrize("status,flavor,decision,expected", [ - ("blocked", "implementation", "", 1), - ("active", "implementation", "decision-1", 0), - ("blocked", "residual", "", 0), - ("finished", "implementation", "", 0), - ("archived", "implementation", "", 0), - ]) - def test_human_gate_routing(self, tmp_path, monkeypatch, status, flavor, decision, expected): - repo = self._make_repo(tmp_path, "id: STATE-WP-0001-T01\nstatus: wait\nneeds_human: true\n" - + (f"decision_id: {decision}\n" if decision else "")) - path = repo / "workplans/STATE-WP-0001-waiting.md" - path.write_text(path.read_text().replace("status: blocked", f"status: {status}\nflavor: {flavor}", 1)) - self._install(monkeypatch, self._fake_get(repo)) - issues = self._issues(check_repo("http://unused", "demo-repo"), "C-39") - assert len(issues) == expected - if issues: - assert "without decision_id" in issues[0].message - assert not issues[0].fixable - - -@pytest.mark.parametrize("read_at,date,flavor,status,description,expected", [ - ("2026-10-03T12:00:00Z", "2026-10-03T11:00:00Z", "implementation", "blocked", "2026-10-01: still waiting.", 1), - (None, "2026-10-01T11:00:00Z", "implementation", "active", "2026-10-02: reply reviewed; still waiting.", 0), - (None, "2026-10-03T11:00:00Z", "residual", "blocked", "2026-10-01: waiting.", 0), - (None, "2026-10-03T11:00:00Z", "implementation", "finished", "2026-10-01: waiting.", 0), - (None, "2026-10-03T11:00:00Z", "implementation", "ready", "Undated wait.", 1), - ("2026-10-03T12:00:00Z", "2026-10-03T11:00:00Z", "implementation", "ready", "Undated wait.", 0), -]) -def test_task_owner_return_sweep(tmp_path, monkeypatch, read_at, date, flavor, status, description, expected): - from consistency_check import _check_blocked_unblock_sweep - - queries = [] - def fake_get(_api_base, _path, params): - queries.append(params) - return [{"id": "reply-1", "from_agent": "provider", "created_at": date, "read_at": read_at}, - {"id": "unrelated", "from_agent": "other", "created_at": date}] - monkeypatch.setattr("consistency_check._api_get", fake_get) - body = f"```task\nid: DEMO-WP-0001-T01\nstatus: wait\nblocked_on: message-from:provider\n```\n\n{description}" - report = ConsistencyReport(repo_slug="demo", repo_path=str(tmp_path)) - _check_blocked_unblock_sweep("http://unused", "demo", tmp_path, - [(tmp_path / "plan.md", {"status": status, "flavor": flavor}, body)], report) - assert len(report.issues) == expected - if expected: - assert report.issues[0]._fix_context == {"task_record_id": "DEMO-WP-0001-T01", "message_ids": ["reply-1"]} - assert queries[0]["unread_only"] is False diff --git a/workplans/STATE-WP-0094-routed-gates-and-owner-returns.md b/workplans/STATE-WP-0094-routed-gates-and-owner-returns.md deleted file mode 100644 index 545075c..0000000 --- a/workplans/STATE-WP-0094-routed-gates-and-owner-returns.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -id: STATE-WP-0094 -type: workplan -title: "Route human gates and expose actionable owner returns" -domain: infotech -repo: state-hub -status: active -owner: codex -topic_slug: infotech -created: "2026-10-03" -updated: "2026-10-04" -flavor: implementation -related: [CUST-WP-0075, STATE-WP-0086, RMGR-WP-0005] -state_hub_workstream_id: "2c4e96a3-ca0d-5003-b759-880976600ecc" ---- - -Owner acceptance of Custodian proposal b4825539-1425-4028-a2e9-9243785a344b. -Scope is source-level monitoring and diagnosis. No schedules, production rollout, -human decision resolution or automatic wait-status transitions are authorized. - -## Warn on unrouted human waits (P1) - -```task -id: STATE-WP-0094-T01 -status: done -priority: high -state_hub_task_id: "ddb88e77-06c0-5e88-be29-a40f3869b392" -``` - -Extend C-39 for needs_human waits in open non-residual workplans without -decision_id. Routed gates, residuals and terminal workplans are excluded. - -## Detect task-level owner replies (P5) - -```task -id: STATE-WP-0094-T02 -status: done -priority: high -state_hub_task_id: "0c888d53-3a39-5f8d-a1a9-6f8e6f258bc6" -``` - -Extend C-25 to task blocked_on markers. Compare messages, including read replies, -against the task's latest dated note; keep stable task/message IDs for deduplication. -Undated tasks use unread replies conservatively. This detects work for owners; -it does not awaken agents or create duplicate tasks on every checker run. - -## Integrate deduplicated owner wakeups with the supported runner - -```task -id: STATE-WP-0094-T03 -status: todo -priority: medium -state_hub_task_id: "cd73e265-68b0-5dd5-9874-d1d439910a7e" -``` - -Select the current owner runner, consume C-25 task/message identities once, -record receipts and provide an opt-in activation plan. Reuse the Custodian -dependency monitor and established runner; do not introduce another schedule. -Production activation remains a separately reviewed founder action. - -## Diagnose qualifier projection parity (P6a) - -```task -id: STATE-WP-0094-T04 -status: todo -priority: high -state_hub_task_id: "cd03399c-a905-5804-9700-e5e763d2500b" -``` - -Investigate report 70d08f35-a008-4b2c-878d-d9f32db892ce with the current editable -CLI and direct checker from alternate CWDs. Separate missing canonical record_id -from real UUID-addressed qualifier drift. Retain the historical report even if -current source does not reproduce it; correct only an evidenced defect. - -## Assess nightly graph maintenance against the Forge boundary (P6) - -```task -id: STATE-WP-0094-T05 -status: todo -priority: medium -state_hub_task_id: "ec0acd06-0e2f-5b5c-ae8a-6a3241041d4c" -``` - -Decline enabling the proposed legacy --all --remote schedule: production values -explicitly disable it pending RMGR-WP-0005-T11, and ADR-002 prohibits central -reads/writes of workstation checkouts. Confirm current Forge reconciliation -coverage, task-level edge support and supported graph freshness schedule before -preparing any production change. No production action in this workplan session. - - -2026-10-04 owner assessment: P1 is implemented in C-39; P5 detection is -implemented in C-25, with task/message IDs exposed in JSON. Owner wakeup execution -remains T03 and is not represented as delivered by detection alone. Date-only -notes acknowledge their full UTC day; undated waits retain unread-only behavior. -The inbox query is bounded to 200 replies per awaited agent; a paginated runner -must retain stable deduplication and evidence before activation. - -P6a reproduction retained: primary snapshot for ops-warden task -33587361-2b8a-5540-9c4f-d7b4f8ef25e0 (WARDEN-WP-0041-T03) has both blocked_on and -blocking_reason null while its file carries message-from:railiance-platform and -an explicit reason. A read-only current checker run narrowed to that owner plan -emits C-41 for both fields. record_id is also null but does not hide the task from -UUID-addressed C-41. Installed statehub resolves the same editable checkout and -Python interpreter; its API_BASE is the direct checker's default localhost:8000. -The earlier CLI PASS therefore is not accepted as convergence evidence. Its -causality remains T04; do not call the gap fixed on this evidence. - -P6 disposition: decline the requested legacy nightly remote checkout sweep in -this form. Production Helm source explicitly disables it pending RMGR-WP-0005-T11; -the supported alternative is immutable Forge reconciliation (STATE-WP-0086) -and a separately reviewed freshness/wakeup runner under T03/T05. No scheduler, -deployment, credential or human decision was changed.