diff --git a/agents/conductors/profiling/AGENTS.md b/agents/conductors/profiling/AGENTS.md index c1a5c0e..cf2cd2f 100644 --- a/agents/conductors/profiling/AGENTS.md +++ b/agents/conductors/profiling/AGENTS.md @@ -110,7 +110,19 @@ as malformed — only a record missing *some* of its key fields is corruption. (`triage`) stays the only judge of what a timing or a drift means, with the human. The projects keep their producers, results, pins and drift policy. Same split as Heart ↔ vitals and Eyes ↔ the Eyes conductor. (Organ row - born in PyAutoMind#463; its reader and board arrive in phase 2.) + born in PyAutoMind#463; its reader and board arrived in phase 2.) +- **Where drift candidates are read** — the + [Pulse board](https://pyautolabs.github.io/PyAutoPulse/) (strip on the + Brain board's Resume section, card on the organ cockpit) is where + cross-project drift candidates and their provenance (the resolved commit + per project, the pair of records, the evidence paths) are read. + `autolens_profiling` owns the measurement semantics: its producers, pins + and drift policy. A `/profiling triage ` prompt copied from + a Pulse drift item names one comparison row on the Pulse board: open that + row, read the pair of records and their evidence paths, then judge with the + `triage` verb. The key is a pointer for the human/agent reading the board — + `triage` itself takes no target argument and still sweeps every pinned + finding. - **vs build** — campaigns are not releases; `profile.yml`'s on-release runs stay CI/Build territory. - **release-validation script cost is NOT ours.** The compile axis was diff --git a/board/AGENTS.md b/board/AGENTS.md index f2c0e93..3377fad 100644 --- a/board/AGENTS.md +++ b/board/AGENTS.md @@ -21,7 +21,7 @@ copy-for-Claude payload: | ⏱ Test performance | the Heart board's published `performance` block — rendered verbatim, never re-derived | each row's own prompt | | 🏷️ Version consistency | the coupled-set stamps (`board: version_stamps`) | `/bug version drift: …` | | 💬 Community | the Ears (`community scan`, reused wholesale) — every open conversation gets a row | `/community`, `/community triage ` | -| 🔄 Resume | the Mind's registry + generated counts; pending-release PRs | `/start_dev …`, `/prm ` | +| 🔄 Resume | the Mind's registry + generated counts; the Cortex, Eyes and Pulse strips (each organ's own `dashboard.md` head counts, display-only, never in `state.json`); pending-release PRs | `/start_dev …`, `/prm ` | | 🧹 Upkeep | open-issue count; the cleanup doors | `/issue_cleanup`, `/hygiene`, `/repo_cleanup` | | 🧼 Hygiene | the hygiene conductor's own `--json` pre-scan, run IN this render (BOARD_HYGIENE_SCAN=1; brain_board.yml checks out the body-map scan set blobless+sparse first) — no machine involved | each row's own delegate door | | 🖥️ Dev box | `state/devbox_board.json` — worktree state (unpushed/dirty/stashes — the one thing only the dev box can see; its hygiene rows render only when no cloud scan ran), pushed by `board publish` (morning.sh's last step); age-stamped, stale at 48h, dropped after 7d | — | diff --git a/board/_board.py b/board/_board.py index 467fecd..4ff67cf 100755 --- a/board/_board.py +++ b/board/_board.py @@ -725,6 +725,21 @@ def collect_eyes(repo="PyAutoEyes"): return _dashboard_counts(repo, head_only=True) +def collect_pulse(repo="PyAutoPulse"): + """The Pulse's own generated counts (projects, records, comparisons, + drifted, refused pairs, cached, failed — the counts table at the head of + its `dashboard.md`), or None. + + Composed exactly as the Eyes strip is: the organ's renderer decides the + numbers, the Brain board only shows them — a drifted count is the + producer's say-so, never a Brain verdict (judgment is the profiling + conductor's `triage`). Only the head of the page is read, because the + body carries one table per project. No Pulse checkout, or no rendered + board, is not a degraded morning either — the strip just does not appear. + """ + return _dashboard_counts(repo, head_only=True) + + def collect_open_issues(org, degraded): """Total open issues across the org — the /issue_cleanup pointer count (the audit itself stays that skill's confirmation-gated job).""" @@ -932,6 +947,7 @@ def collect(): resume = collect_resume(org, degraded) cortex = collect_cortex() eyes = collect_eyes(board_family.get("eyes", "PyAutoEyes")) + pulse = collect_pulse(board_family.get("pulse", "PyAutoPulse")) open_issues = collect_open_issues(org, degraded) boards = {name: f"{pages_base}/{repo}/" for name, repo in board_family.items()} @@ -952,6 +968,7 @@ def collect(): "resume": resume, "cortex": cortex, "eyes": eyes, + "pulse": pulse, "open_issues": open_issues, "hygiene": collect_hygiene(degraded), "devbox": collect_devbox(), @@ -1343,6 +1360,11 @@ def render_md(data): + " · ".join(f"{k.lower()} {n}" for k, n in data["eyes"].items()) + f" — [Eyes board]({data['boards'].get('eyes', '')})") + if data.get("pulse"): + L.append("- Pulse: " + + " · ".join(f"{k.lower()} {n}" + for k, n in data["pulse"].items()) + + f" — [Pulse board]({data['boards'].get('pulse', '')})") for t in data["resume"]["tasks"]: L.append(f" - `Use the start-dev skill. {t['path']}` — {t['title'][:70]}") pending = data["resume"]["pending_prs"] @@ -1727,6 +1749,13 @@ def community_row(e, note, tone): "board \u2197" + pills(*[(f"{k.lower()} {n}", "" if i == 0 else "n") for i, (k, n) in enumerate(data["eyes"].items())]))) + if data.get("pulse"): + pulse_url = data["boards"].get("pulse", "") + H.append(_plain( + f'Run times on the Pulse ' + "board \u2197" + + pills(*[(f"{k.lower()} {n}", "" if i == 0 else "n") + for i, (k, n) in enumerate(data["pulse"].items())]))) for t in data["resume"]["tasks"]: # An in-flight task wears the header facets the Mind gave it, so it # looks like itself on both pages — same pills, same order. diff --git a/docs/organs/pulse.md b/docs/organs/pulse.md index 14c559e..cf38e6b 100644 --- a/docs/organs/pulse.md +++ b/docs/organs/pulse.md @@ -12,7 +12,8 @@ the Eyes: the Eyes show what the software *shows*; the Pulse feels how fast it **Repo:** [PyAutoLabs/PyAutoPulse](https://github.com/PyAutoLabs/PyAutoPulse) **Status:** organ row registered (phase 0 of the `profiling-organ-birth` -epic); the registry, reader, receipts, board and workflows arrive in phase 2. +epic); the registry, reader, receipts, board and workflows arrived in phase 2; +phase 3 puts the organ on the Brain board and the organ cockpit. Design: `docs/research/profiling_inference_organs.md` in the Brain repo. ## The defining function: feeling the pulse @@ -41,6 +42,13 @@ The board shows one row per registered project with scope, evidence time, last fetch, coverage and links, and keeps missing or refused evidence visible beside valid data. A valid empty feed says "no measurements", never "all passed". +Beside the board, the organ publishes two machine surfaces: `badge.json`, a +one-line headline, and `state.json`, the organ-cockpit feed (contract v1 in +`PyAutoBrain/board/state_schema.json`). The feed is what puts the Pulse on the +[organ cockpit](https://pyautolabs.github.io/cockpit/) alongside the other +organs; the Brain board's Resume section carries a Pulse strip composed from +the head counts of the organ's own `dashboard.md`. + ## The driver split The project repos *produce and hold* timings and the Pulse *shows* them; @@ -76,3 +84,7 @@ The organ is being born through the `profiling-organ-birth` epic: the lens profiling project. - **Phase 0** ([PyAutoMind#463](https://github.com/PyAutoLabs/PyAutoMind/issues/463)) added the organ row to the body map. +- **Phase 3** ([PyAutoBrain#450](https://github.com/PyAutoLabs/PyAutoBrain/issues/450)) + added the Pulse strip to the Brain board and the Pulse card to the organ + cockpit, and named the Pulse board in the profiling conductor's prose as + where drift candidates are read. diff --git a/skills/profiling/profiling.md b/skills/profiling/profiling.md index 3e02b76..dc2376a 100644 --- a/skills/profiling/profiling.md +++ b/skills/profiling/profiling.md @@ -21,6 +21,14 @@ and board show how fast each `_profiling` project runs, but it never judges. This command's `triage` is the only judge of what a timing or a drift means; the projects keep their producers, results, pins and drift policy. +The Pulse board (strip on the Brain board, card on the organ cockpit) is where +cross-project drift candidates and their provenance are read; `autolens_profiling` +owns the producers, pins and drift policy. A `/profiling triage ` +prompt copied from a Pulse drift item names one comparison row on the Pulse +board: open that row, read the pair of records and their evidence paths, then +judge with the `triage` verb (which takes no target — the key is where to look, +not an argument). + The Profiling Agent **reasons; it never runs sweeps or edits source.** The classification is the result for CPU-unusable cells; full timings for those belong to the A100 rows. diff --git a/tests/test_board.py b/tests/test_board.py index 1888dc8..ae6a650 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -37,6 +37,9 @@ # The Eyes organ's own counts (the head of its dashboard.md), None when # no Eyes is checked out. "eyes", + # The Pulse organ's own counts (the head of its dashboard.md), None when + # no Pulse is checked out. + "pulse", } AUTONOMY_LOG = """\ @@ -208,6 +211,26 @@ def _default_fixtures(**overrides): | [fit.png](https://example.invalid/fit.png) | 5 | """ +# The head of the Pulse's generated board is its counts table; its body +# carries one table per project, which the strip must not count. +PULSE_DASHBOARD_MD = """\ +# PyAutoPulse — profiling dashboard + +| Where | Count | +|-------|------:| +| [Projects](#projects) | 1 | +| [Records](#projects) | 159 | +| [Comparisons](#projects) | 145 | +| [Drifted](#projects) | 0 | +| [Refused pairs](#projects) | 0 | +| [Cached](#projects) | 0 | +| [Failed](#projects) | 0 | + +## Projects + +| [alpha_profiling](#alpha_profiling) | 7 | 9 | +""" + def _fabricate(tmp_path, fixtures, heart_board=None): """A PYAUTO_ROOT with a fabricated Mind, file:// sibling-board badges, and @@ -243,6 +266,9 @@ def _fabricate(tmp_path, fixtures, heart_board=None): eyes = tmp_path / "PyAutoEyes" eyes.mkdir() (eyes / "dashboard.md").write_text(EYES_DASHBOARD_MD) + pulse = tmp_path / "PyAutoPulse" + pulse.mkdir() + (pulse / "dashboard.md").write_text(PULSE_DASHBOARD_MD) # One badge per sibling board named in the declared config surface — # read from policy.yaml so no board repo name is hardcoded here. @@ -357,6 +383,28 @@ def test_the_eyes_strip_composes_the_eyes_own_head_counts(tmp_path): assert json.loads(r.stdout)["eyes"] is None +def test_the_pulse_strip_composes_the_pulse_own_head_counts(tmp_path): + """The Pulse decides its own numbers; the Brain board reads only the + counts table at the head of its page (the body's per-project tables are + not counts), links its board, and shows nothing when no Pulse is here.""" + s, _ = _surface(tmp_path) + assert s["pulse"] == {"Projects": 1, "Records": 159, "Comparisons": 145, + "Drifted": 0, "Refused pairs": 0, "Cached": 0, + "Failed": 0} + assert s["boards"]["pulse"].endswith("/PyAutoPulse/") + stub = _fabricate(tmp_path / "md", _default_fixtures()) + md = _run([], tmp_path / "md", stub).stdout + assert ("- Pulse: projects 1 · records 159 · comparisons 145 · " + "drifted 0 · refused pairs 0 · cached 0 · failed 0") in md + page = _run(["--html"], tmp_path / "md", stub) + assert page.returncode == 0, page.stderr + assert "Pulse board" in page.stdout and 'data-organ="pulse"' in page.stdout + stub = _fabricate(tmp_path / "no_pulse", _default_fixtures()) + (tmp_path / "no_pulse" / "PyAutoPulse" / "dashboard.md").unlink() + r = _run(["--json"], tmp_path / "no_pulse", stub) + assert json.loads(r.stdout)["pulse"] is None + + def test_json_surface_is_complete_and_derives_org(tmp_path): s, _ = _surface(tmp_path) assert set(s) == SURFACE_KEYS