From 8f94fe1a3dcfb41a4c9bc1a0e4e36ce8be85bc01 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 3 Sep 2026 20:42:25 -0400 Subject: [PATCH] feat(cortex): the by-project check-in view and the two missing prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original request asked for one thing the board could not answer: "where is every project, what folders should I check out, and give me the prompt to continue any one of them". Every section was keyed by phase STATE, the Projects table omitted the three folders a project lives in, and each phase's `## Where to look` bullets — the folder answer itself — were parsed to locate artefacts and never rendered. `## By project` is that view, and it is one tree rendered three ways: the first section of `dashboard.md` / `dashboard.html`, the text `cortex census --by-project` prints from the checkout alone, and the closing summary of a real `checkin` (the only one carrying pull results and six-leg health). Per project: its `local_path` / `mirror` / `ral_root`, its phase counts, then every phase still open — awaiting, live, ready, gated, planned — with its state, its `## Where to look` bullets verbatim, and a copy chip per prompt its state carries. History (accepted / rerun / dropped) is counted, not listed; a project with nothing open folds to one line. Two of the five starting prompts were missing and are now here, beside the rule prompt rather than instead of it: - `_next_phase_payload` — "the results are good": the accept, then the next phase. It prefills from the tree — a planned or gated sibling numbered N+1 is opened rather than duplicated; a taken number is named and the lowest free one offered — and spells out `new`'s own title rule, that it writes `# — phase N: ` in front of `--title` itself. - `_rerun_payload` — "run it again": the rerun ruling first (the only verb that puts a phase back on the board), then ready, the project's own submit and the move that records the job id. `phase_chips()` is the one table saying which prompts a state carries, so a prompt cannot appear on the board and not in the door. The counts strip is deliberately unchanged — `board/_board.py` reads it by regex. Refs PyAutoCortex#12. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013HsZA1ufn3msgPiDFxEXa6 --- agents/conductors/cortex/_cortex.py | 473 +++++++++++++++++++++++----- tests/test_cortex_conductor.py | 189 +++++++++++ 2 files changed, 578 insertions(+), 84 deletions(-) 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