From 64b18b9c1c250b6b15997aebf336c4d46787b0ee Mon Sep 17 00:00:00 2001
From: Jammy2211
Date: Fri, 4 Sep 2026 13:34:46 -0400
Subject: [PATCH 1/2] feat(cortex): board leads with the check-in prompt, a
freshness stamp and projects; epics discarded
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Cortex board opened with every phase of every active project, so a
reader scrolled past the whole Euclid plan before reaching what needed a
verdict, and nothing said when the science state was last refreshed.
- A π "check in since last time" chip is the first task row: the paste
for the laptop science chat, naming the last check-in.
- "Last check-in" is read from PyAutoCortex/checkin.yaml, which
`cortex checkin` / `collect --apply` now write before rendering so
push_ledger carries it; the HTML twin turns it red on view when the
viewer's clock is more than 60 min past it. Age is computed on view,
never at render, and the file is stable between renders so --check
needs no new normaliser rule.
- Summary table: one row per active project (next phase, awaiting /
running / ready, last ruling).
- Projects replaces both the leading By-project wall and the trailing
Projects table: active β planned β dormant, Local / Mirror / RAL on
their own bold lines, the next phase with its chip, the remaining open
phases folded behind links to the phase files, phases// and
the project's issues. No where-to-look bullets on the page; the
printout and census --by-project keep the full tree.
- Awaiting ruling is the first state section; Running / submitted drops
the "run it again" chip; Ready shows one phase per project and folds
the rest; Gated and Recent rulings unchanged.
- Epics: section, parse_epics and the epics.md dependency removed. The
optional `Epic:` phase header stays as a join key to a Mind epic.
Closes #354
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01FDMikgtrzGV799S5WXPpFq
---
agents/conductors/cortex/AGENTS.md | 36 +-
agents/conductors/cortex/_cortex.py | 548 ++++++++++++++++++----------
docs/organs/cortex.md | 9 +-
skills/cortex/cortex.md | 6 +-
tests/test_cortex_conductor.py | 316 ++++++++++++++--
5 files changed, 675 insertions(+), 240 deletions(-)
diff --git a/agents/conductors/cortex/AGENTS.md b/agents/conductors/cortex/AGENTS.md
index 873fa453..0209a773 100644
--- a/agents/conductors/cortex/AGENTS.md
+++ b/agents/conductors/cortex/AGENTS.md
@@ -20,7 +20,7 @@ the Cortex's intake-and-dashboard conductor.
| Verb | Question | Emits |
|------|----------|-------|
| `checkin [--dry-run \| --apply] [--push \| --no-push] [--project KEY] [--skip-pull] [--refreshed ISO]` | **Where is my science?** | the door β the sequence below, ending in a summary keyed by project; exit **1** when a pull failed or the tree needs a look |
-| `census` | What is the Cortex holding? | phase counts by state, the board's section counts, rulings, projects, epics (`--json` for the lot) |
+| `census` | What is the Cortex holding? | phase counts by state, the board's section counts, rulings, projects, the last check-in stamp (`--json` for the lot) |
| `dashboard --check` | Are the committed pages current? | exit 0 current Β· **1 stale** Β· 2 no checkout Β· 3 unreadable tree |
| `dashboard --apply` | β | writes `dashboard.md` + `dashboard.html` |
| `gates` | What is each gated phase waiting on? | every gated phase, its refs and the URL each resolves to β read-only and offline |
@@ -60,18 +60,36 @@ Mind's: a session holding one organ and not the other still works.
## The board
-Sections, in the reading order of a check-in: **Awaiting ruling** (pulled and
-awaiting-ruling phases, ordered failures β a ruling is required β clean) β
-**Running / submitted** (job ids, wall against the phase's budget) β
-**Ready** β **Gated** (the open refs) β **Recent
-rulings** β **Epics** (each card links its Mind half) β **Projects** (the
-where-to-look table straight from `projects.yaml`). A counts table near the
-top is what `board/_board.py` reads for the Brain board's Cortex strip.
+The page opens on the door, not on the work: a counts table (what
+`board/_board.py` reads for the Brain board's Cortex strip), then the
+**check-in chip** β the paste for the laptop's science chat β and the **last
+check-in** stamp under it. Then **Summary** (one row per active project),
+then the sections in the reading order of a check-in: **Projects** (active β
+planned β dormant; each card is the folders, the counts and the ONE phase to
+act on next, with the rest of its open phases and its issue list behind a
+fold) β **Awaiting ruling** (ordered failures β a ruling is required β clean)
+β **Running / submitted** β **Ready** (one visible row per project, the
+queue behind it folded) β **Gated** β **Recent rulings**.
+
+No `## Where to look` bullets ride on the page β they stay in the phase file,
+in `census --by-project` and in the check-in printout, which keep the full
+per-phase tree. The Cortex has no epics of its own (retired 2026-09-04): a
+science project *is* the long programme, and `Epic:` on a phase header is
+only an optional join key to a **Mind** epic.
+
+**`checkin.yaml`** is the one-key file (`refreshed: `) that
+`cmd_checkin` and `collect --apply` write from the stamp they scored against,
+before rendering. `census()` reads it into `c["checkin"]`; missing means
+"never checked in", unparseable is a `problems` line. The stamp means *last
+check-in*, never last render, so a doc-only push cannot fake freshness, and
+the HTML twin computes its age on the **viewer's** clock (a static page
+cannot know when it is read) and reddens it past sixty minutes.
`--check` compares the pages with the generation comment **and** the visible
`Last updated` banner stripped, so a re-render on a new date is not drift β
the Mind's normaliser strips only the comment, which is why its refresh
-workflow self-heals with an empty commit most nights.
+workflow self-heals with an empty commit most nights. `checkin.yaml` needs no
+rule of its own: it is stable between renders.
## Gates
diff --git a/agents/conductors/cortex/_cortex.py b/agents/conductors/cortex/_cortex.py
index da4202e0..c2d320cd 100644
--- a/agents/conductors/cortex/_cortex.py
+++ b/agents/conductors/cortex/_cortex.py
@@ -227,12 +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."),
+ ("projects", "Projects", "projects.yaml",
+ "The science body map, active first: where each project lives, what it "
+ "is holding, and the one phase of it to act on next. The plans and the "
+ "issues are one click away β this page routes to them rather than "
+ "reprinting them; `census --by-project` and the check-in printout keep "
+ "the full per-phase 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 "
@@ -249,20 +249,50 @@ def _home_from_git(root: Path) -> str:
("rulings", "Recent rulings", "rulings/",
"The ledger of record, newest first. A verdict recorded only outside the "
"Cortex does not exist."),
- ("epics", "Epics", "epics.md",
- "Long-running programmes whose science half lives here; each card links "
- "its development half in the Mind."),
- ("projects", "Projects", "projects.yaml",
- "The science body map β where each project lives, what syncs it and "
- "where its witness lands."),
)
REFRESH_BLURB = (
- "generated from `phases/`, `rulings/`, `epics.md` and `projects.yaml`, so "
- "it is only as current as they are. `dashboard_refresh.yml` re-renders it "
- "on every push to `main`."
+ "generated from `phases/`, `rulings/` and `projects.yaml`, so it is only "
+ "as current as they are. `dashboard_refresh.yml` re-renders it on every "
+ "push to `main`."
)
+#: The one-key file the check-in door writes and this page reads back. It
+#: means *last check-in*, not last render: a doc-only push re-renders the
+#: board and must not be able to fake freshness.
+CHECKIN_FILE = "checkin.yaml"
+CHECKIN_STAMP = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?Z$")
+CHECKIN_LABEL = "check in since last time"
+CHECKIN_NEVER = "never checked in"
+
+
+def read_checkin(root: Path) -> tuple[str, list[str]]:
+ """`(stamp, problems)` from `/checkin.yaml`.
+
+ Missing is not a problem: a Cortex nobody has checked in yet is a state
+ the page can say out loud. A file that IS there but carries no readable
+ `refreshed:` is β something wrote it, and a stamp that cannot be parsed
+ cannot be trusted to mean anything.
+ """
+ path = root / CHECKIN_FILE
+ if not path.is_file():
+ return "", []
+ m = re.search(r"^refreshed:\s*(\S+)\s*$", _read(path), re.M)
+ value = (m.group(1) if m else "").strip().strip('"')
+ if not CHECKIN_STAMP.match(value):
+ return "", [f"{CHECKIN_FILE}: no readable `refreshed: ` β "
+ "read as never checked in"]
+ return value, []
+
+
+def write_checkin(root: Path, stamp: str) -> Path:
+ """Persist the refresh stamp, before the pages are rendered: the board is
+ meant to show the check-in that produced it, and `push_ledger` picks the
+ file up in the same commit as the phase moves."""
+ path = root / CHECKIN_FILE
+ path.write_text(f"refreshed: {stamp}\n", encoding="utf-8")
+ return path
+
def _mins(value: str) -> int | None:
"""`H+:MM` β minutes, or None."""
@@ -337,44 +367,13 @@ def _ruling_row(mod, r) -> dict:
}
-EPIC_HEAD = re.compile(r"^## (\S+)\s*$")
-EPIC_FIELD = re.compile(r"^- ([a-z-]+):\s*(.*?)\s*$")
-
-
-def parse_epics(path: Path) -> list[dict]:
- """`epics.md` entries β `## ` then `- key: value` lines. The Cortex
- half additionally carries `- mind-half:`, the slug of the Mind's entry."""
- if not path.is_file():
- return []
- entries: list[dict] = []
- cur: dict | None = None
- fenced = False
- for line in path.read_text(encoding="utf-8").splitlines():
- # `epics.md` documents its own schema in a fenced block whose body is
- # a `## ` template β reading that as an entry would put a ghost
- # card on the board of an empty Cortex.
- if line.startswith("```"):
- fenced = not fenced
- continue
- if fenced:
- continue
- head = EPIC_HEAD.match(line)
- if head:
- cur = {"slug": head.group(1)}
- entries.append(cur)
- continue
- field = EPIC_FIELD.match(line)
- if field and cur is not None:
- cur[field.group(1)] = field.group(2)
- return entries
-
-
def census(root: Path) -> dict:
"""Everything the board, the plan and the counts need, in one read."""
mod = load_cortex(root)
projects, project_problems = mod.load_projects(root)
phases, phase_problems = mod.load_phases(root)
rulings, ruling_problems = mod.load_rulings(root)
+ checkin, checkin_problems = read_checkin(root)
rows = [_phase_row(mod, ph) for ph in phases]
by_state: dict[str, int] = {}
@@ -419,8 +418,9 @@ def census(root: Path) -> dict:
"planned": planned,
"rulings": ruling_rows,
"projects": projects,
- "epics": parse_epics(root / "epics.md"),
- "problems": project_problems + phase_problems + ruling_problems,
+ "checkin": checkin,
+ "problems": (project_problems + phase_problems + ruling_problems
+ + checkin_problems),
}
@@ -531,6 +531,18 @@ def _src_url(blob: str, src: str) -> str:
# ------------------------------------------------------------- the payloads ---
# Every row hands the reader the next command rather than a decision. The
# science verbs stay the Cortex script's and the run stays the human's.
+def _checkin_payload(c: dict) -> str:
+ """The paste for the laptop's science chat β the whole door as one
+ sentence, naming the stamp it is refreshing *from* so "since last time"
+ needs no second explanation."""
+ since = c.get("checkin") or "never"
+ return (f"/cortex β check in on every active science project since the "
+ f"last check-in ({since}): pull each project through its sync "
+ "CLI, score every live run against its pre-registered witness, "
+ "move what came back, re-render and push the board, then read me "
+ "the by-project summary with the prompt each phase needs next.")
+
+
def _ruling_payload(r: dict) -> str:
return (f"Review the PyAutoCortex phase {r['rel']} and help me rule on it: "
f"read its `## Witness` and the pulled evidence under its "
@@ -585,9 +597,9 @@ def _gate_payload(r: dict) -> str:
def _next_phase(r: dict, c: dict) -> dict | None:
"""The phase numbered N+1 of this project, when the tree already holds it.
- A planned sibling IS the ledger naming the next phase β `epics.md` points
- at `phases//` for exactly this reason β so the prompt opens the
- phase that exists rather than writing a second one beside it.
+ A planned sibling IS the ledger naming the next phase β `phases//`
+ is the programme, there is no separate list of it β so the prompt opens
+ the phase that exists rather than writing a second one beside it.
"""
if r["phase"] is None:
return None
@@ -684,7 +696,10 @@ def phase_chips(r: dict, c: dict) -> list[tuple[str, str]]:
chips.append(("its gates", _gate_payload(r)))
if state == "planned":
chips.append(("open it", _planned_payload(r)))
- if state in AWAITING_STATES or state == "running":
+ # Only where a rerun would BE a verdict. On a running phase the job is
+ # still out there and "run it again" is a second submission, not a
+ # ruling β it was noise on the row and a row taller than it needed.
+ if state in AWAITING_STATES:
chips.append(("run it again", _rerun_payload(r, c)))
return chips
@@ -713,6 +728,89 @@ def project_groups(key: str, c: dict) -> list[tuple[str, list[dict]]]:
return out
+#: `status` in the order the reader wants it: what is running, then what is
+#: coming, then what is asleep. An unrecognised status sorts last.
+STATUS_RANK = {"active": 0, "planned": 1, "dormant": 2}
+
+#: The buckets a project's open phases live in, in the order "what is the
+#: next thing" is answered: a verdict owed beats a job out there beats a
+#: launch beats a gate beats a plan.
+OPEN_ORDER = ("awaiting", "live", "ready", "gated", "planned")
+
+
+def project_order(keys: list[str], c: dict) -> list[str]:
+ """Active, then planned, then dormant; alphabetical inside each rank."""
+ return sorted(keys, key=lambda k: (
+ STATUS_RANK.get((c["projects"][k].get("status") or "").strip(), 3), k))
+
+
+def open_phases(key: str, c: dict) -> list[dict]:
+ """Every open phase of one project, in `OPEN_ORDER`, by phase number."""
+ out = []
+ for bucket in OPEN_ORDER:
+ out += sorted((r for r in c[bucket] if r["project"] == key),
+ key=lambda r: (10 ** 6 if r["phase"] is None
+ else r["phase"], r["rel"]))
+ return out
+
+
+def next_open_phase(key: str, c: dict) -> dict | None:
+ """The one phase a project's card shows β the route from Projects to the
+ follow-up work. First non-empty bucket, lowest phase number in it."""
+ phases = open_phases(key, c)
+ return phases[0] if phases else None
+
+
+def ready_groups(c: dict) -> list[tuple[dict, list[dict]]]:
+ """`(the visible row, the ones behind it)` per project, in the section's
+ own order. One project, one launch: four ready phases of one project are
+ a queue, and only the front of it can be submitted today."""
+ rows: dict[str, list[dict]] = {}
+ for r in c["ready"]:
+ rows.setdefault(r["project"], []).append(r)
+ out = []
+ for key in dict.fromkeys(r["project"] for r in c["ready"]):
+ queue = sorted(rows[key], key=lambda r: (10 ** 6 if r["phase"] is None
+ else r["phase"], r["rel"]))
+ out.append((queue[0], queue[1:]))
+ return out
+
+
+def _last_ruling(key: str, c: dict) -> str:
+ """The date of this project's newest ruling, `-` when it has none."""
+ dates = []
+ for r in c["rulings"]:
+ if r["project"] != key:
+ continue
+ stamp = (r["reviewed_at"] or "")[:10]
+ if not stamp:
+ m = re.match(r"R-(\d{4})(\d{2})(\d{2})-", r["id"] or "")
+ stamp = "-".join(m.groups()) if m else ""
+ if stamp:
+ dates.append(stamp)
+ return max(dates) if dates else "-"
+
+
+def summary_rows(c: dict) -> list[dict]:
+ """One row per `status: active` project β the clean summary the page
+ opens on, before any of it has to be read phase by phase."""
+ out = []
+ for key in sorted(k for k, row in c["projects"].items()
+ if (row.get("status") or "").strip() == "active"):
+ nxt = next_open_phase(key, c)
+ out.append({
+ "project": key,
+ "next": "-" if nxt is None else
+ (f"{nxt['phase']} {nxt['state']}" if nxt["phase"] is not None
+ else nxt["state"]),
+ "awaiting": sum(1 for r in c["awaiting"] if r["project"] == key),
+ "live": sum(1 for r in c["live"] if r["project"] == key),
+ "ready": sum(1 for r in c["ready"] if r["project"] == key),
+ "ruling": _last_ruling(key, c),
+ })
+ return out
+
+
def by_project_keys(c: dict) -> tuple[list[str], list[str]]:
"""`(shown, folded)` β a block each for every `status: active` project
plus any project still holding an open phase; everything else folds to
@@ -725,15 +823,33 @@ def by_project_keys(c: dict) -> tuple[list[str], list[str]]:
return sorted(shown), sorted(set(c["projects"]) - shown)
-def project_paths(row: dict) -> list[str]:
- """The three folders a project lives in β the answer to "where is it"."""
- where = [f"local `{row.get('local_path') or '?'}`"]
+def project_paths(row: dict) -> list[tuple[str, str]]:
+ """`(label, path)` for the folders a project lives in. One per line on the
+ board with the label in bold: "where is it" is read, not scanned, and the
+ three run together on one line was the complaint that started this."""
+ where = [("Local", row.get("local_path") or "?")]
if (row.get("mirror") or "none") != "none":
- where.append(f"mirror `{row['mirror']}`")
- where.append(f"RAL `{row.get('ral_root') or '?'}`")
+ where.append(("Mirror", row["mirror"]))
+ where.append(("RAL", row.get("ral_root") or "?"))
return where
+def project_paths_line(row: dict) -> str:
+ """The same folders on one line β what the check-in printout and
+ `census --by-project` read, where a paragraph each would be noise."""
+ return " Β· ".join(f"{label if label == 'RAL' else label.lower()} `{path}`"
+ for label, path in project_paths(row))
+
+
+def _issues_url(row: dict) -> str:
+ """The project repo's issue list, from the row's own `remote:`. Empty for
+ a project with no remote β most dormant rows, by design."""
+ remote = (row.get("remote") or "").strip()
+ if not remote or remote == "none" or "/" not in remote:
+ return ""
+ return f"https://github.com/{remote}/issues"
+
+
def project_counts(key: str, c: dict) -> str:
counts: dict[str, int] = {}
for r in c["phases"]:
@@ -743,15 +859,23 @@ def project_counts(key: str, c: dict) -> str:
or "none"
-def _project_payload(key: str, row: dict) -> str:
- path = row.get("mirror") if row.get("mirror") not in ("", "none") else row.get("local_path", "")
- return f"Show me what is under {path} for the science project {key}."
+def _project_facts(key: str, c: dict) -> str:
+ """The one line of facts under a project's folders."""
+ row = c["projects"][key]
+ return (f"{(row.get('status') or '?').strip()} Β· "
+ f"{(row.get('partition') or '?').strip()} partition Β· phases: "
+ f"{project_counts(key, c)}")
+
+def _fold_label(rest: list[dict]) -> str:
+ return f"{len(rest)} more open phase(s) Β· plans and issues"
-def _epic_payload(e: dict) -> str:
- return (f"Work out where the Cortex half of the epic {e['slug']} stands "
- f"from {e.get('ledger') or 'phases/'} and tell me what its next "
- "phase is.")
+
+def _phase_dir(key: str, blob: str) -> str:
+ """`phases//` on GitHub β where that project's plans actually
+ are. Bare backticks when the owner could not be derived."""
+ tree = blob.replace("/blob/main/", "/tree/main/")
+ return f"{tree}phases/{key}/" if blob else ""
# --------------------------------------------------------------- markdown ---
@@ -787,7 +911,9 @@ def _project_head(r: dict) -> str:
"""A by-project phase line: the phase head, its state, and the warning a
failed run earns. State is on the line because this section is not keyed
by it."""
- head = _phase_head(r) + f" β **{r['state']}**"
+ # ``, not `**`: this head is rendered inside a ``, which is
+ # HTML on both twins and where markdown emphasis would show as asterisks.
+ head = _phase_head(r) + f" β {r['state']}"
if r["failed_runs"]:
head += " β β οΈ " + _summary_label(
"failed runs: " + ", ".join(r["failed_runs"]))
@@ -861,31 +987,65 @@ def h2(key: str) -> list:
L += [f"> - `{_cell(p)}`" for p in c["problems"][:10]]
L += [""]
+ # The first row on the page, before any section: the paste that refreshes
+ # everything below it, and the stamp saying when that last happened.
+ L += [_task_row(CHECKIN_LABEL, _checkin_payload(c)), "",
+ f"**Last check-in:** {c.get('checkin') or CHECKIN_NEVER}", ""]
+
+ L += ["## Summary", "",
+ "One row per active project β the next thing, what it is holding, "
+ "and when it last ruled.", ""]
+ rows = summary_rows(c)
+ if rows:
+ L += ["| Project | Next phase | Awaiting | Running | Ready | "
+ "Last ruling |", "|---|---|---:|---:|---:|---|"]
+ L += [f"| {_cell(r['project'])} | {_cell(r['next'])} | "
+ f"{r['awaiting']} | {r['live']} | {r['ready']} | "
+ f"{_cell(r['ruling'])} |" for r in rows]
+ L += [""]
+ else:
+ L += ["- _(no active project)_", ""]
- # First section, above the state lists: on a phone the state lists are
- # cross-project and every row has to be read to find out whose it is,
- # while this one answers "where is everything" a screen per project and
- # opens with the folders. The counts table sits above it, so the urgency
- # sections stay one tap away.
- L += h2("by_project")
+
+ # The map, first: active projects, then planned, then dormant. Each card
+ # is where the project lives, what it holds and the ONE phase to act on
+ # next β the plans and the issues ride behind a fold, one click away.
+ L += h2("projects")
shown, folded = by_project_keys(c)
- for key in shown:
- L += [f"### {_summary_label(key)}", "",
- "- " + " Β· ".join(project_paths(c["projects"][key])),
- f"- phases: {project_counts(key, c)}", ""]
- groups = project_groups(key, c)
- if not groups:
+ for key in project_order(shown, c):
+ row = c["projects"][key]
+ L += [f"### {_summary_label(key)}", ""]
+ for label, path in project_paths(row):
+ L += [f"**{label}** `{_cell(path)}`", ""]
+ L += [_project_facts(key, c), ""]
+ rest = open_phases(key, c)
+ nxt = next_open_phase(key, c)
+ if nxt is None:
L += ["_Nothing open β every phase of this project is history._",
""]
- for title, rows in groups:
- L += [f"**{title}**", ""]
- for r in rows:
- L += _project_phase_md(r, c)
+ continue
+ rest = rest[1:]
+ chips = phase_chips(nxt, c)
+ L += _items([_task_row(_project_head(nxt), chips[0][1])]
+ + [_task_row("β³ " + label, payload)
+ for label, payload in chips[1:]]) or \
+ [f"- {_project_head(nxt)}"]
+ L += [""]
+ if rest:
+ L += [f"{_fold_label(rest)}", ""]
+ L += [f"- [{_summary_label(r['title'])}]({r['rel']}) β "
+ f"{r['state']}" for r in rest]
+ links = [f"[phases/{key}/]({_phase_dir(key, blob)})" if blob
+ else f"`phases/{key}/`"]
+ if _issues_url(row):
+ links.append(f"[issues]({_issues_url(row)})")
+ L += ["", " Β· ".join(links), "", "", ""]
if folded:
- L += [f"{len(folded)} dormant project(s) with "
- "nothing open", "",
- " Β· ".join(f"`{_cell(k)}` ({project_counts(k, c)})"
- for k in folded),
+ L += [f"{len(folded)} project(s) with nothing "
+ "open", "",
+ " Β· ".join(
+ f"`{_cell(k)}` ({_cell((c['projects'][k].get('status') or '?').strip())}"
+ f" Β· {project_counts(k, c)})" for k in project_order(folded, c)),
"", "", ""]
L += h2("awaiting")
@@ -916,8 +1076,18 @@ def h2(key: str) -> list:
L += [""]
L += h2("ready")
- L += _items([_task_row(_phase_head(r), _ready_payload(r, c["projects"]))
- for r in c["ready"]]) or ["- _(nothing ready to launch)_"]
+ ready_rows = []
+ for first, rest in ready_groups(c):
+ ready_rows.append(_task_row(_phase_head(first),
+ _ready_payload(first, c["projects"])))
+ if rest:
+ ready_rows.append("\n".join(
+ [f"{len(rest)} more ready", ""]
+ + _items([_task_row(_phase_head(r),
+ _ready_payload(r, c["projects"]))
+ for r in rest])
+ + ["", ""]))
+ L += _items(ready_rows) or ["- _(nothing ready to launch)_"]
L += [""]
L += h2("gated")
@@ -937,35 +1107,6 @@ def h2(key: str) -> list:
else:
L += ["- _(no rulings yet)_", ""]
- L += h2("epics")
- epic_rows = []
- for e in c["epics"]:
- head = f"{_summary_label(e.get('title') or e['slug'])}"
- half = e.get("mind-half")
- if half and half != "none":
- head += f" β Mind half: {_summary_label(half)}"
- if e.get("ledger"):
- head += f" β ledger: {_summary_label(e['ledger'])}"
- if e.get("status"):
- head += f" β {_summary_label(e['status'])}"
- epic_rows.append(_task_row(head, _epic_payload(e)))
- L += _items(epic_rows) or ["- _(no epics yet)_"]
- L += [""]
-
- L += h2("projects")
- if c["projects"]:
- L += ["| Project | Status | Partition | Sync | Ledger | Witness |",
- "|---|---|---|---|---|---|"]
- for key, row in sorted(c["projects"].items()):
- L.append(f"| {_cell(key)} | {_cell(row.get('status', '-'))} | "
- f"{_cell(row.get('partition', '-'))} | "
- f"`{_cell(row.get('sync_cli', '-'))}` | "
- f"`{_cell(row.get('ledger', '-'))}` | "
- f"`{_cell(row.get('witness_file', '-'))}` |")
- L += [""]
- else:
- L += ["- _(the science body map is empty)_", ""]
-
links = _board_links(home, THEME_ORGAN)
if links:
L += ["---", "",
@@ -982,7 +1123,21 @@ def h2(key: str) -> list:
"table.map th{text-align:left;color:var(--muted);"
"font-weight:600}"
"table.map td,table.map th{border-bottom:1px solid var(--line);"
- "padding:.4rem .5rem .4rem 0;vertical-align:top}")
+ "padding:.4rem .5rem .4rem 0;vertical-align:top}"
+ ".checkin{margin:.2rem 0 1.4rem}"
+ ".paths{margin:.2rem 0}.paths b{display:inline-block;"
+ "min-width:3.6rem}"
+ ".stale{color:var(--bad);font-weight:600}")
+
+# The page is static, so a stamp is never stale *at render* β it is stale on
+# the reader's clock, an hour later, on a different day. So the age is
+# computed on load, here, and nowhere else.
+_CHECKIN_JS = (
+ "(function(){var e=document.getElementById('checkin');if(!e)return;"
+ "var t=e.getAttribute('datetime'),d=t?new Date(t):null;"
+ "if(!d||isNaN(d.getTime())||(Date.now()-d.getTime())/60000>60){"
+ "e.classList.add('stale');"
+ "e.textContent+=' \\u00b7 stale, paste the check-in';}})();")
def render_dashboard_html(c: dict) -> str:
@@ -1048,41 +1203,81 @@ def phase_head(r, tone_pills=()):
H += [f"
{_attr(p)}
" for p in c["problems"][:10]]
H += [""]
+ # The first button on the page, before any section: the paste that
+ # refreshes everything below it, then the stamp saying when that last
+ # happened. The script at the bottom reddens the stamp on the viewer's
+ # own clock.
+ stamp = c.get("checkin") or ""
+ H += [_html_task(_summary_label(CHECKIN_LABEL), _checkin_payload(c)),
+ f'
Last check-in: '
+ f'
']
+
+ H += ['
Summary
',
+ '
One row per active project β the next thing, what '
+ "it is holding, and when it last ruled.
"]
+ rows = summary_rows(c)
+ if rows:
+ H += ['
',
+ "
Project
Next phase
Awaiting
"
+ "
Running
Ready
Last ruling
"]
+ H += [f"
{_summary_label(r['project'])}
"
+ f"
{_summary_label(r['next'])}
{r['awaiting']}
"
+ f"
{r['live']}
{r['ready']}
"
+ f"
{_summary_label(r['ruling'])}
" for r in rows]
+ H.append("
")
+ else:
+ H.append('
(no active project)
')
+
- H.append(h2("by_project"))
+ H.append(h2("projects"))
shown, folded = by_project_keys(c)
- for key in shown:
- H += [f"
']
- groups = project_groups(key, c)
- if not groups:
+ for key in project_order(shown, c):
+ row = c["projects"][key]
+ H.append(f"
{_summary_label(key)}
")
+ H += [f'
{label} '
+ f"{_summary_label(path)}
"
+ for label, path in project_paths(row)]
+ H.append(f'
{_summary_label(_project_facts(key, c))}'
+ "
")
+ rest = open_phases(key, c)
+ nxt = next_open_phase(key, c)
+ if nxt is None:
H.append('
(nothing open β every phase of this '
"project is history)
")
- for title, rows in groups:
- H.append(f"
{title}
")
- for r in rows:
- text = phase_head(r, ((r["state"], "n"),))
- if r["failed_runs"]:
- text += (' β β οΈ failed runs: '
- + _summary_label(", ".join(r["failed_runs"]))
- + "")
- note = _live_note(r) if r["state"] in LIVE_STATES else ""
- if note:
- text += f' β {_summary_label(note)}'
- H.append(f"
{text}
")
- if r["where"]:
- H += ["
"] + [f"
{_md_inline(_summary_label(b))}
"
- for b in r["where"]] + ["
"]
- for label, payload in phase_chips(r, c):
- H.append(_html_task(_summary_label(label), payload))
+ continue
+ rest = rest[1:]
+ text = phase_head(nxt, ((nxt["state"], "n"),))
+ if nxt["failed_runs"]:
+ text += (' β β οΈ failed runs: '
+ + _summary_label(", ".join(nxt["failed_runs"]))
+ + "")
+ chips = phase_chips(nxt, c)
+ if chips:
+ H.append(_html_task(text, chips[0][1]))
+ H += [_html_task(f"β³ {_summary_label(label)}", payload)
+ for label, payload in chips[1:]]
+ else:
+ H.append(f"
{text}
")
+ if rest:
+ H.append(f"{_fold_label(rest)}
")
+ H += [f"
{link(r['rel'], _summary_label(r['title']))}"
+ f' β {r["state"]}
'
+ for r in rest]
+ links = [f'phases/'
+ f"{_summary_label(key)}/" if blob
+ else f"phases/{_summary_label(key)}/"]
+ if _issues_url(row):
+ links.append(f'issues')
+ H.append("
" + " Β· ".join(links) + "
")
if folded:
H.append('
'
- + f"{len(folded)} dormant project(s) with nothing open: "
- + " Β· ".join(f"{_summary_label(k)} "
- f"({_summary_label(project_counts(k, c))})"
- for k in folded) + "
")
+ + f"{len(folded)} project(s) with nothing open: "
+ + " Β· ".join(
+ f"{_summary_label(k)} "
+ f"({_summary_label((c['projects'][k].get('status') or '?').strip())}"
+ f" Β· {_summary_label(project_counts(k, c))})"
+ for k in project_order(folded, c)) + "
")
H.append(h2("awaiting"))
for r in c["awaiting"]:
@@ -1108,9 +1303,14 @@ def phase_head(r, tone_pills=()):
H.append('
(nothing on the queue)
')
H.append(h2("ready"))
- for r in c["ready"]:
- H.append(_html_task(phase_head(r, (("ready", "g"),)),
- _ready_payload(r, c["projects"])))
+ for first, rest in ready_groups(c):
+ H.append(_html_task(phase_head(first, (("ready", "g"),)),
+ _ready_payload(first, c["projects"])))
+ if rest:
+ H.append(f"{len(rest)} more ready")
+ H += [_html_task(phase_head(r, (("ready", "g"),)),
+ _ready_payload(r, c["projects"])) for r in rest]
+ H.append("")
if not c["ready"]:
H.append('
')
- H.append(h2("epics"))
- for e in c["epics"]:
- text = f"{_summary_label(e.get('title') or e['slug'])}"
- half = e.get("mind-half")
- if half and half != "none":
- # The epic's development half lives in the Mind; the card links it
- # so the two dashboards read as one programme.
- text += f' β Mind half: {_summary_label(half)}'
- if e.get("ledger"):
- text += f' β ledger: {_summary_label(e["ledger"])}'
- if e.get("status"):
- text += f' β {_summary_label(e["status"])}'
- H.append(_html_task(text, _epic_payload(e)))
- if not c["epics"]:
- H.append('
(no epics yet)
')
-
- H.append(h2("projects"))
- if c["projects"]:
- H += ['
',
- "
Project
Status
Partition
"
- "
Sync
Witness
"]
- for key, row in sorted(c["projects"].items()):
- H.append(f"
{_summary_label(key)}
"
- f"
{_summary_label(row.get('status', '-'))}
"
- f"
{_summary_label(row.get('partition', '-'))}
"
- f"
{_summary_label(row.get('sync_cli', '-'))}
"
- f"
{_summary_label(row.get('witness_file', '-'))}
"
- "
")
- H.append("
")
- else:
- H.append('
(the science body map is empty)
')
-
footer = boards_footer(dict(_board_links(home, THEME_ORGAN)), THEME_ORGAN)
if footer:
H.append(footer)
- H += [f"", "