C-42: blocked workplan with no wait task (CUST-WP-0074-T06)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 1m2s

Blocked-with-nothing-waiting is a stale status; warn until a qualified
wait task exists or the workplan is re-statused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 237582@bnt-lap001
Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826
This commit is contained in:
tegwick 2026-09-29 21:50:37 +02:00
parent 49fa3ebcdf
commit 7d04c05484
2 changed files with 46 additions and 0 deletions

View file

@ -45,6 +45,7 @@ Checks:
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-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)
(C-20 also indexes task-block depends_on as workplan_dependencies rows with from_task_id set)
(finished¬DoD-Ok is listed by `statehub quality-debt`, not per-file C-warn — avoids historical flood)
@ -1742,6 +1743,26 @@ def check_repo(
},
)
# C-42: a blocked workplan must say what it waits for — at least one
# task in `wait` (which C-39 then requires to be qualified). Blocked
# with no wait task is a stale status, not a block (CUST-WP-0074-T06).
if file_status == "blocked" and not any(
normalise_task_status(s) == "wait" for s in file_task_statuses
):
report.add(
severity="WARN",
check_id="C-42",
message=(
f"Blocked workplan '{ws.get('slug')}' has no task in status "
f"wait — nothing states what it waits for; qualify a wait "
f"task or re-status the workplan"
),
file_path=fname,
db_id=ws_id,
file_value=file_status,
fixable=False,
)
# C-04: status drift — normalise file value before comparing so that
# legacy file/API aliases are not treated as drift. Lifecycle repairs
# take precedence so a stale planning file cannot regress active work.

View file

@ -2442,6 +2442,31 @@ class TestTaskWaitQualifiers:
assert len(self._issues(report, "C-39")) == 1
assert self._issues(report, "C-40") == []
def test_c42_blocked_workplan_without_wait_task(self, tmp_path, monkeypatch):
# A blocked workplan whose only task is todo states nothing about what
# it waits for (CUST-WP-0074-T06). The head fixture is `blocked`.
repo = self._make_repo(
tmp_path,
"id: STATE-WP-0001-T01\nstatus: todo\npriority: high\n"
"state_hub_task_id: \"task-1\"",
)
self._install(monkeypatch, self._fake_get(repo))
report = check_repo("http://unused", "demo-repo")
c42 = self._issues(report, "C-42")
assert len(c42) == 1 and c42[0].fixable is False
assert "has no task in status wait" in c42[0].message
# A qualified wait task satisfies it.
repo = self._make_repo(
tmp_path / "ok",
"id: STATE-WP-0001-T01\nstatus: wait\npriority: high\n"
"state_hub_task_id: \"task-1\"\nneeds_human: true\n"
"blocking_reason: \"operator decision\"",
)
self._install(monkeypatch, self._fake_get(repo))
report = check_repo("http://unused", "demo-repo")
assert self._issues(report, "C-42") == []
def test_c41_ignores_blocks_that_do_not_carry_the_keys(self, tmp_path, monkeypatch):
repo = self._make_repo(
tmp_path,