diff --git a/agents/conductors/cortex/_cortex.py b/agents/conductors/cortex/_cortex.py index 541d4ea5..da4202e0 100644 --- a/agents/conductors/cortex/_cortex.py +++ b/agents/conductors/cortex/_cortex.py @@ -227,6 +227,12 @@ def _home_from_git(root: Path) -> str: # One section: (key, title, source path in the repo, blurb). SECTIONS = ( + ("by_project", "By project", "projects.yaml", + "Where every project is: the folders it lives in, every phase of it that " + "is still open, the locations its `## Where to look` names, and the " + "prompt for each thing that phase could become. This is the check-in " + "view — `pyauto-brain cortex checkin` and `census --by-project` print " + "the same tree."), ("awaiting", "Awaiting ruling", "phases/", "Results are in and nothing is running — the human's verdict is the only " "thing outstanding. Ordered failures first, then the phases a ruling is " @@ -269,6 +275,24 @@ def _int(value: str) -> int | None: return int(v) if v.isdigit() else None +def _where_bullets(mod, ph) -> list[str]: + """The phase's own `## Where to look` bullets, `- ` stripped, verbatim. + + A phase names where its results are. The by-project view renders this + text as the "which folder do I open" answer and `where_paths` resolves + the same bullets to paths — one reading, two uses. + """ + getter = getattr(mod, "_where_to_look", None) + if getter is not None: + bullets = getter(ph) + else: # a checkout whose script predates the helper + span = mod.sections(ph.text).get("Where to look") + bullets = ([ln for ln in ph.text.split("\n")[span[0]:span[1]] + if ln.startswith("- ") and ln.strip() != "-"] + if span else []) + return [b[2:].strip() for b in bullets if b[2:].strip()] + + def _phase_row(mod, ph) -> dict: refs, bad_refs = mod.gate_refs(ph.get("Gates")) runs = [{"ident": r.ident, "state": r.state, "partition": r.partition, @@ -286,6 +310,7 @@ def _phase_row(mod, ph) -> dict: "gates": refs, "bad_gates": bad_refs, "witness": ph.get("Witness"), + "where": _where_bullets(mod, ph), "budget": budget, "budget_minutes": _mins(budget), "runs": runs, @@ -375,6 +400,10 @@ def census(root: Path) -> dict: r["phase"] or 0, r["rel"])) gated = sorted([r for r in rows if r["state"] == "gated"], key=lambda r: (r["project"], r["phase"] or 0, r["rel"])) + # Not a board section — `planned` is the tail of the by-project tree, the + # "and then what" a check-in reads after everything open. + planned = sorted([r for r in rows if r["state"] == "planned"], + key=lambda r: (r["project"], r["phase"] or 0, r["rel"])) home = _home(root) return { @@ -387,6 +416,7 @@ def census(root: Path) -> dict: "live": live, "ready": ready, "gated": gated, + "planned": planned, "rulings": ruling_rows, "projects": projects, "epics": parse_epics(root / "epics.md"), @@ -518,21 +548,24 @@ def _live_payload(r: dict, projects: dict) -> str: f"({', '.join(x['ident'] for x in r['runs']) or 'no runs'}) stand.") +def _submit_line(project: str, projects: dict) -> str: + """The project's own submit verb, as a human would type it — or the note + that says the row has no such verb. Every path comes from the row.""" + row = projects.get(project, {}) + cli, path = (row.get("sync_cli") or ""), (row.get("local_path") or "") + if cli and path and "submit" in (row.get("sync_verbs") or []): + return f"cd {path} && {cli} submit