CUST-WP-0075: reconcile owner waits and decision routing

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a10391-6355-7b41-b0fa-3e14e874f091
This commit is contained in:
tegwick 2026-10-04 00:48:22 +02:00
parent d61ba327fa
commit 831ae0e598
3 changed files with 233 additions and 3 deletions

View file

@ -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 wait task has none of task-block depends_on, needs_human: true, blocked_on: message-from:<agent>, wait_until: YYYY-MM-DD (CUST-WP-0074 rules 1/6)
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:<agent>, 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,6 +2082,21 @@ 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(
@ -2814,6 +2829,61 @@ 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"]:
@ -4254,6 +4324,7 @@ 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
],

View file

@ -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\n"
"state_hub_task_id: \"task-1\"\nneeds_human: true\ndecision_id: decision-1\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\n"
"state_hub_task_id: \"task-1\"\nneeds_human: true\ndecision_id: decision-1\n"
"decision_id: CCR-2026-0004\n"
"blocking_reason: \"operator must provision creds\"",
extra_files={"docs/decisions.md": decision_doc},
@ -2532,3 +2532,56 @@ 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

View file

@ -0,0 +1,106 @@
---
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]
---
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
```
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
```
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
```
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
```
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
```
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.