Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agents/conductors/cortex/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ note`).
| `checkin [--dry-run \| --apply] [--push \| --no-push] [--project KEY]` | **Where is my science?** | the door — the sequence below, ending in a summary keyed by project; reaches no cluster; exit **1** when the tree does not check |
| `census [--json]` | What is the Cortex holding? | per project: status, summary, runs by state, log length, last update; totals (active projects, runs open, runs running); the check-in stamp; the problems `cortex.py check` would report |
| `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` |
| `dashboard --apply` | — | writes `dashboard.md` + `dashboard.html` + `state.json` — the organ-cockpit feed (`board/_state.py` v1): red on `cortex.py check` problems, yellow while a run is running or the check-in is stale (the page script's 180-min rule, judged at render), items carry `checkin_payload` / `resume_payload`; `--check` ignores its `updated` |
| `issue [--project KEY] [--apply]` | What sits at the top of each project's issue? | the fenced ledger block (`cortex.py`'s `issue_block`) per project with an `Issue:`; `--apply` writes it into the issue body through `gh` (replacing the block between the markers, or prepending it); never creates an issue; exit **1** without `gh` |

```
Expand Down Expand Up @@ -133,7 +133,7 @@ It writes nothing: no stamp, no pages, no ledger. `--dry-run` prints the exact

### `checkin` — the door

1. **Stamp + render.** `checkin.yaml`, then `dashboard.md` + `dashboard.html`.
1. **Stamp + render.** `checkin.yaml`, then `dashboard.md` + `dashboard.html` + `state.json`.
2. **Push** (the rule below).
3. **Summarise, by project** — printed **last**, so a chat sees it above the
fold: `key — summary`, Now, the runs **as the ledger holds them**, the last
Expand Down
129 changes: 125 additions & 4 deletions agents/conductors/cortex/_cortex.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
from _theme import ( # noqa: E402
JS as _THEME_JS, boards_footer, css as _theme_css, hero, pills, stats,
)
# The organ-cockpit feed contract (board/_state.py, state.json v1) — the same
# constructor the Brain board uses, so this renderer cannot write a feed the
# cockpit would reject.
from _state import build_state # noqa: E402

THEME_ORGAN = "cortex" # whose logo this page wears
CORTEX_REPO = "PyAutoCortex" # an organ name, not an instance fact
Expand Down Expand Up @@ -696,9 +700,126 @@ def keep(line: str) -> bool:
return "\n".join(l for l in page.splitlines() if keep(l))


# ------------------------------------------------------- the cockpit feed ---
# The third render of the one census (PyAutoBrain#418): `state.json`, the
# per-organ machine surface the cockpit and the phone read (board/_state.py,
# contract v1). Every prompt is a payload the page already copies —
# `checkin_payload`, `resume_payload` — composed, never re-derived.
STATE_ITEM_CAP = 20


def _now() -> _dt.datetime:
"""The render clock (one seam, so tests can move it)."""
return _dt.datetime.now(_dt.timezone.utc)


def checkin_stale(stamp: str, now: _dt.datetime | None = None) -> bool:
"""The page script's freshness rule (`_CHECKIN_JS`), in Python: a missing
or unparseable stamp, or one older than CHECKIN_FRESH_MINUTES, is stale.
The page judges on the reader's clock at load; the feed judges at render."""
if not stamp:
return True
try:
then = _dt.datetime.fromisoformat(stamp.replace("Z", "+00:00"))
except ValueError:
return True
if then.tzinfo is None:
return True
now = now or _now()
return (now - then).total_seconds() / 60 > CHECKIN_FRESH_MINUTES


def _problem_ledger(c: dict, problem: str) -> dict | None:
"""The ledger a `cortex.py check` problem names (`projects/<key>.md: …`)."""
for d in c["ledgers"]:
if problem.startswith(f"{d['rel']}:"):
return d
return None


def _state_items(c: dict, stale: bool) -> list:
items = []
for p in c["problems"]:
d = _problem_ledger(c, p)
items.append({"severity": "red", "text": " ".join(p.split()),
"url": (d or {}).get("issue_url") or None,
"prompt": None})
if stale:
items.append({"severity": "yellow",
"text": f"check-in stale — last "
f"{c.get('checkin') or CHECKIN_NEVER}",
"url": None, "prompt": checkin_payload(c)})
for d in c["ledgers"]:
row = c["projects"].get(d["key"], {})
for r in d["runs"]:
if r["state"] != "running":
continue
items.append({
"severity": "info",
"text": " ".join(f"{d['key']}: {r['ident']} running "
f"({r['partition'] or '?'})".split()),
"url": d.get("issue_url") or None,
"prompt": resume_payload(d["key"], row)})
order = {"red": 0, "yellow": 1, "info": 2}
items.sort(key=lambda i: order[i["severity"]]) # stable: section order kept
return items[:STATE_ITEM_CAP]


