From 84f2e1899c25f6da8884dc7628cac63e9d9a2256 Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 3 Oct 2026 23:10:54 +0200 Subject: [PATCH] CUST-WP-0075: add repeatable dependency monitor and handoff evidence --- .../README.md | 36 +++ .../monitor-2026-10-03.json | 225 ++++++++++++++++++ .../scan_workplan_files.py | 30 ++- scripts/monitor_dependency_triage.py | 140 +++++++++++ tests/test_dependency_triage.py | 98 ++++++++ ...0075-dependency-graph-driven-unblocking.md | 27 ++- 6 files changed, 548 insertions(+), 8 deletions(-) create mode 100644 docs/evidence/2026-10-03-dependency-graph-triage/monitor-2026-10-03.json create mode 100644 scripts/monitor_dependency_triage.py create mode 100644 tests/test_dependency_triage.py diff --git a/docs/evidence/2026-10-03-dependency-graph-triage/README.md b/docs/evidence/2026-10-03-dependency-graph-triage/README.md index 76a7827a..74ad1c52 100644 --- a/docs/evidence/2026-10-03-dependency-graph-triage/README.md +++ b/docs/evidence/2026-10-03-dependency-graph-triage/README.md @@ -25,3 +25,39 @@ Run with `TRIAGE_SCRATCH=` holding `repos.json`, `wps.json`, `waits.json`, - Top roots by transitive fan-in: `SAND-WP-0015-T04` (7 workplans), `SECRETS-WP-0009-T03` (5), `GLAS-WP-0012` (4), `NK-WP-0040` (4), `FEP-WP-0002-T03` (4, a `todo` — not blocked at all), `RAPPS-WP-0014-T03` (3). + +## Repeatable monitor (CUST-WP-0075-T05) + +From the Custodian checkout, with Python and PyYAML installed: + +```bash +python3 scripts/monitor_dependency_triage.py --output /tmp/dependency-triage +``` + +The monitor makes GET requests only. It captures the repository registry, +scans available owner files, compares every open non-residual wait's status +and qualifiers with its hub projection, and records the three T06 handoffs. +`monitor.json` contains counts and actionable findings; `file-view.txt` contains +the graph analysis. Raw registry and file indexes stay in the scratch directory. +No schedule, worker injection, owner-file change, or decision resolution is enabled. + +`unavailable_checkouts` and `parse_warnings` make coverage limits explicit. +The scanner now reads YAML lists and quoted text correctly; historic task +fences containing prose use a reported fallback. The detailed file view includes +residuals for observation; the monitor's actionable counts exclude them. +Terminal predecessors are candidates for review: independent human and owner +gates remain binding. Replies newer than a proposal are candidates, not evidence +of acceptance; reviewed owner workplan IDs or declines still belong in T06. + +The refreshed snapshot is [monitor-2026-10-03.json](monitor-2026-10-03.json). +It supersedes the earlier headline counts for the visible checkouts. Four new +unrouted human gates, sixteen unqualified waits, and twelve qualifier differences +need follow-up through T05; the earlier T02/T03 completion records are historical. +Six registered checkouts were unavailable. The sixteen degraded parses are in +historic Vergabe workplans. These results do not establish fleet-wide completeness. + +Focused regression checks: + +```bash +python3 -m unittest discover -s tests -p test_dependency_triage.py +``` diff --git a/docs/evidence/2026-10-03-dependency-graph-triage/monitor-2026-10-03.json b/docs/evidence/2026-10-03-dependency-graph-triage/monitor-2026-10-03.json new file mode 100644 index 00000000..23d1a8d9 --- /dev/null +++ b/docs/evidence/2026-10-03-dependency-graph-triage/monitor-2026-10-03.json @@ -0,0 +1,225 @@ +{ + "open_non_residual_workplans": 100, + "workplan_statuses": { + "active": 1, + "backlog": 5, + "blocked": 94 + }, + "wait_tasks": 232, + "human_gates": 79, + "unrouted_human_gates": [ + "FEP-WP-0009-T01", + "INFD-WP-0005-T05", + "INFD-WP-0005-T10", + "RPF-WP-0035-T08" + ], + "unqualified_waits": [ + "EMERGENT-WP-0002-T02", + "EMERGENT-WP-0002-T05", + "EMERGENT-WP-0002-T07", + "EMERGENT-WP-0002-T08", + "EMERGENT-WP-0002-T09", + "EMERGENT-WP-0002-T10", + "NK-WP-0011-T02", + "NK-WP-0011-T03", + "NK-WP-0011-T04", + "NK-WP-0011-T05", + "NK-WP-0011-T06", + "NK-WP-0011-T07", + "NK-WP-0011-T08", + "NK-WP-0011-T09", + "QONTO-WP-0006-T02", + "QONTO-WP-0006-T03" + ], + "owner_return_waits": 56, + "terminal_dependency_waits": [ + { + "task": "ACTIVITY-WP-0042-T05", + "depends_on": [ + "ACTIVITY-WP-0042-T01" + ], + "needs_human": false, + "blocked_on": null, + "wait_until": null + }, + { + "task": "ACTIVITY-WP-0042-T08", + "depends_on": [ + "RPF-WP-0049-T05" + ], + "needs_human": false, + "blocked_on": null, + "wait_until": null + }, + { + "task": "INFD-WP-0005-T05", + "depends_on": [ + "INFD-WP-0005-T04" + ], + "needs_human": true, + "blocked_on": "message-from:railiance-platform", + "wait_until": null + }, + { + "task": "USER-WP-0037-T03", + "depends_on": [ + "USER-WP-0037-T02" + ], + "needs_human": false, + "blocked_on": "message-from:state-hub", + "wait_until": null + } + ], + "qualifier_drift": [ + { + "task": "ACTIVITY-WP-0042-T08", + "field": "blocking_reason", + "file": "Provider prerequisite verified at lane KV4; scoped consumer bundle/hostPath and env refresh remain unperformed. Exact primary projection also blocked by existing historical identifier compatibility in repo-manager.", + "hub": "Provider repair verified: lane KV4 coordinates-only CAS, encrypted upload/readback and independent decryption. Remaining reviewed consumer source/env refresh and acceptance unperformed; exact primary projection blocked by existing repo-manager historical identifier compatibility." + }, + { + "task": "ADAPTIVE-WP-0010-T01", + "field": "blocked_on", + "file": "message-from:tenant-engine", + "hub": null + }, + { + "task": "FI-WP-0005-T04", + "field": "blocked_on", + "file": "message-from:activity-core", + "hub": null + }, + { + "task": "FI-WP-0005-T08", + "field": "blocked_on", + "file": "message-from:rein-aharness", + "hub": null + }, + { + "task": "INFO-WP-0029-T05", + "field": "blocked_on", + "file": "message-from:audit-core", + "hub": null + }, + { + "task": "KG-WP-0005-T03", + "field": "blocked_on", + "file": "message-from:qonto-assistant", + "hub": null + }, + { + "task": "NK-WP-0027-T06", + "field": "blocked_on", + "file": "message-from:info-tech-canon", + "hub": "message-from:ops-warden" + }, + { + "task": "NK-WP-0035-T04", + "field": "blocked_on", + "file": "message-from:approval-engine", + "hub": "message-from:info-tech-canon" + }, + { + "task": "RAPP-QONTO-WP-0002-T01", + "field": "wait_until", + "file": "2026-10-31", + "hub": null + }, + { + "task": "RAPP-TELEMETRY-WP-0001-T04", + "field": "blocked_on", + "file": "message-from:railiance-platform", + "hub": null + }, + { + "task": "REINAH-WP-0003-T04", + "field": "blocked_on", + "file": "message-from:activity-core", + "hub": null + }, + { + "task": "SAND-WP-0015-T04", + "field": "blocked_on", + "file": "message-from:railiance-platform", + "hub": null + } + ], + "parse_warnings": [ + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0001-projektgeruest.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0002-fachmodelle.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0003-basis-ui.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0005-lose-anforderungen.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0006-aufgaben-bieterfragen.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0007-dokumente.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0008-preise.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0009-abgabe-nachbetrachtung.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0010-partner-bibliothek.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0011-marktbegleiter.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0012-querschnitt.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0013-feedback-bugs.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0014-aufgaben-lose-scores.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0015-aufgaben-verknuepfungen-frist-issuefacade.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0016-issue-facade-integration.md", + "/home/worsch/vergabe-teilnahme/workplans/VT-WP-0017-whynot-design-tokens.md" + ], + "captured_at": "2026-10-03T21:08:51.190098+00:00", + "registered_repos": 146, + "unavailable_checkouts": [ + "access-engine", + "inter-hub", + "markitect-project", + "railiance-bootstrap", + "railiance-hosts", + "repo-manager" + ], + "handoffs": [ + { + "case_id": "case:cross-owner:CUST-WP-0075-T06:state-hub", + "scenario_type": "cross_owner_wait", + "beneficiary_workplan_id": "CUST-WP-0075", + "obligor_repo": "state-hub", + "obligor_workplan_id": null, + "state": "waiting", + "reason": "Owner acceptance or decline not yet recorded", + "proposal_message_id": "b4825539-1425-4028-a2e9-9243785a344b", + "proposal_read_at": null, + "reply_candidates": [] + }, + { + "case_id": "case:cross-owner:CUST-WP-0075-T06:railiance-platform", + "scenario_type": "cross_owner_wait", + "beneficiary_workplan_id": "CUST-WP-0075", + "obligor_repo": "railiance-platform", + "obligor_workplan_id": null, + "state": "waiting", + "reason": "Owner acceptance or decline not yet recorded", + "proposal_message_id": "f84637fa-2355-4971-a2dc-7f1fd01710a8", + "proposal_read_at": null, + "reply_candidates": [ + { + "id": "84cb5557-cc89-43fa-b8b8-13508c5ea52b", + "subject": "Re: D1 readiness: two lanes need a reviewed command before they can be approved", + "thread_id": "208c18ec-79fe-4b21-8cc8-928ecccfd4c2" + } + ] + }, + { + "case_id": "case:cross-owner:CUST-WP-0075-T06:ops-warden", + "scenario_type": "cross_owner_wait", + "beneficiary_workplan_id": "CUST-WP-0075", + "obligor_repo": "ops-warden", + "obligor_workplan_id": null, + "state": "waiting", + "reason": "Owner acceptance or decline not yet recorded", + "proposal_message_id": "79549660-4fc8-44d5-989e-074f6c7b6f07", + "proposal_read_at": null, + "reply_candidates": [ + { + "id": "94d07ae0-a2a4-47f8-8bf7-9957aaf2a84f", + "subject": "Re: BUG: warden secret-fetch policy check is sent to the SSH signing pin and always 403s for non-ops-warden owners", + "thread_id": "faf11061-b490-435a-b472-cb315a459760" + } + ] + } + ] +} diff --git a/docs/evidence/2026-10-03-dependency-graph-triage/scan_workplan_files.py b/docs/evidence/2026-10-03-dependency-graph-triage/scan_workplan_files.py index da5a8cbf..6bc4a0b6 100644 --- a/docs/evidence/2026-10-03-dependency-graph-triage/scan_workplan_files.py +++ b/docs/evidence/2026-10-03-dependency-graph-triage/scan_workplan_files.py @@ -1,15 +1,31 @@ import json,re,os,glob,collections,datetime,sys +import yaml import os; S=os.environ.get('TRIAGE_SCRATCH','.') repos=json.load(open(f'{S}/repos.json')); repos=repos if isinstance(repos,list) else repos['items'] FM=re.compile(r'^---\n(.*?)\n---',re.S); TB=re.compile(r'```task\n(.*?)```',re.S) +parse_warnings=[] def kv(block): - d={} - for line in block.splitlines(): - m=re.match(r'^([A-Za-z_]+):\s*(.*?)\s*(#.*)?$',line) - if m and not line.startswith(' ') : d[m.group(1)]=m.group(2).strip().strip('"') + try: + d=yaml.safe_load(block) + except yaml.YAMLError: + # Historic records mix prose into task fences. Preserve their fields, + # but expose the degraded parse rather than silently certify the scan. + parse_warnings.append(f) + d={} + for match in re.finditer(r'^([A-Za-z_]+):\s*([^\n]*)$',block,re.M): + key,value=match.groups() + if not value.strip(): + tail=block[match.end():] + items=re.match(r'\n((?:[ \t]+-[ \t]+[^\n]*\n?)+)',tail) + value='\n'+items.group(1) if items else '' + try: d[key]=yaml.safe_load(value) + except yaml.YAMLError: d[key]=value.strip().strip('"') + if not isinstance(d,dict): raise ValueError('work record must be a YAML mapping') return d def lst(v): if not v: return [] + if isinstance(v,list): return [str(x) for x in v] + v=str(v) v=v.strip() if v.startswith('['): return [x.strip().strip('"\'') for x in v[1:-1].split(',') if x.strip()] return [v] @@ -30,8 +46,8 @@ for r in repos: for tb in TB.findall(txt): d=kv(tb); tid=d.get('id') if not tid: continue - T[tid]={'wp':wid,'repo':r['slug'],'status':d.get('status'),'depends_on':lst(d.get('depends_on')),'needs_human':d.get('needs_human','').lower()=='true','blocked_on':d.get('blocked_on') or None,'wait_until':d.get('wait_until') or None,'decision_id':d.get('decision_id') or None,'reason':d.get('blocking_reason') or ''} -json.dump({'WP':WP,'T':T},open(f'{S}/files.json','w')) + T[tid]={'wp':wid,'repo':r['slug'],'status':d.get('status'),'depends_on':lst(d.get('depends_on')),'needs_human':str(d.get('needs_human','')).lower()=='true','blocked_on':d.get('blocked_on') or None,'wait_until':str(d['wait_until']) if d.get('wait_until') else None,'decision_id':d.get('decision_id') or None,'reason':d.get('blocking_reason') or '', 'hub_id':d.get('state_hub_task_id')} +json.dump({'WP':WP,'T':T,'parse_warnings':sorted(set(parse_warnings))},open(f'{S}/files.json','w')) TW={'finished','archived'}; TT={'done','cancel'} def term(x): if x in T: return T[x]['status'] in TT @@ -100,7 +116,7 @@ for n in nodes: if any(not term(b) for b in dep.get(n,[])): continue f=fanin(n) if f: rows.append((len(f),n,sorted(f))) -rows.sort(key=lambda r:-r[0]) +rows.sort(key=lambda r:(-r[0],r[1])) for n,k,f in rows[:45]: t=T.get(k); desc=(f"wait h={t['needs_human']} on={t['blocked_on']} dec={t['decision_id']} | {t['reason'][:70]}" if t and t['status']=='wait' else (f"{t['status']}" if t else f"WP {WP[k]['status']} {WP[k]['repo']}" if k in WP else 'UNKNOWN')) diff --git a/scripts/monitor_dependency_triage.py b/scripts/monitor_dependency_triage.py new file mode 100644 index 00000000..21121992 --- /dev/null +++ b/scripts/monitor_dependency_triage.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Read-only CUST-WP-0075 snapshot. Requires PyYAML for the file scanner. + +Writes evidence only to --output; never changes owner records or contacts owners. +""" +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +import os +from pathlib import Path +import subprocess +import sys +from urllib.parse import urlencode +from urllib.request import urlopen + +ROOT = Path(__file__).resolve().parents[1] +SCANNER = ROOT / "docs/evidence/2026-10-03-dependency-graph-triage/scan_workplan_files.py" +OPEN = {"proposed", "ready", "active", "blocked", "backlog"} +HANDOFFS = { + "state-hub": "b4825539-1425-4028-a2e9-9243785a344b", + "railiance-platform": "f84637fa-2355-4971-a2dc-7f1fd01710a8", + "ops-warden": "79549660-4fc8-44d5-989e-074f6c7b6f07", +} + + +def get_rows(base, route, **filters): + # Only /tasks has offset pagination. /messages uses a caller-selected + # limit; /repos returns the complete registry without paging parameters. + if route != "tasks": + query = urlencode(dict(filters, limit=5000)) if route == "messages" else urlencode(filters) + with urlopen(f"{base.rstrip('/')}/{route}/?{query}", timeout=30) as response: + rows = json.load(response) + if isinstance(rows, dict): + rows = rows["items"] + if route == "messages" and len(rows) >= 5000: + raise ValueError("message limit reached; narrow the owner filter before scanning") + return rows + rows = [] + while True: + query = urlencode(dict(filters, limit=200, offset=len(rows))) + with urlopen(f"{base.rstrip('/')}/{route}/?{query}", timeout=30) as response: + page = json.load(response) + if isinstance(page, dict): + page = page["items"] + if not isinstance(page, list): + raise ValueError(f"{route}: expected a list") + rows.extend(page) + if len(page) < 200: + return rows + + +def audit(records, hub_tasks): + plans, tasks = records["WP"], records["T"] + def terminal(token): + if token in tasks: + return tasks[token]["status"] in {"done", "cancel"} + return records["WP"].get(token, {}).get("status") in {"finished", "archived"} + plans = {k: v for k, v in plans.items() if v["status"] in OPEN and v["flavor"] != "residual"} + waits = {k: v for k, v in tasks.items() if v["wp"] in plans and v["status"] == "wait"} + by_id = {t["id"]: t for t in hub_tasks} + drift = [] + for tid, task in sorted(waits.items()): + hub = by_id.get(task["hub_id"]) + if hub is None: + drift.append({"task": tid, "field": "projection", "file": task["hub_id"], "hub": None}) + continue + fields = {"status": "status", "needs_human": "needs_human", "reason": "blocking_reason", + "decision_id": "decision_id", "blocked_on": "blocked_on", "wait_until": "wait_until"} + for file_key, hub_key in fields.items(): + expected = task[file_key] + actual = hub.get(hub_key) + if file_key == "needs_human": + actual = bool(actual) + elif file_key != "status": + expected, actual = expected or None, actual or None + if file_key == "wait_until": + expected = str(expected)[:10] if expected else None + actual = str(actual)[:10] if actual else None + if expected != actual: + drift.append({"task": tid, "field": hub_key, "file": expected, "hub": actual}) + return { + "open_non_residual_workplans": len(plans), + "workplan_statuses": dict(sorted(Counter(p["status"] for p in plans.values()).items())), + "wait_tasks": len(waits), + "human_gates": sum(t["needs_human"] for t in waits.values()), + "unrouted_human_gates": sorted(k for k, t in waits.items() if t["needs_human"] and not t["decision_id"]), + "unqualified_waits": sorted(k for k, t in waits.items() if not any( + t[field] for field in ("depends_on", "needs_human", "blocked_on", "wait_until"))), + "owner_return_waits": sum(str(t["blocked_on"] or "").startswith("message-from:") for t in waits.values()), + "terminal_dependency_waits": [ + {"task": k, "depends_on": t["depends_on"], "needs_human": t["needs_human"], + "blocked_on": t["blocked_on"], "wait_until": t["wait_until"]} + for k, t in sorted(waits.items()) if t["depends_on"] and all(terminal(x) for x in t["depends_on"])], + "qualifier_drift": drift, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--api-base", default="http://127.0.0.1:8000") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + repos = get_rows(args.api_base, "repos") + (args.output / "repos.json").write_text(json.dumps(repos)) + result = subprocess.run([sys.executable, str(SCANNER)], capture_output=True, text=True, + env=dict(os.environ, TRIAGE_SCRATCH=str(args.output.resolve())), check=True) + (args.output / "file-view.txt").write_text(result.stdout) + records = json.loads((args.output / "files.json").read_text()) + report = audit(records, get_rows(args.api_base, "tasks")) + report["parse_warnings"] = records.get("parse_warnings", []) + report["captured_at"] = datetime.now(timezone.utc).isoformat() + report["registered_repos"] = len(repos) + report["unavailable_checkouts"] = sorted(r["slug"] for r in repos + if not r.get("local_path") or not Path(r["local_path"]).is_dir()) + report["handoffs"] = [] + for owner, message_id in HANDOFFS.items(): + incoming = get_rows(args.api_base, "messages", to_agent="the-custodian", from_agent=owner) + outgoing = get_rows(args.api_base, "messages", to_agent=owner, from_agent="the-custodian") + original = next(m for m in outgoing if m["id"] == message_id) + candidates = [m for m in incoming if m["from_agent"] == owner + and m["created_at"] > original["created_at"]] + report["handoffs"].append({ + "case_id": f"case:cross-owner:CUST-WP-0075-T06:{owner}", + "scenario_type": "cross_owner_wait", + "beneficiary_workplan_id": "CUST-WP-0075", + "obligor_repo": owner, + "obligor_workplan_id": None, + "state": "waiting", "reason": "Owner acceptance or decline not yet recorded", + "proposal_message_id": message_id, "proposal_read_at": original.get("read_at"), + "reply_candidates": [{"id": m["id"], "subject": m["subject"], "thread_id": m.get("thread_id")} + for m in candidates], + }) + (args.output / "monitor.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tests/test_dependency_triage.py b/tests/test_dependency_triage.py new file mode 100644 index 00000000..b51f7253 --- /dev/null +++ b/tests/test_dependency_triage.py @@ -0,0 +1,98 @@ +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch, MagicMock + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("monitor", ROOT / "scripts/monitor_dependency_triage.py") +monitor = importlib.util.module_from_spec(spec) +spec.loader.exec_module(monitor) + + +class DependencyTriageTests(unittest.TestCase): + def test_yaml_task_lists_and_quoted_hash_are_preserved(self): + with tempfile.TemporaryDirectory() as directory: + scratch = Path(directory) + repo = scratch / "repo" + (repo / "workplans").mkdir(parents=True) + (scratch / "repos.json").write_text(json.dumps([{"slug": "example", "local_path": str(repo)}])) + (repo / "workplans/plan.md").write_text('''--- +id: EX-WP-0001 +type: workplan +status: blocked +flavor: implementation +--- +```task +id: EX-WP-0001-T01 +status: wait +depends_on: + - EX-WP-0002-T01 +needs_human: true +blocking_reason: "Review packet #1" +wait_until: 2026-10-04 +``` +''') + subprocess.run([sys.executable, str(monitor.SCANNER)], check=True, capture_output=True, + env=dict(os.environ, TRIAGE_SCRATCH=directory)) + task = json.loads((scratch / "files.json").read_text())["T"]["EX-WP-0001-T01"] + self.assertEqual(task["depends_on"], ["EX-WP-0002-T01"]) + self.assertEqual(task["reason"], "Review packet #1") + self.assertTrue(task["needs_human"]) + self.assertEqual(task["wait_until"], "2026-10-04") + + def records(self): + task = {"wp": "plan", "status": "wait", "hub_id": "uuid", "needs_human": True, + "reason": "Review", "decision_id": "decision", "blocked_on": None, + "wait_until": None, "depends_on": []} + return {"WP": {"plan": {"status": "blocked", "flavor": "implementation"}}, "T": {"task": task}} + + def test_missing_projection_is_visible(self): + result = monitor.audit(self.records(), []) + self.assertEqual(result["qualifier_drift"][0]["field"], "projection") + + def test_decision_sync_gap_is_detected(self): + result = monitor.audit(self.records(), [{"id": "uuid", "status": "wait", + "needs_human": True, "blocking_reason": "Review"}]) + self.assertEqual(result["qualifier_drift"], [{"task": "task", "field": "decision_id", + "file": "decision", "hub": None}]) + self.assertEqual(result["unrouted_human_gates"], []) + + def test_residuals_are_excluded_from_actionable_findings(self): + records = self.records() + records["WP"]["plan"]["flavor"] = "residual" + result = monitor.audit(records, []) + self.assertEqual(result["wait_tasks"], 0) + self.assertEqual(result["qualifier_drift"], []) + + def test_terminal_predecessor_keeps_separate_human_gate(self): + records = self.records() + records["T"]["task"]["depends_on"] = ["predecessor"] + records["T"]["predecessor"] = {"wp": "plan", "status": "done"} + result = monitor.audit(records, []) + self.assertTrue(result["terminal_dependency_waits"][0]["needs_human"]) + self.assertEqual(result["human_gates"], 1) + + def test_tasks_follow_offset_pagination(self): + response = MagicMock() + with patch.object(monitor, "urlopen", return_value=response) as fetch, patch.object( + monitor.json, "load", side_effect=[[{"id": str(i)} for i in range(200)], [{"id": "last"}]]): + rows = monitor.get_rows("http://hub", "tasks") + self.assertEqual(len(rows), 201) + self.assertIn("offset=200", fetch.call_args_list[1].args[0]) + + def test_messages_do_not_use_unsupported_offset(self): + with patch.object(monitor, "urlopen", return_value=MagicMock()) as fetch, patch.object( + monitor.json, "load", return_value=[{"id": str(i)} for i in range(200)]): + rows = monitor.get_rows("http://hub", "messages", from_agent="owner") + self.assertEqual(len(rows), 200) + self.assertEqual(fetch.call_count, 1) + self.assertNotIn("offset", fetch.call_args.args[0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md b/workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md index 8e874790..79b466a8 100644 --- a/workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md +++ b/workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md @@ -126,6 +126,16 @@ After every slice: commit, `statehub fix-consistency` for each touched repo, one progress event on this workplan, and the headline numbers appended below so progress is visible from the hub. +2026-10-03 Codex continuation: repeatable read-only monitor implemented in +`scripts/monitor_dependency_triage.py`, with seven regression checks covering +YAML lists/quoting, pagination, missing projections, decision drift, residual +exclusion and independent human gates. Run it with `--output `; +review `monitor.json` before acting. Current live findings are retained in +`docs/evidence/2026-10-03-dependency-graph-triage/monitor-2026-10-03.json`: +four unrouted human gates, sixteen unqualified waits and twelve qualifier +differences. This task remains in progress until those findings are reconciled; +the earlier T02/T03 results are point-in-time, not a current clean scan. + ## Hand the infrastructure proposals to their owners ```task @@ -147,6 +157,14 @@ qualifier sync gap, already reported to state-hub as message `70d08f35`). the proposal and the evidence; record their workplan ids here. Do not write their workplans for them. +2026-10-03 Codex continuation: all three original proposal messages remain +unread; no acceptance, owner workplan ID or decline found. Three local derived +`cross_owner_wait` observations are attached under `handoffs` in the monitor +snapshot, with stable case IDs and original message IDs. Obligor workplan IDs +remain unknown pending owner identification. Newer platform/warden replies +concern D1 readiness and the fetch-policy bug, respectively; neither answers +these proposals. No worker was woken. T06 remains `wait`. + ## Progress log - 2026-10-03 start: 108 open / 100 blocked / 232 wait / 69 human gates / @@ -193,4 +211,11 @@ write their workplans for them. and two unread owner replies: GH-WP-0004-T06 (informed-decision) and NK-WP-0042-T02 (user-engine). Follow-up on the CCR-2026-0031 lane: ops-warden catalog entry plus fetch-pin regeneration (checklist row 6). - +- 2026-10-03 Codex continuation: monitored 146 registered repos; six checkouts + unavailable. Visible file view: 107 open plans including residuals; actionable + non-residual view: 100 open / 94 blocked / 232 wait / 79 human gates, four + unrouted; 56 owner returns. Twelve qualifier differences and sixteen + unqualified waits are recorded as T05 follow-up. Four waits have terminal + predecessors, but separate human/owner gates and task prose still need review. + T06's three proposals remain unanswered. The monitor and seven regression + checks are implemented; no fleet cleanup or proposal acceptance is claimed.