#!/usr/bin/env python3 """Apply task-block requalifications across repo workplans from a JSON plan. Deterministic replacement for agent sessions that re-read wait tasks after a decision resolves. Plan format (list of objects): {"id": "RPF-WP-0044-T03", "set": {"status": "todo"}, "drop": ["needs_human", "decision_id"], "note": "D2 resolved 2026-10-11"} `set` replaces or adds keys (values are written verbatim). `drop` removes keys. `blocking_reason` is dropped automatically when the new status is not `wait`. `note` is appended after the task block as a dated line. Use --dry-run first. """ import argparse, glob, json, os, re, sys BLOCK = re.compile(r"```task\n(.*?)```", re.S) def edit_block(body, change): lines = body.rstrip("\n").split("\n") drop = set(change.get("drop", [])) new_status = change.get("set", {}).get("status") if new_status and new_status != "wait": drop |= {"blocking_reason", "needs_human", "blocked_on"} out, seen = [], set() for ln in lines: key = ln.split(":", 1)[0].strip() if key in drop: continue if key in change.get("set", {}): ln = f"{key}: {change['set'][key]}" seen.add(key) out.append(ln) # insert new keys before state_hub_task_id so the writeback field stays last extra = [f"{k}: {v}" for k, v in change.get("set", {}).items() if k not in seen] idx = next((i for i, l in enumerate(out) if l.startswith("state_hub_task_id")), len(out)) out[idx:idx] = extra return "\n".join(out) + "\n" def main(): ap = argparse.ArgumentParser() ap.add_argument("plan") ap.add_argument("--root", default=os.path.expanduser("~")) ap.add_argument("--dry-run", action="store_true") a = ap.parse_args() plan = {c["id"]: c for c in json.load(open(a.plan))} touched, found = set(), set() for f in glob.glob(os.path.join(a.root, "*/workplans/*.md")): text = open(f, encoding="utf-8").read() def repl(m): body = m.group(1) mid = re.search(r"^id:\s*(\S+)", body, re.M) if not mid or mid.group(1) not in plan: return m.group(0) c = plan[mid.group(1)] found.add(c["id"]) new = "```task\n" + edit_block(body, c) + "```" if c.get("note"): new += f"\n\n{c['note']}" return new new_text = BLOCK.sub(repl, text) if new_text != text: touched.add(f) if not a.dry_run: open(f, "w", encoding="utf-8").write(new_text) for f in sorted(touched): print(("would edit " if a.dry_run else "edited ") + f) missing = set(plan) - found if missing: print("NOT FOUND:", ", ".join(sorted(missing)), file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())