Skip to content

Commit 7ebd8f1

Browse files
authored
Merge pull request #276 from PyAutoLabs/claude/pyautoheart-stale-r4swxg
board: render the Heart's gap remedies and its whole-tier plan
2 parents 979fac5 + 522ea81 commit 7ebd8f1

5 files changed

Lines changed: 111 additions & 6 deletions

File tree

agents/faculties/vitals/AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ faculty every cycle.)
124124
5. **Explain and recommend.** Produce the structured report below. Recommendations
125125
should be actionable and, where Heart offers a remediation entry point, cite it
126126
(`pyauto-heart fix ci <repo>`, `fix dirty <repo>`, `fix drift`,
127-
`fix timing <project>`). Do not invent fixes Heart cannot support.
127+
`fix timing <project>`). For an evidence gap the entry point is
128+
`fix stale`: it prints the check that closes each gap and the one plan that
129+
closes them all — quote those, do not compose your own. Do not invent fixes
130+
Heart cannot support.
128131

129132
## Output schema
130133

@@ -143,7 +146,8 @@ Status: <GREEN | STALE | YELLOW | RED> (score <0-100>, snapshot <ts>)
143146
- <yellow reason, mapped to its capability> (or "None")
144147
145148
### Evidence Gaps
146-
- <stale reason, mapped to its capability, with the check to re-run> (or "None")
149+
- <stale reason, mapped to its capability, with the check to re-run — the
150+
command Heart publishes for it, never one you invented> (or "None")
147151
148152
### Recommendations
149153
- <actionable next step, citing a `pyauto-heart fix ...` where applicable> (or "None")

agents/faculties/vitals/HEART_CAPABILITIES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ reimplement inside Brain.
1717
- `pyauto-heart watch [seconds]` / `live` — continuous monitoring daemon.
1818
- `pyauto-heart stop` / `stop --all` — daemon lifecycle control.
1919
- `pyauto-heart fix <topic>` — emits a context bundle/invocation for remediation;
20-
it must not mutate other repos directly.
20+
it must not mutate other repos directly. `fix stale` is the freshness door:
21+
the current evidence gaps, the check that closes each, and the ONE plan
22+
(prompt, plus a command chain where every gap has one) that closes them all —
23+
the same payloads the board's ⌨/📋 chips carry.
2124

2225
## Continuous checks in Heart
2326

board/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ copy-for-Claude payload:
1717
|---------|--------------|-----------------|
1818
| ⌨ Morning sync | `bin/morning.sh` (local) | the terminal command itself |
1919
| 🌙 Overnight | scheduled workflows (`config/policy.yaml board: overnight_jobs`); a ⏸ blocked gate's ::warning annotation is rendered inline | `/bug … — <run url>` on failures |
20-
| ❤️ Readiness & release | the Heart board's `badge.json` + `board.json` (structured blockers, each carrying its OWN `/bug` prompt — rendered verbatim, never re-derived) and the Hands badge | `/health`, the blockers' own prompts |
20+
| ❤️ Readiness & release | the Heart board's `badge.json` + `board.json` (structured blockers, each carrying its OWN `/bug` prompt — or, for an evidence gap, the `command` that re-runs its check — plus `stale_plan`, the one payload that closes every gap; all rendered verbatim, never re-derived) and the Hands badge | `/health`, the plan's prompt or command chain, the blockers' own prompts |
2121
| ⏱ Test performance | the Heart board's published `performance` block — rendered verbatim, never re-derived | each row's own prompt |
2222
| 🏷️ Version consistency | the coupled-set stamps (`board: version_stamps`) | `/bug version drift: …` |
2323
| 💬 Community | the Ears (`community scan`, reused wholesale) — every open conversation gets a row | `/community`, `/community triage <ref>` |

board/_board.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,26 @@ def extract_heart_blockers(board):
256256
"repo_url": b.get("repo_url"),
257257
"run_url": b.get("run_url"),
258258
"prompt": b.get("prompt"),
259+
# An evidence gap also arrives with the command that re-runs its check
260+
# (Heart board.json v3); absent on other severities and on an older
261+
# Heart publish. Forwarded verbatim, like everything else here.
262+
"command": b.get("command"),
259263
} for b in blockers[:HEART_BLOCKER_CAP]]
260264

261265

