Skip to content

Commit 14d2f3d

Browse files
authored
Merge pull request #315 from PyAutoLabs/claude/pyautoheart-stale-r4swxg
prompt: stale reasons carry their own remedy (filed + shipped)
2 parents 1cf58a8 + 85b50a5 commit 14d2f3d

4 files changed

Lines changed: 195 additions & 5 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# stale-remedies-on-the-heart-board
2+
3+
**Completed:** 2026-08-25
4+
**Type:** feature · **Target:** PyAutoHeart, PyAutoBrain · **PRs:**
5+
PyAutoHeart#177, PyAutoBrain#276, PyAutoMind#315 (branch
6+
`claude/pyautoheart-stale-r4swxg` in all three)
7+
8+
## Summary
9+
10+
A STALE Heart board offered nothing copyable that closed a gap. Every stale
11+
reason got the same generic string from `_reason_item()`
12+
`/health re-run the stale evidence: <text>` — which hands the sentence back to
13+
the reader, and a **reason** row carried no command at all on any surface
14+
(commands existed only on **section** rows: `pyauto-heart fix drift`,
15+
`fix timing <project>`). That is most of what the board is asked to do: the
16+
cloud verdict had been `STALE · score 65` on every `heart-health.yml` run since
17+
2026-08-19, alternating only with a transient `RED · 45`.
18+
19+
The 35-point penalty decomposes uniquely into the three evidence gaps a
20+
working-tree-less CI job always has — `install_unknown` (10), `test_unknown`
21+
(10), `validation_absent` (15) — none of which the cloud job can produce, which
22+
is why the published board never reaches GREEN on its own.
23+
24+
## What shipped
25+
26+
**PyAutoHeart**`readiness.compute` emits `stale_details`: each stale reason
27+
with the **gate key** that produced it, index for index with `stale_reasons`.
28+
The `stale.append(...)` / `hit(...)` pairs collapsed into one `add_stale(msg,
29+
key)` helper, so a gap cannot be filed without its key or its score penalty.
30+
Additive: flat reason lists, verdict, score, profiles and the release gate are
31+
untouched (`tests/test_readiness.py` passed unmodified).
32+
33+
`dashboard.py` keys `STALE_REMEDIES` off that key — never off the reason text:
34+
35+
- a stale row gains `command` (the shell remedy; `None` where the remedy needs a
36+
conversation) and a prompt naming both the check and the gap;
37+
- the tier gains `stale_plan`**one** prompt walking every current gap, plus
38+
the shell chain that does the same, offered only when *every* gap has a
39+
command, since a chain that silently skips one reads as if it cleared it;
40+
- html renders a ⌨ command chip beside the 📋 prompt chip and a "clear them all"
41+
line above the gaps; md leads its prompts block with the plan; json carries
42+
`blockers[].command` and `stale_plan` (schema **v3**).
43+
44+
`pyauto-heart fix stale` is the terminal door to the same payloads from the same
45+
persisted verdict, and `readiness` ends a stale block with `→ clear them:
46+
pyauto-heart fix stale`.
47+
48+
**PyAutoBrain**`board/_board.py` forwards each gap's `command` and renders
49+
the Heart's plan verbatim: a 📋 row with the prompt, a ⌨ row with the chain, one
50+
digest line for the tier. The Brain derives no remedy of its own, exactly as it
51+
already treats the Heart's `/bug` prompts.
52+
53+
## Traps and findings
54+
55+
- **The remedy must be keyed, not parsed.** Sniffing the reason sentence would
56+
have coupled the Brain and the board to prose that changes; the gate key is
57+
the gap's real identity and readiness already had it in `hit(key)`.
58+
- **A partial command chain is a lie.** With a release-validation gap in the
59+
set there is no chain that clears everything, so `stale_plan.command` is
60+
withheld and only the prompt is offered. Emitting the runnable subset would
61+
read as "that cleared it".
62+
- **Fall back, never guess.** A verdict from an older Heart carries no keys, so
63+
the rows degrade to the old generic nudge and no plan is offered.
64+
- **STALE's rule survives every path** — every remedy re-runs a check; a stale
65+
row can never emit a `/bug` prompt (asserted).
66+
- **The README strip stays one line for STALE.**
67+
`test_md_brief_is_one_line_unless_something_is_wrong` pins that deliberately;
68+
a glance surface is the wrong place for a multi-step plan.
69+
- **`skew_pypi_unknown` keeps a prompt but no command** — the deep PyPI leg has
70+
no `pyauto-heart` verb, and inventing one would have been a fake remedy.
71+
72+
**Validated:** 615 Heart tests (11 new), 494 Brain tests (3 new). A synthetic
73+
cloud snapshot reproduces the live `STALE · score 65` with the three current
74+
gaps, and `fix stale` prints their commands and the one plan.
75+
76+
## Notes
77+
78+
Filed and shipped the same day, straight from the question "why is pyautoheart
79+
stale" — the answer was legible, but the board had no hand to offer once it was.
80+
The prompt never passed through `active/` (no issue), so this record was written
81+
directly with `lifecycle.py record` and the draft removed in the same PR.
82+
83+
## Original prompt
84+
85+
## The complaint
86+
87+
The Heart board's whole promise is the one in its own lede — "📋 copies a
88+
ready-to-paste prompt or command for a Claude Code chat". That holds for RED
89+
and YELLOW: a blocker row carries a `/bug Heart board: <text> — failing run:
90+
<url>` prompt, and a section row can carry a real command (`pyauto-heart fix
91+
drift`, `pyauto-heart fix timing <project>`). It does **not** hold for STALE.
92+
Every evidence gap gets the same generic string from `_reason_item()`
93+
(`heart/dashboard.py:1031`):
94+
95+
/health re-run the stale evidence: install verification not run
96+
97+
That names the gap the human just read and stops. It contains no command, no
98+
repo, no artifact path, no next step — copying it just hands the sentence back
99+
to a Claude session that then has to work out the remedy from scratch. A reason
100+
row has no command chip at all, on any surface; commands exist only on section
101+
rows. So on a STALE morning there is nothing on the board to copy that actually
102+
closes the gap.
103+
104+
This matters more than a RED chip would, because STALE is the board's steady
105+
state: the cloud verdict has been `STALE · score 65` on every `heart-health.yml`
106+
run since 2026-08-19 (alternating only with a transient `RED · 45` when a repo's
107+
CI goes red), with exactly three gaps, all of which have a known remedy nobody
108+
can copy:
109+
110+
| stale reason | gate key | weight | remedy today (undiscoverable) |
111+
|---|---|---|---|
112+
| `install verification not run` | `install_unknown` | 10 | `pyauto-heart verify_install --report-json` then `pyauto-heart tick` |
113+
| `test run status unknown (no report.json)` | `test_unknown` | 10 | a workspace validation run (Hands `run_logs/latest/report.json`) then `pyauto-heart tick` |
114+
| `no release validation for current source` | `validation_absent` | 15 | `/release rehearse`, then `pyauto-heart validate --ingest <artifacts>` |
115+
116+
## What to build
117+
118+
Give every stale reason a **remedy of its own** — a copyable command where one
119+
exists, and a targeted prompt where it does not — keyed by the readiness gate
120+
key, never sniffed out of the reason string.
121+
122+
STALE's rule holds throughout: a remedy **re-runs a check, it never fixes
123+
code**. Nothing here may emit a `/bug` door for a stale reason.
124+
125+
1. **Carry the key out of readiness.** `heart/readiness.py` already knows the
126+
identity of each gap — it calls `hit("install_unknown")` beside every
127+
`stale.append(...)` (and `scope_local(msg, key)` does both at
128+
`readiness.py:263`). Emit it: an additive `reason_details` list of
129+
`{text, severity, key}` alongside the existing flat `red_reasons` /
130+
`yellow_reasons` / `stale_reasons`. The flat lists and the verdict/score
131+
contract stay byte-for-byte unchanged, so every existing consumer — and an
132+
older Heart's persisted `release_ready.json` — behaves exactly as before.
133+
134+
2. **A remedy table in the dashboard**, keyed by that key, covering the gate
135+
keys that actually occur (`install_unknown`, `install_stale`,
136+
`install_non_release`, `test_unknown`, `test_stale`, `validation_absent`,
137+
`validation_stale`, `validation_stale_sha`, `validation_profile`,
138+
`validation_unknown`, `lib_unknown`, `lib_ci_unavailable`, `skew_unknown`).
139+
Each entry is `{command, prompt}`; `command` may be `None` when the remedy is
140+
genuinely a conversation (e.g. a rehearsal that needs a human's go-ahead),
141+
and then the prompt must still be specific — `/release rehearse` for the
142+
validation family, not `/health re-run the stale evidence: …`.
143+
144+
3. **Fall back, never guess.** A reason that arrives without a key (an older
145+
snapshot) keeps today's generic prompt. No string matching on reason text.
146+
147+
4. **Render it on every surface**, each in its own idiom, all from the same
148+
structured item:
149+
- `--html`: the reason row gains a command chip beside the existing prompt
150+
📋 (two payloads, distinct titles — "copy the command" / "copy the fix
151+
prompt"), so the Evidence-gaps block is finally actionable.
152+
- `--md`: the collapsed `📋 fix prompts` block gains the command as its own
153+
fenced line per gap (GitHub's copy button makes it one-tap).
154+
- `--json`: `blockers[]` gains `command` (additive; bump `schema_version`).
155+
- `--oneline`: unchanged.
156+
- README strip (`_render_md_brief`): today it deliberately prints no reasons
157+
for STALE. Reconsider *only* to the extent of one line naming the gap count
158+
and the single highest-value command; if that reads as noise on a glance
159+
surface, leave it alone and say so in the PR.
160+
161+
5. **A CLI door to match**, in the same family as the existing topics:
162+
`pyauto-heart fix stale` — read the persisted verdict and print each current
163+
gap with its command and prompt. This is what a terminal-first morning
164+
copies, and it keeps `heart/fix.py`'s "bundle context, emit a command"
165+
contract (it must not mutate anything).
166+
167+
6. **Brain leg (PyAutoBrain).** `board/_board.py:245 extract_heart_blockers()`
168+
forwards a fixed key set; add `command` and render it as a second chip in the
169+
Readiness & release rows. Same invariant as today — the Brain renders what
170+
the Heart sends and never re-derives a remedy.
171+
172+
## Acceptance
173+
174+
- On a board whose only reasons are the three gaps above, each Evidence-gaps row
175+
offers a command that, run on the dev box, clears that row on the next tick.
176+
- `pyauto-heart fix stale` prints the same commands the board shows, from the
177+
same verdict — the two surfaces cannot disagree.
178+
- `--json` blockers carry `command`; the Brain board renders it verbatim.
179+
- No stale reason anywhere emits a `/bug` prompt.
180+
- Verdict, score, `red_reasons` / `yellow_reasons` / `stale_reasons` and the
181+
release gate are unchanged: `tests/test_readiness.py` passes untouched, and
182+
new coverage lands in `tests/test_dashboard.py` (remedy per key, fallback for
183+
a keyless reason, json/md/html rendering).
184+
185+
## Out of scope
186+
187+
What counts as stale, the weights, the profiles, and the GREEN-for-release gate.
188+
This prompt is about the *hand* the board offers once a gap exists, not about
189+
which gaps exist.

complete/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Token-light navigation over the finished-work records (schema:
66
only then grep a dated bucket. Curators: edit the band between the CURATED
77
markers; everything below GENERATED is rebuilt.
88

9-
1128 records across 7 buckets.
9+
1129 records across 7 buckets.
1010

1111
<!-- CURATED:START -->
1212
## Highlights
@@ -227,6 +227,7 @@ _(curate hard-won records here — survives regeneration.)_
227227
- [spawn-empty-body-privacy-fix](2026/08/spawn-empty-body-privacy-fix.md)
228228
- [spawn-github-instance-automation](2026/08/spawn-github-instance-automation.md)
229229
- [sph-transform-name-check](2026/08/sph-transform-name-check.md) — auto-closed by the merge via "Closes #555"
230+
- [stale-remedies-on-the-heart-board](2026/08/stale-remedies-on-the-heart-board.md)
230231
- [stored-sample-reconstruction-guard](2026/08/stored-sample-reconstruction-guard.md) — stored samples that current model validation rejects no longer raise raw
231232
- [sub-312-install-tombstone](2026/08/sub-312-install-tombstone.md)
232233
- [tenant-firewall-drift-aug](2026/08/tenant-firewall-drift-aug.md) — issue #198; 9 → OK, and a green `--check` is only evidence for the organs actually checked out

dashboard.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<title>PyAutoMind Dashboard</title>
7-
<!-- generated by `pyauto-brain intake dashboard --apply` on 2026-08-24 — regenerate, do not hand-edit -->
7+
<!-- generated by `pyauto-brain intake dashboard --apply` on 2026-08-25 — regenerate, do not hand-edit -->
88
<style>:root{color-scheme:light dark;--bg:#fff;--fg:#1f2328;--muted:#59636e;
99
--line:#d8dee4;--btn:#f6f8fa;--ok:#1a7f37;--warn:#9a6700;--bad:#d1242f;
1010
--accent:#0a7d72;--tint:#0a7d7214;--edge:#0a7d723d;
@@ -186,7 +186,7 @@
186186
<body>
187187
<header class="hero"><span class="orb"><svg class="mark" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="24" cy="24" r="20.4"/><path d="M20.2,37.6 L20.2,33.6 C20.2,32.4 19.4,31.6 18.4,30.8 C16.8,29.6 15.8,28.2 15.6,26.6 C15.5,25.6 14.9,25.2 13.9,24.9 C12.6,24.5 12.3,23.7 13.0,22.7 C13.8,21.6 14.5,20.6 14.6,19.4 C15.0,15.4 18.2,12.2 22.4,11.8 C27.2,11.3 31.6,14.6 32.3,19.2 C32.8,22.4 31.6,25.0 30.6,27.0 C29.9,28.4 29.6,29.6 29.6,31.2 L29.6,37.6"/><circle cx="24.6" cy="19.6" r="4.4"/><path d="M22.5,19.7 L24.0,21.3 L26.8,18.1"/><path d="M34.2,16.5 L38.8,16.5 M35.2,21.0 L39.4,21.0 M33.6,25.5 L37.2,25.5"/><g fill="currentColor" stroke="none"><circle cx="40.1" cy="16.5" r="1.4"/><circle cx="40.7" cy="21.0" r="1.4"/><circle cx="38.5" cy="25.5" r="1.4"/></g></svg></span><h1>PyAuto<b>Mind</b><span class="kind">Dashboard</span></h1><div class="rule"></div><p class="tag">Intent. Priority. Flow.</p></header><p class="lede">Every task the Mind is holding. Tap a task's 📋 and its <code>/start_dev</code> command is on your clipboard — paste it into a Claude Code chat to route Claude straight to that task. <a href="#recent">Recent</a> is the same work by date — what has been happening rather than what to do next.</p>
188188
<ul class="stats"><li><b>1</b><span>In flight</span></li><li><b>3</b><span>Parked</span></li><li><b>5</b><span>Planned</span></li><li><b>139</b><span>Backlog</span></li></ul>
189-
<div class="fresh"><p><b>Last updated 2026-08-24.</b> This page is generated from <code>active/</code>, <code>draft/</code> and the registry files, so it is only as current as they are. <code>dashboard_refresh.yml</code> re-renders it on every push to <code>main</code> — that heals a stale page, but not a stale prompt: a task that shipped without its prompt advancing to <code>complete/</code> keeps rendering here as pickable backlog. Reconciling those is the refresh below.</p>
189+
<div class="fresh"><p><b>Last updated 2026-08-25.</b> This page is generated from <code>active/</code>, <code>draft/</code> and the registry files, so it is only as current as they are. <code>dashboard_refresh.yml</code> re-renders it on every push to <code>main</code> — that heals a stale page, but not a stale prompt: a task that shipped without its prompt advancing to <code>complete/</code> keeps rendering here as pickable backlog. Reconciling those is the refresh below.</p>
190190
<div class="task"><button class="copy" data-cmd="Bring the PyAutoMind dashboard up to date. Work in the PyAutoMind checkout:
191191
192192
1. `git fetch origin &amp;&amp; git status`. If behind `origin/main`, `git pull --ff-only`

dashboard.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# PyAutoMind Dashboard
22

3-
<!-- generated by `pyauto-brain intake dashboard --apply` on 2026-08-24 — regenerate, do not hand-edit -->
3+
<!-- generated by `pyauto-brain intake dashboard --apply` on 2026-08-25 — regenerate, do not hand-edit -->
44

55
This is the markdown version of the [PyAutoMind Dashboard](https://pyautolabs.github.io/PyAutoMind/), which puts a task's command on your clipboard with a single tap of 📋.
66

77
Every task the Mind is holding, on one page: what is in flight, what is parked, and the whole backlog to pick from. Pick a task and run its `/start_dev` command in a Claude Code chat to start it. [Recent](#recent) is the same work by date — what has been happening rather than what to do next.
88

9-
> **Last updated 2026-08-24.** This page is generated from `active/`, `draft/` and the registry files, so it is only as current as they are. `dashboard_refresh.yml` re-renders it on every push to `main` — that heals a stale page, but not a stale prompt: a task that shipped without its prompt advancing to `complete/` keeps rendering here as pickable backlog. Reconciling those is the refresh below.
9+
> **Last updated 2026-08-25.** This page is generated from `active/`, `draft/` and the registry files, so it is only as current as they are. `dashboard_refresh.yml` re-renders it on every push to `main` — that heals a stale page, but not a stale prompt: a task that shipped without its prompt advancing to `complete/` keeps rendering here as pickable backlog. Reconciling those is the refresh below.
1010
1111
<details><summary>📋 <b>Refresh this page</b> — reconcile finished prompts, then regenerate</summary>
1212

0 commit comments

Comments
 (0)