def render_state(c: dict) -> str:
"""`state.json` — the Cortex's organ-cockpit feed, as JSON text.

Status: red when `cortex.py check` reports problems; yellow when a run is
on the cluster or the check-in is stale; green otherwise; grey when there
is no ledger at all. Headline `A active · R running`, state-word prefixed
when not green."""
now = _now().replace(microsecond=0)
n = c["counts"]
stale = checkin_stale(c.get("checkin") or "", now)
if not c["ledgers"]:
status = "grey"
elif c["problems"]:
status = "red"
elif n["running"] > 0 or stale:
status = "yellow"
else:
status = "green"
line = f"{n['active']} active · {n['running']} running"
if status == "grey":
line = "no project ledgers"
home = c.get("home", "")
state = build_state(
organ="cortex",
repo=home.rstrip("/").rsplit("/", 1)[-1] if home else CORTEX_REPO,
status=status,
headline=line if status == "green" else f"{status.upper()} — {line}",
updated=now.strftime("%Y-%m-%dT%H:%M:%SZ"),
# the Pages site this feed is served from; relative when underivable
pages_url=_pages_url(home) or "./",
items=_state_items(c, stale) if c["ledgers"] else [],
)
return json.dumps(state, indent=2) + "\n"


def state_body(text: str) -> str:
"""The feed minus its `updated` render stamp — what `--check` compares
(`dashboard_body`'s job for the pages)."""
try:
obj = json.loads(text)
except ValueError:
return text
if isinstance(obj, dict):
obj.pop("updated", None)
return json.dumps(obj, sort_keys=True)


def page_body(name: str, text: str) -> str:
return state_body(text) if name == "state.json" else dashboard_body(text)


def render_pages(c: dict) -> dict:
return {"dashboard.md": render_dashboard(c),
"dashboard.html": render_dashboard_html(c)}
"dashboard.html": render_dashboard_html(c),
"state.json": render_state(c)}


def cmd_dashboard(root: Path, c: dict, a) -> int:
Expand All @@ -708,10 +829,10 @@ def cmd_dashboard(root: Path, c: dict, a) -> int:
for name, want in pages.items():
target = root / name
on_disk = _read(target) if target.is_file() else ""
if dashboard_body(on_disk) != dashboard_body(want):
if page_body(name, on_disk) != page_body(name, want):
stale.append(name)
if not stale:
print("dashboard.md + dashboard.html are current")
print("dashboard.md + dashboard.html + state.json are current")
return RC_OK
print(f"{' + '.join(stale)} stale — regenerate with "
"`pyauto-brain cortex dashboard --apply`", file=sys.stderr)
Expand Down Expand Up @@ -1109,7 +1230,7 @@ def cmd_checkin(root: Path, mod, a) -> int:
for n in notes:
print(f"\nnote: {n}")
print(f"\nwould stamp {CHECKIN_FILE}, re-render dashboard.md + "
"dashboard.html, and read back "
"dashboard.html + state.json, and read back "
f"{len(keys)} project(s). Run with --apply to check in.")
print("The pull is its own verb, on the laptop: "
"`pyauto-brain cortex pull`.")
Expand Down
2 changes: 1 addition & 1 deletion agents/conductors/intake/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ the page's Hygiene section), and an un-themed prompt falls back to `Target:`.
| **classify** | `intake "<text>"` / `intake classify --file P` | classify one raw input; `--apply` writes the prompt |
| **ideas** | `intake ideas` | scan `ideas.md`, propose one prompt per bullet; `--apply` writes them |
| **census** | `intake census` | inventory every filed prompt (work-type/target/difficulty/status + hygiene flags); always read-only |
| **dashboard** | `intake dashboard` | render the census as the Mind **task** page — picks, in flight, human review, parked, planned, backlog, bundles, recent, epics; `--apply` writes `PyAutoMind/dashboard.md`, `--check` exits 1 on drift |
| **dashboard** | `intake dashboard` | render the census as the Mind **task** page — picks, in flight, human review, parked, planned, backlog, bundles, recent, epics; `--apply` writes `PyAutoMind/dashboard.md` + `dashboard.html` + `state.json` (the organ-cockpit feed, `board/_state.py` v1: red when a task awaits input/is blocked or prompts drift, yellow when work waits on a merge/release/review or picks exist; items carry the page's own copy payloads), `--check` exits 1 on drift (ignoring the page date and the feed's `updated`) |
| **formalise** | `intake formalise [prefix]` | retroactively header the prompts census flags — derive the missing fields, insert in place, prose untouched; `--apply` writes |
| **reconcile** | `intake reconcile [prefix]` | rank backlog prompts that look already-shipped (vs the `complete/` records / `active/`), and pair live prompts that look like the same work filed twice; always read-only — retiring stays human |
| **reconcile --repo** | `intake reconcile --repo <target> [prefix]` | **also** read the target repo's source: identifiers the prompts name that exist upstream, and lines they quote that are **gone** — the two signals that see a prompt with no Mind-side trace. Opt-in; the default path is offline |
Expand Down
Loading
Loading