266+
def extract_heart_plan(board):
267+
"""The Heart's whole-tier remedy — one payload that closes every current
268+
evidence gap ({count, command, prompt}). Rendered here verbatim; the Brain
269+
never derives a remedy of its own. None when nothing is stale, or when the
270+
surface predates the field."""
271+
plan = (board or {}).get("stale_plan")
272+
if not isinstance(plan, dict) or not plan.get("prompt"):
273+
return None
274+
return {"count": plan.get("count"),
275+
"command": plan.get("command"),
276+
"prompt": str(plan["prompt"])}
277+
278+
262279
def fetch_heart_blockers(pages_base, repo, degraded):
263280
"""Fetch-and-extract in one call (the blockers-only door)."""
264281
return extract_heart_blockers(fetch_heart_board(pages_base, repo, degraded))
@@ -725,6 +742,7 @@ def collect():
725742
# and the test-performance block.
726743
heart_board = fetch_heart_board(pages_base, heart_repo, degraded)
727744
heart_blockers = extract_heart_blockers(heart_board)
745+
heart_plan = extract_heart_plan(heart_board)
728746
performance = extract_heart_performance(
729747
heart_board, f"{pages_base}/{heart_repo}/")
730748
hands = fetch_badge(pages_base, board_family.get("hands", "PyAutoHands"))
@@ -743,6 +761,7 @@ def collect():
743761
"overnight": overnight,
744762
"heart": heart,
745763
"heart_blockers": heart_blockers,
764+
"heart_plan": heart_plan,
746765
"performance": performance,
747766
"hands": hands,
748767
"versions": versions,
@@ -871,12 +890,20 @@ def render_md(data):
871890
f"[board]({data['boards'].get('heart', '')}) · re-run via `/health`")
872891
else:
873892
L.append("- Heart board unreachable — consult `/health` directly")
893+
plan = data.get("heart_plan")
894+
if plan:
895+
# The gaps are worth one line, not N: the Heart already wrote the plan
896+
# that closes all of them, and `fix stale` prints it in a terminal.
897+
clear = f"`{plan['command']}`" if plan.get("command") else "`pyauto-heart fix stale`"
898+
L.append(f"- Evidence gaps: {plan.get('count', '?')} — clear them all: {clear}")
874899
for b in data.get("heart_blockers") or []:
875900
sev = f"[{b['severity']}] " if b.get("severity") else ""
876901
line = f" - {sev}{b['text']}"
877902
if b.get("run_url"):
878903
line += f" — [run]({b['run_url']})"
879904
L.append(line)
905+
if b.get("command"):
906+
L.append(f" - `{b['command']}`")
880907
if b.get("prompt"):
881908
L.append(f" - `{b['prompt']}`")
882909
if data.get("hands"):
@@ -1166,6 +1193,15 @@ def render_html(data):
11661193
else:
11671194
H.append(_row("Heart board unreachable — consult the clinician "
11681195
"directly." + pills(("unreachable", "y")), "/health"))
1196+
plan = data.get("heart_plan")
1197+
if plan:
1198+
# One tap for the whole stale tier, above the gaps it closes: the
1199+
# Claude prompt always, the shell chain when the Heart could offer one.
1200+
H.append(_row(f'Clear all {plan.get("count", "?")} evidence gaps — one prompt'
1201+
+ pills(("stale", "y")), plan["prompt"]))
1202+
if plan.get("command"):
1203+
H.append(_row("… or the command chain that re-runs every check",
1204+
plan["command"], term=True))
11691205
for b in data.get("heart_blockers") or []:
11701206
sev = b.get("severity")
11711207
links = "".join(

tests/test_board.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515

1616
import base64
17+
import html
1718
import json
1819
import os
1920
import re
@@ -26,7 +27,8 @@
2627
BRAIN = BRAIN_HOME / "bin" / "pyauto-brain"
2728

2829
SURFACE_KEYS = {
29-
"generated", "org", "overnight", "heart", "heart_blockers", "performance",
30+
"generated", "org", "overnight", "heart", "heart_blockers", "heart_plan",
31+
"performance",
3032
"hands", "versions", "community", "resume", "open_issues", "hygiene",
3133
"devbox", "autonomy", "doors", "boards", "degraded", "history",
3234
}
@@ -78,14 +80,17 @@
7880
}
7981

