$ cat node-template.py
P
Plan aggregate
// Collect the tasks every Plan review produced into one ordered, de-duplicated list. Several reviews wire into its one Tasks input; it drops the duplicates four lenses inevitably raise, sorts by priority then by lens, and emits both the JSON a Task node is built from and the markdown checklist a design doc carries.
Process
Data
#plan#tasks#aggregate#vibe
template.py
1"""Plan aggregate — every review's tasks, de-duplicated and ordered.23Replicates gstack's `tasks-aggregator`: four lenses read the same plan, so four4lenses raise overlapping work, and the useful artefact is the union with the5overlap collapsed — not four separate lists to reconcile by hand.67The de-dup key is gstack's: `(component, sorted(files), title)`, exact match8only. Fuzzy matching was deliberately not copied. Two lenses phrase the same9task differently often enough that near-matching would silently drop one of10them, and a duplicate the human deletes costs a second — a dropped P1 costs11whatever it was guarding.1213Ordering is priority first, then lens. Priority is what a reader acts on;14`ceo, design, eng, devex` breaks the ties in the order the lenses conventionally15run, so the list is stable across re-runs rather than reflecting dict order.16"""1718import json19import sys2021PRIORITY_RANK = {"P1": 0, "P2": 1, "P3": 2}22LENS_RANK = {"ceo": 0, "design": 1, "eng": 2, "devex": 3}2324#: Where an unrecognised value sorts. Last, never first — a task with a25#: priority nobody recognises must not outrank a real P1 by accident.26UNRANKED = 99272829def flatten(raw):30 """One list, whatever arity the board delivered.3132 A single wired edge arrives as a bare list; several arrive as a list of33 lists. Both shapes must work, or adding a second reviewer to a working34 board breaks it.35 """36 if not isinstance(raw, list):37 return []38 out = []39 for item in raw:40 if isinstance(item, list):41 out.extend(x for x in item if isinstance(x, dict))42 elif isinstance(item, dict):43 out.append(item)44 return out454647def dedup_key(task):48 files = task.get("files")49 files = sorted(str(f) for f in files) if isinstance(files, list) else []50 return (51 str(task.get("component", "")).strip().lower(),52 tuple(files),53 str(task.get("title", "")).strip().lower(),54 )555657def sort_key(task):58 return (59 PRIORITY_RANK.get(str(task.get("priority", "")).upper(), UNRANKED),60 LENS_RANK.get(str(task.get("lens", "")).lower(), UNRANKED),61 str(task.get("title", "")),62 )636465def aggregate(tasks):66 """Collapse duplicates, keeping the most urgent copy of each."""67 groups = {}68 for task in tasks:69 key = dedup_key(task)70 current = groups.get(key)71 if current is None or sort_key(task) < sort_key(current):72 # The surviving copy is the highest-priority one, so a task raised73 # as P3 by design and P1 by eng lands as the P1. Recording the74 # lenses that agreed is the point of the merge — a task three75 # lenses independently asked for is different evidence from one.76 merged = dict(task)77 merged["lenses"] = sorted(78 {str(t.get("lens", "")) for t in tasks if dedup_key(t) == key} - {""}79 )80 groups[key] = merged81 return sorted(groups.values(), key=sort_key)828384def to_markdown(tasks):85 """The checklist format this repo's design docs already use."""86 if not tasks:87 return "_No tasks were raised by any review._"88 lines = []89 for task in tasks:90 lenses = task.get("lenses") or ([task["lens"]] if task.get("lens") else [])91 raised = ", ".join(lenses) if lenses else "unknown"92 lines.append(93 f"- [ ] **{task.get('id', '?')} "94 f"({task.get('priority', 'P?')}, "95 f"human: {task.get('effort_human', '?')} / "96 f"CC: {task.get('effort_cc', '?')}) — "97 f"{task.get('component', 'unknown')}** — {task.get('title', '')}\n"98 f" - Raised by: {raised} — {task.get('source_finding', 'no finding recorded')}"99 )100 files = task.get("files")101 if isinstance(files, list) and files:102 lines.append(f" - Files: {', '.join(str(f) for f in files)}")103 return "\n".join(lines)104105106def main():107 try:108 envelope = json.loads(sys.stdin.read() or "{}")109 inputs = envelope.get("inputs", {}) if isinstance(envelope, dict) else {}110111 tasks = flatten(inputs.get("tasks"))112 findings = flatten(inputs.get("findings"))113 blocking = sum(1 for f in findings if f.get("severity") == "blocking")114115 merged = aggregate(tasks)116117 print(118 f"[plan-aggregate] in={len(tasks)} out={len(merged)} "119 f"dropped={len(tasks) - len(merged)} blocking={blocking}",120 file=sys.stderr,121 )122123 json.dump({124 "tasks": merged,125 "markdown": to_markdown(merged),126 "count": len(merged),127 "blocking": blocking,128 }, sys.stdout)129 except Exception as e:130 print(json.dumps({"error": str(e), "errorType": type(e).__name__}), file=sys.stderr)131 sys.exit(1)132133134if __name__ == "__main__":135 main()136$ git log --oneline
v1.0.0
HEAD
2026-10-14First marketplace publish: vibe-coding lanes and the gstack plan flow (PR #1313).