8082
HEART_BOARD_JSON = {
81-
"schema_version": 2,
83+
"schema_version": 3,
8284
"blockers": [{
8385
"text": "RepoA: nightly smoke red",
8486
"severity": "red",
8587
"repo": "RepoA",
8688
"repo_url": "https://example.invalid/RepoA",
8789
"run_url": "https://example.invalid/run/9",
8890
"prompt": "/bug Heart board: RepoA nightly smoke red — https://example.invalid/run/9",
91+
# v3: a stale row carries the command that re-runs its check; a red
92+
# one has no such remedy — the fix is code, not a re-run.
93+
"command": None,
8994
}],
9095
# Additive to schema v2 — an older Heart publish simply omits it.
9196
"performance": HEART_PERFORMANCE,
@@ -529,6 +534,63 @@ def test_heart_blockers_render_with_their_own_prompts(tmp_path):
529534
assert "Shipped: **GREEN**" in md # the Hands headline joined the section
530535

531536

537+
HEART_BOARD_STALE = {
538+
"schema_version": 3,
539+
"blockers": [
540+
{"text": "install verification not run", "severity": "stale",
541+
"repo": None, "repo_url": None, "run_url": None,
542+
"command": "pyauto-heart verify_install --report-json",
543+
"prompt": "/health run `pyauto-heart verify_install --report-json` — …"},
544+
{"text": "no release validation for current source", "severity": "stale",
545+
"repo": None, "repo_url": None, "run_url": None, "command": None,
546+
"prompt": "/health dispatch a release rehearsal with `/release rehearse` — …"},
547+
],
548+
"stale_plan": {
549+
"count": 2,
550+
"command": None,
551+
"prompt": "/health clear the Heart's 2 evidence gap(s) — …\n1. …\n2. …",
552+
},
553+
}
554+
555+
556+
def test_a_stale_heart_offers_the_one_plan_that_clears_every_gap(tmp_path):
557+
stub = _fabricate(tmp_path, _default_fixtures(), HEART_BOARD_STALE)
558+
page = _run(["--html"], tmp_path, stub).stdout
559+
plan = HEART_BOARD_STALE["stale_plan"]
560+
561+
# The Heart's own plan is the chip payload — the Brain never derives one.
562+
assert html.escape(plan["prompt"], quote=True) in page
563+
assert "Clear all 2 evidence gaps" in page
564+
# Each gap still arrives with the command that closes it, forwarded whole.
565+
s = json.loads(_run(["--json"], tmp_path, stub).stdout)
566+
assert [b["command"] for b in s["heart_blockers"]] == [
567+
"pyauto-heart verify_install --report-json", None]
568+
assert s["heart_plan"]["count"] == 2
569+
# The digest spends one line on the tier, not one per gap.
570+
md = _run([], tmp_path, stub).stdout
571+
assert "Evidence gaps: 2 — clear them all: `pyauto-heart fix stale`" in md
572+
573+
574+
def test_a_plan_with_a_command_chain_offers_the_terminal_door(tmp_path):
575+
chain = "pyauto-heart verify_install --report-json && pyauto-heart tick"
576+
board = {**HEART_BOARD_STALE,
577+
"stale_plan": {**HEART_BOARD_STALE["stale_plan"], "command": chain}}
578+
stub = _fabricate(tmp_path, _default_fixtures(), board)
579+
580+
page = _run(["--html"], tmp_path, stub).stdout
581+
assert f'data-cmd="{html.escape(chain, quote=True)}"' in page
582+
assert "copy term" in page # the ⌨ terminal chip, not the 📋 one
583+
md = _run([], tmp_path, stub).stdout
584+
assert f"clear them all: `{chain}`" in md
585+
586+
587+
def test_a_heart_board_without_a_plan_renders_none(tmp_path):
588+
stub = _fabricate(tmp_path, _default_fixtures()) # no stale_plan published
589+
s = json.loads(_run(["--json"], tmp_path, stub).stdout)
590+
assert s["heart_plan"] is None
591+
assert "Evidence gaps:" not in _run([], tmp_path, stub).stdout
592+
593+
532594
def test_test_performance_rows_carry_their_own_prompts(tmp_path):
533595
stub = _fabricate(tmp_path, _default_fixtures(), HEART_BOARD_WITH_EVENT)
534596
s = json.loads(_run(["--json"], tmp_path, stub).stdout)

0 commit comments

Comments
 (0)